1
0
Fork 0
mirror of synced 2024-06-14 00:34:51 +12:00

Merge branch 'master' of github.com:appwrite/appwrite into feat-add-error-codes

This commit is contained in:
Christy Jacob 2022-02-08 20:56:03 +04:00
commit 661630c1c6
123 changed files with 3321 additions and 9415 deletions

View file

@ -1,9 +1,10 @@
name: "Tests"
on: [pull_request]
jobs:
tests:
name: Unit & E2E
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- name: Checkout repository
@ -18,29 +19,16 @@ jobs:
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Prepare Docker
- name: Build Appwrite
# Upstream bug causes buildkit pulls to fail so prefetch base images
# https://github.com/moby/moby/issues/41864
run: |
export COMPOSE_INTERACTIVE_NO_CLI
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
echo "_APP_FUNCTIONS_RUNTIMES=php-8.0" >> .env
docker pull composer:2.0
docker pull php:8.0-cli-alpine
docker compose pull
- name: Prepare Cache
uses: satackey/action-docker-layer-caching@v0.0.11
# Ignore the failure of a step and avoid terminating the job.
continue-on-error: true
- name: Build Appwrite
run: docker compose build --progress=plain
- name: Start Appwrite
run: |
docker compose build --progress=plain
docker compose up -d
sleep 30
sleep 10
- name: Doctor
run: docker compose exec -T appwrite doctor
@ -49,3 +37,9 @@ jobs:
- name: Run Tests
run: docker compose exec -T appwrite test --debug
- name: Teardown
if: always()
run: |
docker compose down -v
docker ps -aq | xargs docker rm --force

View file

@ -9,9 +9,10 @@
</p>
<!-- [![Hacktoberfest](https://img.shields.io/static/v1?label=hacktoberfest&message=friendly&color=90a88b&style=flat-square)](https://hacktoberfest.appwrite.io) -->
<!-- [![Build Status](https://img.shields.io/travis/com/appwrite/appwrite?style=flat-square)](https://travis-ci.com/appwrite/appwrite) -->
[![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord?r=Github)
[![Docker Pulls](https://img.shields.io/docker/pulls/appwrite/appwrite?color=f02e65&style=flat-square)](https://hub.docker.com/r/appwrite/appwrite)
[![Build Status](https://img.shields.io/travis/com/appwrite/appwrite?style=flat-square)](https://travis-ci.com/appwrite/appwrite)
[![Build Status](https://img.shields.io/github/workflow/status/appwrite/appwrite/Tests?label=tests&style=flat-square)](https://github.com/appwrite/appwrite/actions)
[![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
[![Translate](https://img.shields.io/badge/translate-f02e65?style=flat-square)](docs/tutorials/add-translations.md)
[![Swag Store](https://img.shields.io/badge/swag%20store-f02e65?style=flat-square)](https://store.appwrite.io)
@ -171,4 +172,4 @@ Join our growing community around the world! See our official [Blog](https://med
## License
This repository is available under the [BSD 3-Clause License](./LICENSE).
This repository is available under the [BSD 3-Clause License](./LICENSE).

File diff suppressed because it is too large Load diff

View file

@ -99,8 +99,8 @@ $collections = [
'$id' => '_fulltext_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [1024],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -574,8 +574,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -1106,8 +1106,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
[
'$id' => '_key_deleted_email',
@ -1158,7 +1158,29 @@ $collections = [
'filters' => [],
],
[
'$id' => 'providerToken',
'$id' => 'providerAccessToken',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => 'providerAccessTokenExpiry',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'providerRefreshToken',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
@ -1422,8 +1444,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -1720,8 +1742,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -1892,8 +1914,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -1983,8 +2005,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -2118,8 +2140,8 @@ $collections = [
'$id' => '_fulltext_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [16384],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -2295,7 +2317,7 @@ $collections = [
],
],
],
'realtime' => [
'$collection' => Database::METADATA,
'$id' => 'realtime',
@ -2347,4 +2369,4 @@ $collections = [
],
];
return $collections;
return $collections;

View file

@ -82,6 +82,11 @@ return [
'model' => Response::MODEL_SESSION,
'note' => '',
],
'account.sessions.update' => [
'description' => 'This event triggers when the account session is updated.',
'model' => Response::MODEL_SESSION,
'note' => '',
],
'database.collections.create' => [
'description' => 'This event triggers when a database collection is created.',
'model' => Response::MODEL_COLLECTION,

View file

@ -116,7 +116,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '0.3.1',
'version' => '0.3.3',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@ -370,7 +370,7 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '0.2.2',
'version' => '0.2.5',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,

View file

@ -127,7 +127,7 @@ return [ // Ordered by ABC.
'icon' => 'icon-windows',
'enabled' => true,
'sandbox' => false,
'form' => false,
'form' => 'microsoft.phtml',
'beta' => false,
'mock' => false,
],
@ -287,6 +287,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false
],
'stripe' => [
'name' => 'Stripe',
'developers' => 'https://stripe.com/docs/api',
'icon' => 'icon-stripe',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false
],
// Keep Last
'mock' => [
'name' => 'Mock',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -3,7 +3,6 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Detector\Detector;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
@ -12,6 +11,7 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Template\Template;
use Appwrite\URL\URL as URLParser;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Database\Validator\CustomId;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
@ -23,6 +23,7 @@ use Utopia\Database\Validator\UID;
use Appwrite\Extend\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -315,7 +316,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(1024), 'OAuth2 code.')
->param('code', '', new Text(2048), 'OAuth2 code.')
->param('state', '', new Text(2048), 'Login state params.', true)
->inject('request')
->inject('response')
@ -342,7 +343,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(1024), 'OAuth2 code.')
->param('code', '', new Text(2048), 'OAuth2 code.')
->param('state', '', new Text(2048), 'Login state params.', true)
->inject('request')
->inject('response')
@ -370,7 +371,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->label('abuse-key', 'ip:{ip}')
->label('docs', false)
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(1024), 'OAuth2 code.')
->param('code', '', new Text(2048), 'OAuth2 code.')
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
->inject('request')
->inject('response')
@ -430,7 +431,10 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
}
$state['failure'] = null;
$accessToken = $oauth2->getAccessToken($code);
$refreshToken =$oauth2->getRefreshToken($code);
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
if (empty($accessToken)) {
if (!empty($state['failure'])) {
@ -528,7 +532,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'userId' => $user->getId(),
'provider' => $provider,
'providerUid' => $oauth2ID,
'providerToken' => $accessToken,
'providerAccessToken' => $accessToken,
'providerRefreshToken' => $refreshToken,
'providerAccessTokenExpiry' => \time() + (int) $accessTokenExpiry,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@ -1198,6 +1204,7 @@ App::get('/v1/account/logs')
'account.update.password',
'account.update.prefs',
'account.sessions.create',
'account.sessions.update',
'account.sessions.delete',
'account.recovery.create',
'account.recovery.update',
@ -1593,8 +1600,8 @@ App::delete('/v1/account/sessions/:sessionId')
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
$sessions = $user->getAttribute('sessions', []);
@ -1647,6 +1654,108 @@ App::delete('/v1/account/sessions/:sessionId')
throw new Exception('Session not found', 404, Exception::USER_SESSION_NOT_FOUND);
});
App::patch('/v1/account/sessions/:sessionId')
->desc('Update Session (Refresh Tokens)')
->groups(['api', 'account'])
->label('scope', 'account')
->label('event', 'account.sessions.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateSession')
->label('sdk.description', '/docs/references/account/update-session.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->param('sessionId', null, new UID(), 'Session ID. Use the string \'current\' to update the current device session.')
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('audits')
->inject('events')
->inject('usage')
->action(function ($sessionId, $request, $response, $user, $dbForProject, $project, $locale, $audits, $events, $usage) {
/** @var Appwrite\Utopia\Request $request */
/** @var boolean $force */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$sessionId = ($sessionId === 'current')
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
$sessions = $user->getAttribute('sessions', []);
foreach ($sessions as $key => $session) {/** @var Document $session */
if ($sessionId == $session->getId()) {
// Comment below would skip re-generation if token is still valid
// We decided to not include this because developer can get expiration date from the session
// I kept code in comment because it might become relevant in the future
// $expireAt = (int) $session->getAttribute('providerAccessTokenExpiry');
// if(\time() < $expireAt - 5) { // 5 seconds time-sync and networking gap, to be safe
// return $response->noContent();
// }
$provider = $session->getAttribute('provider');
$refreshToken = $session->getAttribute('providerRefreshToken');
$appId = $project->getAttribute('providers', [])[$provider.'Appid'] ?? '';
$appSecret = $project->getAttribute('providers', [])[$provider.'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider);
if (!\class_exists($className)) {
throw new Exception('Provider is not supported', 501);
}
$oauth2 = new $className($appId, $appSecret, '', [], []);
$oauth2->refreshTokens($refreshToken);
$session
->setAttribute('providerAccessToken', $oauth2->getAccessToken(''))
->setAttribute('providerRefreshToken', $oauth2->getRefreshToken(''))
->setAttribute('providerAccessTokenExpiry', \time() + (int) $oauth2->getAccessTokenExpiry(''))
;
$dbForProject->updateDocument('sessions', $sessionId, $session);
$user->setAttribute("sessions", $sessions);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.update')
->setParam('resource', 'user/' . $user->getId())
;
$events
->setParam('eventData', $response->output($session, Response::MODEL_SESSION))
;
$usage
->setParam('users.sessions.update', 1)
->setParam('users.update', 1)
;
return $response->dynamic($session, Response::MODEL_SESSION);
}
}
throw new Exception('Session not found', 404);
});
App::delete('/v1/account/sessions')
->desc('Delete All Account Sessions')
->groups(['api', 'account'])

View file

@ -27,10 +27,10 @@ use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\Structure as StructureException;
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
@ -2012,14 +2012,18 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach ($data['$read'] as $read) {
if (!Authorization::isRole($read)) {
throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400, Exception::USER_UNAUTHORIZED);
if(!is_null($read)) {
foreach ($data['$read'] as $read) {
if (!Authorization::isRole($read)) {
throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400, Exception::USER_UNAUTHORIZED);
}
}
}
foreach ($data['$write'] as $write) {
if (!Authorization::isRole($write)) {
throw new Exception('Write permissions must be one of: ('.\implode(', ', $roles).')', 400, Exception::USER_UNAUTHORIZED);
if(!is_null($write)) {
foreach ($data['$write'] as $write) {
if (!Authorization::isRole($write)) {
throw new Exception('Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED);
}
}
}
}

View file

@ -2,7 +2,7 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\CustomId;
use Utopia\Database\Validator\UID;
use Utopia\Storage\Storage;
use Utopia\Storage\Validator\File;

View file

@ -2,10 +2,10 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
@ -537,7 +537,7 @@ App::delete('/v1/projects/:projectId')
->inject('deletes')
->action(function ($projectId, $password, $response, $user, $dbForConsole, $deletes) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Document $user */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Appwrite\Event\Event $deletes */

View file

@ -1,31 +1,31 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Appwrite\Extend\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\HexColor;
use Utopia\Cache\Cache;
use Utopia\Cache\Adapter\Filesystem;
use Appwrite\ClamAV\Network;
use Utopia\Config\Config;
use Utopia\Database\Validator\Authorization;
use Appwrite\Database\Validator\CustomId;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Image\Image;
use Utopia\Storage\Storage;
use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileSize;
use Utopia\Storage\Validator\Upload;
use Utopia\Storage\Compression\Algorithms\GZIP;
use Utopia\Image\Image;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\HexColor;
App::post('/v1/storage/files')
->desc('Create File')

View file

@ -1,19 +1,15 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Detector\Detector;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Appwrite\Extend\Exception;
use Utopia\Config\Config;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
@ -22,6 +18,10 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
App::post('/v1/teams')
->desc('Create Team')

View file

@ -2,25 +2,25 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Detector\Detector;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Appwrite\Extend\Exception;
use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\Boolean;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Appwrite\Extend\Exception;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\UID;
use Appwrite\Detector\Detector;
use Appwrite\Database\Validator\CustomId;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\Boolean;
App::post('/v1/users')
->desc('Create User')
@ -289,6 +289,7 @@ App::get('/v1/users/:userId/logs')
'account.update.password',
'account.update.prefs',
'account.sessions.create',
'account.sessions.update',
'account.sessions.delete',
'account.recovery.create',
'account.recovery.update',

View file

@ -13,9 +13,6 @@ use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Appwrite\Auth\Auth;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Response\Filters\V06;
use Appwrite\Utopia\Response\Filters\V07;
use Appwrite\Utopia\Response\Filters\V08;
use Appwrite\Utopia\Response\Filters\V11;
use Utopia\CLI\Console;
use Utopia\Database\Document;
@ -159,15 +156,6 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
$responseFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
switch($responseFormat) {
case version_compare ($responseFormat , '0.6.2', '<=') :
Response::setFilter(new V06());
break;
case version_compare ($responseFormat , '0.7.2', '<=') :
Response::setFilter(new V07());
break;
case version_compare ($responseFormat , '0.8.0', '<=') :
Response::setFilter(new V08());
break;
case version_compare ($responseFormat , '0.11.0', '<=') :
Response::setFilter(new V11());
break;
@ -337,8 +325,8 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project, $l
if($logger) {
if($error->getCode() >= 500 || $error->getCode() === 0) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
/** @var Appwrite\Database\Document $user */
} catch(\Throwable $th) {
// All good, user is optional information for logger
}

View file

@ -10,6 +10,7 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
use Utopia\Storage\Validator\File;
use Utopia\Validator\WhiteList;
App::get('/v1/mock/tests/foo')
->desc('Get Foo')
@ -478,11 +479,13 @@ App::get('/v1/mock/tests/general/oauth2/token')
->label('docs', false)
->label('sdk.mock', true)
->param('client_id', '', new Text(100), 'OAuth2 Client ID.')
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.')
->param('client_secret', '', new Text(100), 'OAuth2 scope list.')
->param('code', '', new Text(100), 'OAuth2 state.')
->param('grant_type', 'authorization_code', new WhiteList(['refresh_token', 'authorization_code']), 'OAuth2 Grant Type.', true)
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.', true)
->param('code', '', new Text(100), 'OAuth2 state.', true)
->param('refresh_token', '', new Text(100), 'OAuth2 refresh token.', true)
->inject('response')
->action(function ($client_id, $redirectURI, $client_secret, $code, $response) {
->action(function ($client_id, $client_secret, $grantType, $redirectURI, $code, $refreshToken, $response) {
/** @var Appwrite\Utopia\Response $response */
if ($client_id != '1') {
@ -493,11 +496,27 @@ App::get('/v1/mock/tests/general/oauth2/token')
throw new Exception('Invalid client secret');
}
if ($code != 'abcdef') {
throw new Exception('Invalid token');
}
$responseJson = [
'access_token' => '123456',
'refresh_token' => 'tuvwxyz',
'expires_in' => 14400
];
$response->json(['access_token' => '123456']);
if($grantType === 'authorization_code') {
if ($code !== 'abcdef') {
throw new Exception('Invalid token');
}
$response->json($responseJson);
} else if($grantType === 'refresh_token') {
if ($refreshToken !== 'tuvwxyz') {
throw new Exception('Invalid refresh token');
}
$response->json($responseJson);
} else {
throw new Exception('Invalid grant type');
}
});
App::get('/v1/mock/tests/general/oauth2/user')
@ -517,7 +536,7 @@ App::get('/v1/mock/tests/general/oauth2/user')
$response->json([
'id' => 1,
'name' => 'User Name',
'email' => 'user@localhost.test',
'email' => 'useroauth@localhost.test',
]);
});

View file

@ -1,13 +1,13 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Messaging\Adapter\Realtime;
use Utopia\App;
use Appwrite\Extend\Exception;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;

View file

@ -193,8 +193,8 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$logger = $app->getResource("logger");
if($logger) {
try {
/** @var Utopia\Database\Document $user */
$user = $app->getResource('user');
/** @var Appwrite\Database\Document $user */
} catch(\Throwable $_th) {
// All good, user is optional information for logger
}

View file

@ -22,7 +22,6 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database as DatabaseOld;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
@ -63,7 +62,7 @@ const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10000;
const APP_CACHE_BUSTER = 201;
const APP_VERSION_STABLE = '0.12.1';
const APP_VERSION_STABLE = '0.13.0';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@ -160,43 +159,6 @@ if(!empty($user) || !empty($pass)) {
Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '').':'.App::getEnv('_APP_REDIS_PORT', ''));
}
/**
* Old DB Filters
*/
DatabaseOld::addFilter('json',
function($value) {
if(!is_array($value)) {
return $value;
}
return json_encode($value);
},
function($value) {
return json_decode($value, true);
}
);
DatabaseOld::addFilter('encrypt',
function($value) {
$key = App::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$tag = null;
return json_encode([
'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
'method' => OpenSSL::CIPHER_AES_128_GCM,
'iv' => bin2hex($iv),
'tag' => bin2hex($tag),
'version' => '1',
]);
},
function($value) {
$value = json_decode($value, true);
$key = App::getEnv('_APP_OPENSSL_KEY_V'.$value['version']);
return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag']));
}
);
/**
* New DB Filters
*/
@ -493,11 +455,12 @@ $register->set('geodb', function () {
});
$register->set('db', function () { // This is usually for our workers or CLI commands scope
$dbHost = App::getEnv('_APP_DB_HOST', '');
$dbPort = App::getEnv('_APP_DB_PORT', '');
$dbUser = App::getEnv('_APP_DB_USER', '');
$dbPass = App::getEnv('_APP_DB_PASS', '');
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
$pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,

View file

@ -1,18 +1,17 @@
<?php
global $cli, $register, $projectDB, $console;
global $cli, $register;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
use Appwrite\Database\Adapter\Redis as RedisAdapter;
use Appwrite\Migration\Migration;
use Utopia\App;
use Utopia\Cache\Cache;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Text;
Config::load('collections.old', __DIR__ . '/../config/collections.old.php');
$cli
->task('migrate')
->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true)
@ -23,86 +22,36 @@ $cli
Console::exit(1);
return;
}
$options = [];
if (str_starts_with($version, '0.12.')) {
Console::error('--------------------');
Console::error('WARNING');
Console::error('--------------------');
Console::warning('Migrating to Version 0.12.x introduces a major breaking change within the Database Service!');
Console::warning('Before migrating, please read about the breaking changes here:');
Console::info('https://dev.to/appwrite/appwrite-012-migration-post-3cha');
$confirm = Console::confirm("If you want to proceed, type 'yes':");
if ($confirm != 'yes') {
Console::exit(1);
return;
}
Console::log('');
Console::log('Collections');
Console::log('--------------------');
Console::warning('Be aware that following actions will happen during the migration:');
Console::warning('- Nested Document rules will be migrated to String attributes');
Console::warning('- Numeric rules will be migrated to float attributes');
Console::warning('- Wildcard and Markdown rules will be converted to string attributes');
Console::info("Do you want to migrate your Database Collections?");
$options['migrateCollections'] = Console::confirm("Type 'yes' or 'no':");
if ($options['migrateCollections'] === 'yes') {
Console::log('');
Console::log('Documents');
Console::log('------------------');
Console::warning('Be aware that following actions will happen during the migration:');
Console::warning('- Nested Documents will be stored as JSON values');
Console::warning('- All Numeric values will be converted to float');
Console::warning('- All Wildcard and Markdown values will be converted to string');
Console::info("Do you want to migrate your Database Documents?");
$options['migrateDocuments'] = Console::confirm("Type 'yes' or 'no':");
} else {
$options['migrateDocuments'] = 'no';
}
if (
!in_array($options['migrateDocuments'], ['yes', 'no'])
|| !in_array($options['migrateCollections'], ['yes', 'no'])
) {
Console::error("You must reply with 'yes' or 'no'!");
Console::exit(1);
return;
}
}
Config::load('collectionsold', __DIR__ . '/../config/collections.old.php');
$app = new App('UTC');
Console::success('Starting Data Migration to version ' . $version);
$db = $register->get('db', true);
$cache = $register->get('cache', true);
$cache->flushAll();
$consoleDB = new Database();
$consoleDB
->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache))
->setNamespace('app_console') // Main DB
->setMocks(Config::getParam('collectionsold', []));
$cache = new Cache(new RedisCache($cache));
$projectDB = new Database();
$projectDB
->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache))
->setMocks(Config::getParam('collectionsold', []));
$projectDB = new Database(new MariaDB($db), $cache);
$projectDB->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$console = $consoleDB->getDocument('console');
$consoleDB = new Database(new MariaDB($db), $cache);
$consoleDB->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$consoleDB->setNamespace('_project_console');
$console = $app->getResource('console');
$limit = 30;
$sum = 30;
$offset = 0;
$projects = [$console];
$count = 0;
$totalProjects = $consoleDB->count('projects') + 1;
$class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version];
$migration = new $class($register->get('db'), $register->get('cache'), $options);
$migration = new $class();
while ($sum > 0) {
while (!empty($projects)) {
foreach ($projects as $project) {
try {
$migration
@ -114,23 +63,14 @@ $cli
}
}
$projects = $consoleDB->getCollection([
'limit' => $limit,
'offset' => $offset,
'filters' => [
'$collection=' . Database::SYSTEM_COLLECTION_PROJECTS,
],
]);
$sum = \count($projects);
$projects = $consoleDB->find('projects', limit: $limit, offset: $offset);
$offset = $offset + $limit;
$count = $count + $sum;
if ($sum > 0) {
Console::log('Fetched ' . $count . '/' . $consoleDB->getSum() . ' projects...');
}
Console::log('Migrated ' . $count . '/' . $totalProjects . ' projects...');
}
$cache->flushAll();
Swoole\Event::wait(); // Wait for Coroutines to finish
Console::success('Data Migration Completed');

View file

@ -527,15 +527,6 @@ $logs = $this->getParam('logs', null);
<hr class="margin-top-small" />
<div class="row">
<div class="col span-1"><input name="permission" value="document" type="radio" class="margin-top-no" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-11">
<b>Document Level</b>
<p class="text-fade margin-top-tiny">With Document Level permissions, you have granular access control over every document. Users will only be able to access documents for which they have explicit permissions.</p>
<p class="text-fade margin-top-tiny">In this permission level, document permissions take precedence and collection permissions are ignored.</p>
</div>
</div>
<div class="row">
<div class="col span-1"><input name="permission" value="collection" type="radio" class="margin-top-tiny" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-11">
@ -554,6 +545,15 @@ $logs = $this->getParam('logs', null);
</div>
</div>
<div class="row">
<div class="col span-1"><input name="permission" value="document" type="radio" class="margin-top-no" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-11">
<b>Document Level</b>
<p class="text-fade margin-top-tiny">With Document Level permissions, you have granular access control over every document. Users will only be able to access documents for which they have explicit permissions.</p>
<p class="text-fade margin-top-tiny">In this permission level, document permissions take precedence and collection permissions are ignored.</p>
</div>
</div>
<hr class="margin-top-no" />
<button>Update</button>
@ -880,10 +880,10 @@ $logs = $this->getParam('logs', null);
<div class="margin-bottom-large">
<template x-if="!(array || required)">
<input name="xdefault" class="button switch" type="checkbox" />
<input name="xdefault" class="button switch" type="checkbox" />
</template>
<template x-if="(array || required)">
<input name="xdefault" class="button switch" type="checkbox" disabled />
<input name="" class="button switch" type="checkbox" disabled />
</template>
&nbsp; Default Value <span class="tooltip" data-tooltip="Whether this attribute is set to true or false on creation"><i class="icon-info-circled"></i></span>
</div>

View file

@ -169,8 +169,11 @@ $logs = $this->getParam('logs', null);
</template>
<template x-if="attr.format === 'enum'">
<select
:required="attr.required"
:name="attr.key"
data-cast-to="string">
<option :disabled="attr.required" selected label=" "></option>
<template x-for="element in attr.elements">
<option
:value="element"
@ -261,9 +264,12 @@ $logs = $this->getParam('logs', null);
</template>
<template x-if="attr.format === 'enum'">
<select
:required="attr.required"
:name="attr.key"
data-cast-to="string">
<template x-for="element in attr.elements">
<option :disabled="attr.required" selected label=" "></option>
<option
:value="element"
x-text="element"

View file

@ -108,7 +108,7 @@
<label for="collection-name">Name</label>
<input type="text" class="full-width" id="collection-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="collection-permission" name="permission" required value="document" />
<input type="hidden" id="collection-permission" name="permission" required value="collection" />
<input type="hidden" id="collection-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />

View file

@ -137,7 +137,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
</div>
</div>
<p class="margin-top-small"><i class="icon-info-circled"></i> Data is aggregated and updated every 15 minutes</p>
<p class="margin-top-small text-size-small"><i class="icon-info-circled"></i> Data is aggregated and updated every 15 minutes</p>
</div>
</div>
</div>

View file

@ -1,4 +1,6 @@
<?php
use Appwrite\Utopia\View;
$providers = $this->getParam('providers', []);
$auth = $this->getParam('auth', []);
$smtpEnabled = $this->getParam('smtpEnabled', false);
@ -475,9 +477,12 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret">App Secret</label>
<input name="secret" data-forms-show-secret id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="password" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}">
<?php else: ?>
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">Bundle ID <span class="tooltip" data-tooltip="Attribute internal display name"><i class="icon-info-circled"></i></span></label>
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}" placeholder="com.company.appname" />
<input name="secret" data-forms-oauth-apple id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="hidden" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}" />
<?php
$form = new View(__DIR__.'/oauth/'.$this->escape($form));
echo $form
->setParam("provider", $provider)
->render();
?>
<?php endif; ?>
<div class="info row thin margin-bottom margin-top">

View file

@ -0,0 +1,13 @@
<?php
$provider = $this->getParam('provider', '');
?>
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">Bundle ID <span class="tooltip" data-tooltip="Attribute internal display name"><i class="icon-info-circled"></i></span></label>
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}" placeholder="com.company.appname" />
<input name="secret" data-forms-oauth-custom="<?php echo $this->escape(ucfirst($provider)); ?>" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="hidden" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}" />
<div>
<div class="row thin">
<div class="col span-6"><label>Key ID</label><input id="oauth2AppleKeyId" type="text" placeholder="SHAB13ROFN"></div>
<div class="col span-6"><label>Team ID</label><input id="oauth2AppleTeamId" type="text" placeholder="ELA2CD3AED"></div>
</div><label>P8 File</label><textarea id="oauth2AppleP8" class="margin-bottom-no"></textarea>
</div>

View file

@ -0,0 +1,12 @@
<?php
$provider = $this->getParam('provider', '');
?>
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">Application (Client) ID<span class="tooltip" data-tooltip="Provided by AzureAD"><i class="icon-info-circled"></i></span></label>
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}" placeholder="Application ID" />
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>ClientSecret">Client Secret <span class="tooltip" data-tooltip="Created by you in AzureAD Portal"><i class="icon-info-circled"></i></span></label>
<input name="appSecret" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>ClientSecret" type="password" autocomplete="off" placeholder="Client Secret" />
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>TenantId">Target Tenant<span class="tooltip" data-tooltip="'common', 'organizations', 'consumers' or your TenantId"><i class="icon-info-circled"></i></span><a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints">More info</a></label>
<input name="appSecret" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>TenantId" type="text" autocomplete="off" placeholder="'common', 'organizations', 'consumers' or your TenantId" />
<?php /*Hidden input for the final secret. Gets filled with a JSON via JS. */ ?>
<input name="secret" data-forms-oauth-custom="<?php echo $this->escape(ucfirst($provider)); ?>" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="hidden" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}" />

View file

@ -81,6 +81,23 @@ class [PROVIDER NAME] extends OAuth2
*/
private $endpoint = '[ENDPOINT API URL]';
/**
* @var array
*/
protected $scopes = [
// [ARRAY_OF_REQUIRED_SCOPES]
];
/**
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
*/
@ -101,14 +118,31 @@ class [PROVIDER NAME] extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
// TODO: Fire request to oauth API to generate access_token
$accessToken = "[FETCHED ACCESS TOKEN]";
return $accessToken;
if(empty($this->tokens)) {
// TODO: Fire request to oauth API to generate access_token
// Make sure to use '$this->getScopes()' to include all scopes properly
$this->tokens = "[FETCH TOKEN RESPONSE]";
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
// TODO: Fire request to oauth API to generate access_token using refresh token
$this->tokens = "[FETCH TOKEN RESPONSE]";
return $this->tokens;
}
/**
@ -118,8 +152,10 @@ class [PROVIDER NAME] extends OAuth2
*/
public function getUserID(string $accessToken): string
{
// TODO: Fetch user from oauth API and select the user ID
$userId = "[FETCHED USER ID]";
$user = $this->getUser($accessToken);
// TODO: Pick user ID from $user response
$userId = "[USER ID]";
return $userId;
}
@ -131,8 +167,10 @@ class [PROVIDER NAME] extends OAuth2
*/
public function getUserEmail(string $accessToken): string
{
// TODO: Fetch user from oauth API and select the user's email
$userEmail = "[FETCHED USER EMAIL]";
$user = $this->getUser($accessToken);
// TODO: Pick user email from $user response
$userEmail = "[USER EMAIL]";
return $userEmail;
}
@ -144,16 +182,35 @@ class [PROVIDER NAME] extends OAuth2
*/
public function getUserName(string $accessToken): string
{
// TODO: Fetch user from oauth API and select the username
$username = "[FETCHED USERNAME]";
$user = $this->getUser($accessToken);
// TODO: Pick username from $user response
$username = "[USERNAME]";
return $username;
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken)
{
if (empty($this->user)) {
// TODO: Fire request to oauth API to get information about users
$this->user = "[FETCH USER RESPONSE]";
}
return $this->user;
}
}
```
> If you copy this template, make sure to replace all placeholders wrapped like `[THIS]` and to implement everything marked as `TODO:`.
> If your OAuth2 provider has different endpoints for getting username/email/id, you can fire specific requests from specific get-method, and stop using `getUser` method.
Please mention in your documentation what resources or API docs you used to implement the provider's OAuth2 protocol.
## 3. Test your provider
@ -175,4 +232,34 @@ If everything goes well, raise a pull request and be ready to respond to any fee
First of all, commit the changes with the message `Added XXX OAuth2 Provider` and push it. This will publish a new branch to your forked version of Appwrite. If you visit it at `github.com/YOUR_USERNAME/appwrite`, you will see a new alert saying you are ready to submit a pull request. Follow the steps GitHub provides, and at the end, you will have your pull request submitted.
## 🤕 Stuck ?
If you need any help with the contribution, feel free to head over to [our discord channel](https://appwrite.io/discord) and we'll be happy to help you out.
## 😉 Need more freedom
If your OAuth provider requires special configuration apart from `clientId` and `clientSecret` you can create a custom form. Currently this is being realized through putting all custom fields as JSON into the `clientSecret` field to keep the project API stable. You can implement your custom form following these steps:
1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/providers.php`.
```php
<?php
$provider = $this->getParam('provider', '');
?>
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">Application (Client) ID<span class="tooltip" data-tooltip="Provided by AzureAD"><i class="icon-info-circled"></i></span></label>
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}" placeholder="Application ID" />
<?php /*Hidden input for the final secret. Gets filled with a JSON via JS. */ ?>
<input name="secret" data-forms-oauth-custom="<?php echo $this->escape(ucfirst($provider)); ?>" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="hidden" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}" />
<!-- [Your custom form inputs go here] -->
```
2. Add the config for creating the JSON in `public/scripts/views/forms/oauth-custom.js` using this template
```js
{
"[Provider]":{
"[JSON property name 1]":"[html element Id 1]",
"[JSON property name 2]":"[html element Id 2]"
}
}
```
3. In your provider class `src/Appwrite/Auth/OAuth2/[Provider].php` add logic to decode the JSON using the same property names.

View file

@ -57,7 +57,7 @@ const configApp = {
'public/scripts/views/forms/move-down.js',
'public/scripts/views/forms/move-up.js',
'public/scripts/views/forms/nav.js',
'public/scripts/views/forms/oauth-apple.js',
'public/scripts/views/forms/oauth-custom.js',
'public/scripts/views/forms/password-meter.js',
'public/scripts/views/forms/pell.js',
'public/scripts/views/forms/required.js',

17
package-lock.json generated
View file

@ -12,7 +12,7 @@
"chart.js": "^3.7.0",
"markdown-it": "^12.3.2",
"pell": "^1.0.6",
"prismjs": "^1.25.0",
"prismjs": "^1.26.0",
"turndown": "^7.1.1"
},
"devDependencies": {
@ -3566,9 +3566,12 @@
}
},
"node_modules/prismjs": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.26.0.tgz",
"integrity": "sha512-HUoH9C5Z3jKkl3UunCyiD5jwk0+Hz0fIgQ2nbwU2Oo/ceuTAQAg+pPVnfdt2TJWRVLcxKh9iuoYDUSc8clb5UQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
@ -7978,9 +7981,9 @@
"dev": true
},
"prismjs": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.26.0.tgz",
"integrity": "sha512-HUoH9C5Z3jKkl3UunCyiD5jwk0+Hz0fIgQ2nbwU2Oo/ceuTAQAg+pPVnfdt2TJWRVLcxKh9iuoYDUSc8clb5UQ=="
},
"process-nextick-args": {
"version": "2.0.1",

View file

@ -20,7 +20,7 @@
"chart.js": "^3.7.0",
"markdown-it": "^12.3.2",
"pell": "^1.0.6",
"prismjs": "^1.25.0",
"prismjs": "^1.26.0",
"turndown": "^7.1.1"
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -778,10 +778,14 @@ list["filters-"+filter.key]=params[key][i];}}}}
return list;};let apply=function(params){let cached=container.get(name);cached=cached?cached.params:[];params=Object.assign(cached,params);container.set(name,{name:name,params:params,query:serialize(params),forward:parseInt(params.offset)+parseInt(params.limit),backward:parseInt(params.offset)-parseInt(params.limit),keys:flatten(params)},true,name);document.dispatchEvent(new CustomEvent(name+"-changed",{bubbles:false,cancelable:true}));};switch(element.tagName){case"INPUT":break;case"TEXTAREA":break;case"BUTTON":element.addEventListener("click",function(){apply(JSON.parse(expression.parse(element.dataset["params"]||"{}")));});break;case"FORM":element.addEventListener("input",function(){apply(form.toJson(element));});element.addEventListener("change",function(){apply(form.toJson(element));});element.addEventListener("reset",function(){setTimeout(function(){apply(form.toJson(element));},0);});events=events.trim().split(",");for(let y=0;y<events.length;y++){if(events[y]==="init"){element.addEventListener("rendered",function(){apply(form.toJson(element));},{once:true});}else{}
element.setAttribute("data-event","none");}
break;default:break;}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-headers",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.value=key.value.toLowerCase()+":"+value.value.toLowerCase();};let syncB=function(){let split=element.value.toLowerCase().split(":");key.value=split[0]||"";value.value=split[1]||"";key.value=key.value.trim();value.value=value.value.trim();};syncB();}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-key-value",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";key.required=true;value.type="text";value.className="margin-bottom-no";value.placeholder="Value";value.required=true;wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.name=key.value;element.value=value.value;};let syncB=function(){key.value=element.name||"";value.value=element.value||"";};syncB();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-down",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-down]")).map(function(obj){obj.addEventListener("click",function(){if(element.nextElementSibling){console.log('down',element.offsetHeight);element.parentNode.insertBefore(element.nextElementSibling,element);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-up",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-up]")).map(function(obj){obj.addEventListener("click",function(){if(element.previousElementSibling){console.log('up',element);element.parentNode.insertBefore(element,element.previousElementSibling);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-nav",repeat:false,controller:function(element,view,container,document){let titles=document.querySelectorAll('[data-forms-nav-anchor]');let links=element.querySelectorAll('[data-forms-nav-link]');let minLink=null;let check=function(){let minDistance=null;let minElement=null;for(let i=0;i<titles.length;++i){let title=titles[i];let distance=title.getBoundingClientRect().top;console.log(i);if((minDistance===null||minDistance>=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');}
console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-apple",controller:function(element){let container=document.createElement("div");let row=document.createElement("div");let col1=document.createElement("div");let col2=document.createElement("div");let keyID=document.createElement("input");let keyLabel=document.createElement("label");let teamID=document.createElement("input");let teamLabel=document.createElement("label");let p8=document.createElement("textarea");let p8Label=document.createElement("label");keyLabel.textContent='Key ID';teamLabel.textContent='Team ID';p8Label.textContent='P8 File';row.classList.add('row');row.classList.add('thin');container.appendChild(row);container.appendChild(p8Label);container.appendChild(p8);row.appendChild(col1);row.appendChild(col2);col1.classList.add('col');col1.classList.add('span-6');col1.appendChild(keyLabel);col1.appendChild(keyID);col2.classList.add('col');col2.classList.add('span-6');col2.appendChild(teamLabel);col2.appendChild(teamID);keyID.type='text';keyID.placeholder='SHAB13ROFN';teamID.type='text';teamID.placeholder='ELA2CD3AED';p8.accept='.p8';p8.classList.add('margin-bottom-no');element.parentNode.insertBefore(container,element.nextSibling);element.addEventListener('change',sync);keyID.addEventListener('change',update);teamID.addEventListener('change',update);p8.addEventListener('change',update);function update(){let json={};json.keyID=keyID.value;json.teamID=teamID.value;json.p8=p8.value;element.value=JSON.stringify(json);}
console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-custom",controller:function(element){let providers={"Microsoft":{"clientSecret":"oauth2MicrosoftClientSecret","tenantId":"oauth2MicrosoftTenantId"},"Apple":{"keyId":"oauth2AppleKeyId","teamId":"oauth2AppleTeamId","p8":"oauth2AppleP8"}}
let provider=element.getAttribute("data-forms-oauth-custom");if(!provider||!providers.hasOwnProperty(provider)){console.error("Provider for custom form not set or unkown")}
let config=providers[provider];element.addEventListener('change',sync);let elements={};for(const key in config){if(Object.hasOwnProperty.call(config,key)){elements[key]=document.getElementById(config[key]);elements[key].addEventListener('change',update);}}
function update(){let json={};for(const key in elements){if(Object.hasOwnProperty.call(elements,key)){json[key]=elements[key].value}}
element.value=JSON.stringify(json);}
function sync(){if(!element.value){return;}
let json={};try{json=JSON.parse(element.value);}catch(error){console.error('Failed to parse secret key');}
teamID.value=json.teamID||'';keyID.value=json.keyID||'';p8.value=json.p8||'';}
for(const key in elements){if(Object.hasOwnProperty.call(elements,key)){elements[key].value=json[key]||'';}}}
sync();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-password-meter",controller:function(element,window){var calc=function(password){var score=0;if(!password)return score;var letters=new window.Object();for(var i=0;i<password.length;i++){letters[password[i]]=(letters[password[i]]||0)+1;score+=5.0/letters[password[i]];}
var variations={digits:/\d/.test(password),lower:/[a-z]/.test(password),upper:/[A-Z]/.test(password),nonWords:/\W/.test(password)};var variationCount=0;for(var check in variations){if(variations.hasOwnProperty(check)){variationCount+=variations[check]===true?1:0;}}
score+=(variationCount-1)*10;return parseInt(score);};var callback=function(){var score=calc(this.value);if(""===this.value)return(meter.className="password-meter");if(score>60)return(meter.className="password-meter strong");if(score>30)return(meter.className="password-meter medium");if(score>=0)return(meter.className="password-meter weak");};var meter=window.document.createElement("div");meter.className="password-meter";element.parentNode.insertBefore(meter,element.nextSibling);element.addEventListener("change",callback);element.addEventListener("keypress",callback);element.addEventListener("keyup",callback);element.addEventListener("keydown",callback);}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-pell",controller:function(element,window,document,markdown,rtl){var div=document.createElement("div");element.className="pell hide";div.className="input pell";element.parentNode.insertBefore(div,element);element.tabIndex=-1;var turndownService=new TurndownService();turndownService.addRule("underline",{filter:["u"],replacement:function(content){return"__"+content+"__";}});var editor=window.pell.init({element:div,onChange:function onChange(html){alignText();element.value=turndownService.turndown(html);},defaultParagraphSeparator:"p",actions:[{name:"bold",icon:'<i class="icon-bold"></i>'},{name:"underline",icon:'<i class="icon-underline"></i>'},{name:"italic",icon:'<i class="icon-italic"></i>'},{name:"olist",icon:'<i class="icon-list-numbered"></i>'},{name:"ulist",icon:'<i class="icon-list-bullet"></i>'},{name:"link",icon:'<i class="icon-link"></i>'}]});var clean=function(e){e.stopPropagation();e.preventDefault();var clipboardData=e.clipboardData||window.clipboardData;console.log(clipboardData.getData("Text"));window.pell.exec("insertText",clipboardData.getData("Text"));return true;};var alignText=function(){let paragraphs=editor.content.querySelectorAll('p,li');let last='';for(let paragraph of paragraphs){var content=paragraph.textContent;if(content.trim()===''){content=last.textContent;}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -1,93 +0,0 @@
(function(window) {
"use strict";
window.ls.container.get("view").add({
selector: "data-forms-oauth-apple",
controller: function(element) {
let container = document.createElement("div");
let row = document.createElement("div");
let col1 = document.createElement("div");
let col2 = document.createElement("div");
let keyID = document.createElement("input");
let keyLabel = document.createElement("label");
let teamID = document.createElement("input");
let teamLabel = document.createElement("label");
let p8 = document.createElement("textarea");
let p8Label = document.createElement("label");
keyLabel.textContent = 'Key ID';
teamLabel.textContent = 'Team ID';
p8Label.textContent = 'P8 File';
row.classList.add('row');
row.classList.add('thin');
container.appendChild(row);
container.appendChild(p8Label);
container.appendChild(p8);
row.appendChild(col1);
row.appendChild(col2);
col1.classList.add('col');
col1.classList.add('span-6');
col1.appendChild(keyLabel);
col1.appendChild(keyID);
col2.classList.add('col');
col2.classList.add('span-6');
col2.appendChild(teamLabel);
col2.appendChild(teamID);
keyID.type = 'text';
keyID.placeholder = 'SHAB13ROFN';
teamID.type = 'text';
teamID.placeholder = 'ELA2CD3AED';
p8.accept = '.p8';
p8.classList.add('margin-bottom-no');
element.parentNode.insertBefore(container, element.nextSibling);
element.addEventListener('change', sync);
keyID.addEventListener('change', update);
teamID.addEventListener('change', update);
p8.addEventListener('change', update);
function update() {
let json = {};
json.keyID = keyID.value;
json.teamID = teamID.value;
json.p8 = p8.value;
element.value = JSON.stringify(json);
}
function sync() {
if(!element.value) {
return;
}
let json = {};
try {
json = JSON.parse(element.value);
} catch (error) {
console.error('Failed to parse secret key');
}
teamID.value = json.teamID || '';
keyID.value = json.keyID || '';
p8.value = json.p8 || '';
}
// function syncB() {
// picker.value = element.value;
// }
// element.parentNode.insertBefore(preview, element);
// update();
sync();
}
});
})(window);

View file

@ -0,0 +1,73 @@
(function (window) {
"use strict";
//TODO: Make this generic
window.ls.container.get("view").add({
selector: "data-forms-oauth-custom",
controller: function (element) {
// provider configuration for custom forms. Keys will be property names in JSON, values the elementIDs for the according inputs
let providers = {
"Microsoft": {
"clientSecret": "oauth2MicrosoftClientSecret",
"tenantId": "oauth2MicrosoftTenantId"
},
"Apple": {
"keyId": "oauth2AppleKeyId",
"teamId": "oauth2AppleTeamId",
"p8": "oauth2AppleP8"
}
}
let provider = element.getAttribute("data-forms-oauth-custom");
if (!provider || !providers.hasOwnProperty(provider)) { console.error("Provider for custom form not set or unkown") }
let config = providers[provider];
// Add Change Listeners for element
element.addEventListener('change', sync);
// Get all inputs by id and register change event listener
let elements = {};
for (const key in config) {
if (Object.hasOwnProperty.call(config, key)) {
elements[key] = document.getElementById(config[key]);
elements[key].addEventListener('change', update);
}
}
// Build the JSON based on input in custom input fields
function update() {
let json = {};
for (const key in elements) {
if (Object.hasOwnProperty.call(elements, key)) {
json[key] = elements[key].value
}
}
element.value = JSON.stringify(json);
}
// When the JSON changes (on load) change values in custom input fields
function sync() {
if (!element.value) {
return;
}
let json = {};
try {
json = JSON.parse(element.value);
} catch (error) {
console.error('Failed to parse secret key');
}
for (const key in elements) {
if (Object.hasOwnProperty.call(elements, key)) {
elements[key].value = json[key] || '';
}
}
}
sync();
}
});
})(window);

View file

@ -1,32 +1,31 @@
// .loader {
// height: 0;
// direction: ltr;
// position: fixed;
// top: 0;
// width: 0;
// margin: 0 auto;
// .func-start(0);
// opacity: 0;
// z-index: 4;
// }
.loader {
height: 0;
direction: ltr;
position: fixed;
top: 0;
width: 0;
margin: 0 auto;
.func-start(0);
opacity: 0;
z-index: 4;
}
// .load-start ~ .loader {
// width: 100%;
// background: var(--config-color-focus);
// height: 3px;
// opacity: 1;
// transition: width .3s ease-out, opacity .5s ease-out;
// -moz-transition: width .3s ease-out, opacity .5s ease-out;
// -webkit-transition: width .3s ease-out, opacity .5s ease-out;
// -o-transition: width .3s ease-out, opacity .5s ease-out;
// }
.load-start ~ .loader {
width: 100%;
background: var(--config-color-focus);
height: 3px;
opacity: 1;
transition: width .3s ease-out, opacity .5s ease-out;
-moz-transition: width .3s ease-out, opacity .5s ease-out;
-webkit-transition: width .3s ease-out, opacity .5s ease-out;
-o-transition: width .3s ease-out, opacity .5s ease-out;
}
// .www .load-start ~ .loader {
// background: #fff;
// }
.www .load-start ~ .loader {
background: #fff;
}
/*
.load-start > * {
opacity: 0;
}
@ -37,7 +36,7 @@
-moz-transition: opacity .3s ease-out;
-webkit-transition: opacity .3s ease-out;
-o-transition: opacity .3s ease-out;
}*/
}
[data-ls-if] {
display: none;

View file

@ -62,9 +62,16 @@ abstract class OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
abstract public function getAccessToken(string $code):string;
abstract protected function getTokens(string $code):array;
/**
* @param string $refreshToken
*
* @return array
*/
abstract public function refreshTokens(string $refreshToken):array;
/**
* @param $accessToken
@ -109,6 +116,38 @@ abstract class OAuth2
return $this->scopes;
}
/**
* @param string $code
*
* @return string
*/
public function getAccessToken(string $code):string
{
$tokens = $this->getTokens($code);
return $tokens['access_token'] ?? '';
}
/**
* @param string $code
*
* @return string
*/
public function getRefreshToken(string $code):string
{
$tokens = $this->getTokens($code);
return $tokens['refresh_token'] ?? '';
}
/**
* @param string $code
*
* @return string
*/
public function getAccessTokenExpiry(string $code):string
{
$tokens = $this->getTokens($code);
return $tokens['expires_in'] ?? '';
}
// The parseState function was designed specifically for Amazon OAuth2 Adapter to override.
// The response from Amazon is html encoded and hence it needs to be html_decoded before

View file

@ -15,6 +15,11 @@ class Amazon extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -59,31 +64,54 @@ class Amazon extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8'];
$this->tokens = \json_decode($this->request(
'POST',
'https://api.amazon.com/auth/o2/token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://api.amazon.com/auth/o2/token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID ,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback ,
'grant_type' => 'authorization_code'
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
$accessToken = \json_decode($accessToken, true);
), true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -14,6 +14,11 @@ class Apple extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -54,34 +59,60 @@ class Apple extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://appleid.apple.com/auth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->getAppSecret(),
'redirect_uri' => $this->callback,
])
), true);
$this->claims = (isset($this->tokens['id_token'])) ? \explode('.', $this->tokens['id_token']) : [0 => '', 1 => ''];
$this->claims = (isset($this->claims[1])) ? \json_decode(\base64_decode($this->claims[1]), true) : [];
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://appleid.apple.com/auth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->getAppSecret(),
'redirect_uri' => $this->callback,
])
);
), true);
$accessToken = \json_decode($accessToken, true);
$this->claims = (isset($accessToken['id_token'])) ? \explode('.', $accessToken['id_token']) : [0 => '', 1 => ''];
$this->claims = (isset($this->claims[1])) ? \json_decode(\base64_decode($this->claims[1]), true) : [];
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
$this->claims = (isset($this->tokens['id_token'])) ? \explode('.', $this->tokens['id_token']) : [0 => '', 1 => ''];
$this->claims = (isset($this->claims[1])) ? \json_decode(\base64_decode($this->claims[1]), true) : [];
return $this->tokens;
}
/**

View file

@ -13,6 +13,11 @@ class Bitbucket extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -43,32 +48,54 @@ class Bitbucket extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
// Required as per Bitbucket Spec.
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://bitbucket.org/site/oauth2/access_token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
// Required as per Bitbucket Spec.
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://bitbucket.org/site/oauth2/access_token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'authorization_code'
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
// Reference Material
// https://dev.bitly.com/v4_documentation.html
@ -29,6 +30,11 @@ class Bitly extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -54,31 +60,52 @@ class Bitly extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$response = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->resourceEndpoint . 'oauth/access_token',
["Content-Type: application/x-www-form-urlencoded"],
\http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"redirect_uri" => $this->callback,
"state" => \json_encode($this->state)
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->resourceEndpoint . 'oauth/access_token',
["Content-Type: application/x-www-form-urlencoded"],
\http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"redirect_uri" => $this->callback,
"state" => \json_encode($this->state)
"refresh_token" => $refreshToken,
'grant_type' => 'refresh_token'
])
);
), true);
$result = null;
if ($response) {
\parse_str($response, $result);
return $result['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -23,6 +23,11 @@ class Box extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -59,32 +64,55 @@ class Box extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$header = "Content-Type: application/x-www-form-urlencoded";
$accessToken = $this->request(
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'token',
$headers,
\http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"grant_type" => "authorization_code",
"scope" => \implode(',', $this->getScopes()),
"redirect_uri" => $this->callback
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'token',
[$header],
$headers,
\http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"grant_type" => "authorization_code",
"scope" => \implode(',', $this->getScopes()),
"redirect_uri" => $this->callback
"refresh_token" => $refreshToken,
"grant_type" => "refresh_token",
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (array_key_exists('access_token', $accessToken)) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -18,6 +18,11 @@ class Discord extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -55,31 +60,53 @@ class Discord extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth2/token',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->callback,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'scope' => \implode(' ', $this->getScopes())
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth2/token',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->callback,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'scope' => \implode(' ', $this->getScopes())
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -14,6 +14,11 @@ class Dropbox extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -44,31 +49,54 @@ class Dropbox extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://api.dropboxapi.com/oauth2/token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://api.dropboxapi.com/oauth2/token',
$headers,
\http_build_query([
'code' => $code,
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
'grant_type' => 'refresh_token'
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Facebook extends OAuth2
{
@ -15,6 +16,11 @@ class Facebook extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -47,27 +53,48 @@ class Facebook extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'GET',
'https://graph.facebook.com/' . $this->version . '/oauth/access_token?' . \http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'GET',
'https://graph.facebook.com/'.$this->version.'/oauth/access_token?'.\http_build_query([
'https://graph.facebook.com/' . $this->version . '/oauth/access_token?' . \http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Github extends OAuth2
{
@ -10,6 +11,11 @@ class Github extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -42,31 +48,51 @@ class Github extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://github.com/login/oauth/access_token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://github.com/login/oauth/access_token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
), true);
$output = [];
\parse_str($accessToken, $output);
if (isset($output['access_token'])) {
return $output['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -13,6 +13,11 @@ class Gitlab extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -46,28 +51,48 @@ class Gitlab extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
'POST',
'https://gitlab.com/oauth/token?'.\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
])
);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://gitlab.com/oauth/token?' . \http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
])
), true);
}
return '';
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://gitlab.com/oauth/token?' . \http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token'
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**

View file

@ -29,6 +29,11 @@ class Google extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -55,29 +60,49 @@ class Google extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
'POST',
'https://oauth2.googleapis.com/token?'.\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'scope' => null,
'grant_type' => 'authorization_code'
])
);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth2.googleapis.com/token?' . \http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'scope' => null,
'grant_type' => 'authorization_code'
])
), true);
}
return '';
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth2.googleapis.com/token?' . \http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token'
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**

View file

@ -10,6 +10,11 @@ class Linkedin extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -57,30 +62,53 @@ class Linkedin extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://www.linkedin.com/oauth/v2/accessToken',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->callback,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://www.linkedin.com/oauth/v2/accessToken',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'redirect_uri' => $this->callback,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -14,6 +14,11 @@ class Microsoft extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -36,7 +41,7 @@ class Microsoft extends OAuth2
*/
public function getLoginURL(): string
{
return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?'.\http_build_query([
return 'https://login.microsoftonline.com/'.$this->getTenantId().'/oauth2/v2.0/authorize?'.\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'state'=> \json_encode($this->state),
@ -49,33 +54,55 @@ class Microsoft extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$accessToken = $this->request(
'POST',
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'grant_type' => 'authorization_code'
])
);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://login.microsoftonline.com/' . $this->getTenantId() . '/oauth2/v2.0/token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->getClientSecret(),
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'grant_type' => 'authorization_code'
])
), true);
}
return '';
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://login.microsoftonline.com/' . $this->getTenantId() . '/oauth2/v2.0/token',
$headers,
\http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->getClientSecret(),
'grant_type' => 'refresh_token'
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
@ -141,4 +168,39 @@ class Microsoft extends OAuth2
return $this->user;
}
/**
* Extracts the Client Secret from the JSON stored in appSecret
* @return string
*/
protected function getClientSecret(): string
{
$secret = $this->decodeJson();
return (isset($secret['clientSecret'])) ? $secret['clientSecret'] : '';
}
/**
* Decode the JSON stored in appSecret
* @return array
*/
protected function decodeJson(): array
{
try {
$secret = \json_decode($this->appSecret, true);
} catch (\Throwable $th) {
throw new Exception('Invalid secret');
}
return $secret;
}
/**
* Extracts the Tenant Id from the JSON stored in appSecret. Defaults to 'common' as a fallback
* @return string
*/
protected function getTenantId(): string
{
$secret = $this->decodeJson();
return (isset($secret['tenantId'])) ? $secret['tenantId'] : 'common';
}
}

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Mock extends OAuth2
{
@ -22,6 +23,11 @@ class Mock extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -47,28 +53,49 @@ class Mock extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
'GET',
'http://localhost/'.$this->version.'/mock/tests/general/oauth2/token?'.
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
])
);
$accessToken = \json_decode($accessToken, true); //
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'GET',
'http://localhost/' . $this->version . '/mock/tests/general/oauth2/token?' .
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
])
), true);
}
return '';
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'GET',
'http://localhost/' . $this->version . '/mock/tests/general/oauth2/token?' .
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**

View file

@ -20,6 +20,11 @@ class Notion extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -51,32 +56,50 @@ class Notion extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$headers = [
"Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret),
];
if(empty($this->tokens)) {
$headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'redirect_uri' => $this->callback,
'code' => $code
])
), true);
}
$response = $this->request(
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'redirect_uri' => $this->callback,
'code' => $code
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
])
);
), true);
$response = \json_decode($response, true);
if (isset($response['access_token'])) {
return $response['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -34,6 +34,11 @@ class Paypal extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -74,29 +79,47 @@ class Paypal extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->resourceEndpoint[$this->environment] . 'oauth2/token',
['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)],
\http_build_query([
'code' => $code,
'grant_type' => 'authorization_code',
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->resourceEndpoint[$this->environment] . 'oauth2/token',
['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)],
\http_build_query([
'code' => $code,
'grant_type' => 'authorization_code',
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token',
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -15,6 +15,11 @@ class Salesforce extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -59,32 +64,56 @@ class Salesforce extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$this->tokens = \json_decode($this->request(
'POST',
'https://login.salesforce.com/services/oauth2/token',
$headers,
\http_build_query([
'code' => $code,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = [
"Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret),
"Content-Type: application/x-www-form-urlencoded",
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://login.salesforce.com/services/oauth2/token',
$headers,
\http_build_query([
'code' => $code,
'redirect_uri' => $this->callback ,
'grant_type' => 'authorization_code'
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
);
$accessToken = \json_decode($accessToken, true);
), true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Slack extends OAuth2
{
@ -10,6 +11,11 @@ class Slack extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -46,28 +52,48 @@ class Slack extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
// https://api.slack.com/docs/oauth#step_3_-_exchanging_a_verification_code_for_an_access_token
$accessToken = $this->request(
'GET',
'https://slack.com/api/oauth.access?'.\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'code' => $code,
'redirect_uri' => $this->callback
])
);
$accessToken = \json_decode($accessToken, true); //
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
// https://api.slack.com/docs/oauth#step_3_-_exchanging_a_verification_code_for_an_access_token
$this->tokens = \json_decode($this->request(
'GET',
'https://slack.com/api/oauth.access?' . \http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'code' => $code,
'redirect_uri' => $this->callback
])
), true);
}
return '';
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'GET',
'https://slack.com/api/oauth.access?' . \http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**

View file

@ -31,6 +31,11 @@ class Spotify extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -58,27 +63,50 @@ class Spotify extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$header = "Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret);
$result = \json_decode($this->request(
if(empty($this->tokens)) {
$headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'api/token',
$headers,
\http_build_query([
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'api/token',
[$header],
$headers,
\http_build_query([
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
"refresh_token" => $refreshToken,
"grant_type" => "refresh_token",
])
), true);
if (isset($result['access_token'])) {
return $result['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -0,0 +1,182 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Stripe extends OAuth2
{
/**
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var string
*/
protected $stripeAccountId = '';
/**
* @var array
*/
protected $scopes = [
'read_write',
];
/**
* @return string
*/
protected $grantType = [
'authorize' => 'authorization_code',
'refresh' => 'refresh_token',
];
/**
* @return string
*/
public function getName():string
{
return 'stripe';
}
/**
* @return string
*/
public function getLoginURL():string
{
return 'https://connect.stripe.com/oauth/authorize?'. \http_build_query([
'response_type' => 'code', // The only option at the moment is "code."
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($this->state)
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://connect.stripe.com/oauth/token',
[],
\http_build_query([
'grant_type' => $this->grantType['authorize'],
'code' => $code
])
), true);
$this->stripeAccountId = $this->tokens['stripe_user_id'];
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://connect.stripe.com/oauth/token',
[],
\http_build_query([
'grant_type' => $this->grantType['refresh'],
'refresh_token' => $refreshToken,
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
$this->stripeAccountId = $this->tokens['stripe_user_id'];
return $this->tokens;
}
/**
* @param $accessToken
*
* @return string
*/
public function getUserID(string $accessToken):string
{
$user = $this->getUser($accessToken);
if (isset($user['id'])) {
return $user['id'];
}
return '';
}
/**
* @param $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken):string
{
$user = $this->getUser($accessToken);
if(empty($user)) {
return '';
}
return $user['email'] ?? '';
}
/**
* @param $accessToken
*
* @return string
*/
public function getUserName(string $accessToken):string
{
$user = $this->getUser($accessToken);
if (isset($user['name'])) {
return $user['name'];
}
return '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken)
{
if (empty($this->user) && !empty($this->stripeAccountId)) {
$this->user = \json_decode(
$this->request(
'GET',
'https://api.stripe.com/v1/accounts/' . $this->stripeAccountId,
['Authorization: Bearer '.\urlencode($accessToken)]
),
true
);
}
return $this->user;
}
}

View file

@ -33,6 +33,11 @@ class Tradeshift extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
protected $scopes = [
@ -69,23 +74,47 @@ class Tradeshift extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$response = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint[$this->environment] . 'auth/token',
['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint[$this->environment] . 'auth/token',
['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
])
);
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
$accessToken = \json_decode($response, true);
return $accessToken['access_token'] ?? '';
return $this->tokens;
}
/**

View file

@ -31,6 +31,11 @@ class Twitch extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -59,26 +64,48 @@ class Twitch extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$result = \json_decode($this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'token?' . \http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'token?'. \http_build_query([
$this->endpoint . 'token?' . \http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
"refresh_token" => $refreshToken,
"grant_type" => "refresh_token",
])
), true);
if (isset($result['access_token'])) {
return $result['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
// Reference Material
// https://vk.com/dev/first_guide
@ -16,6 +17,11 @@ class Vk extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -57,36 +63,59 @@ class Vk extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8'];
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth.vk.com/access_token?',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback
])
), true);
$this->user['email'] = $this->tokens['email'];
$this->user['user_id'] = $this->tokens['user_id'];
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth.vk.com/access_token?',
$headers,
\http_build_query([
'code' => $code,
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback
'grant_type' => 'refresh_token'
])
);
$accessToken = \json_decode($accessToken, true);
), true);
if (isset($accessToken['email'])) {
$this->user['email'] = $accessToken['email'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
if (isset($accessToken['user_id'])) {
$this->user['user_id'] = $accessToken['user_id'];
}
$this->user['email'] = $this->tokens['email'];
$this->user['user_id'] = $this->tokens['user_id'];
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
}
return '';
return $this->tokens;
}
/**

View file

@ -13,6 +13,11 @@ class WordPress extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -46,30 +51,52 @@ class WordPress extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://public-api.wordpress.com/oauth2/token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'grant_type' => 'authorization_code',
'code' => $code
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://public-api.wordpress.com/oauth2/token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'grant_type' => 'authorization_code',
'code' => $code
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -32,6 +32,11 @@ class Yahoo extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -70,31 +75,58 @@ class Yahoo extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$header = [
"Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret),
"Content-Type: application/x-www-form-urlencoded",
if(empty($this->tokens)) {
$headers = [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'get_token',
$headers,
\http_build_query([
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$result = \json_decode($this->request(
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'get_token',
$header,
$headers,
\http_build_query([
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
"refresh_token" => $refreshToken,
"grant_type" => "refresh_token",
])
), true);
if (isset($result['access_token'])) {
return $result['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -18,6 +18,11 @@ class Yammer extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -44,31 +49,53 @@ class Yammer extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'access_token?',
$headers,
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'code' => $code,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'access_token?',
$headers,
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'code' => $code,
'grant_type' => 'authorization_code'
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token']['token'])) {
return $accessToken['access_token']['token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -15,6 +15,11 @@ class Yandex extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -56,31 +61,55 @@ class Yandex extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth.yandex.com/token',
$headers,
\http_build_query([
'code' => $code,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = [
"Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret),
"Content-Type: application/x-www-form-urlencoded",
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth.yandex.com/token',
$headers,
\http_build_query([
'code' => $code,
'refresh_token' => $refreshToken,
'grant_type' => 'authorization_code'
])
);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -1,161 +0,0 @@
<?php
namespace Appwrite\Database;
use Exception;
abstract class Adapter
{
/**
* @var string
*/
protected $namespace = '';
/**
* Set Namespace.
*
* Set namespace to divide different scope of data sets
*
* @param $namespace
*
* @throws Exception
*
* @return bool
*/
public function setNamespace($namespace)
{
if (empty($namespace)) {
throw new Exception('Missing namespace');
}
$this->namespace = $namespace;
return true;
}
/**
* Get Namespace.
*
* Get namespace of current set scope
*
* @throws Exception
*
* @return string
*/
public function getNamespace()
{
if (empty($this->namespace)) {
throw new Exception('Missing namespace');
}
return $this->namespace;
}
/**
* Get Document.
*
* @param string $id
*
* @return array
*/
abstract public function getDocument($id);
/**
* Create Document
**.
*
* @param array $data
*
* @return array
*/
abstract public function createDocument(array $data = [], array $unique = []);
/**
* Update Document.
*
* @param array $data
*
* @return array
*/
abstract public function updateDocument(array $data = []);
/**
* Delete Node.
*
* @param string $id
*
* @return array
*/
abstract public function deleteDocument(string $id);
/**
* Delete Unique Key.
*
* @param string $key
*
* @return array
*/
abstract public function deleteUniqueKey($key);
/**
* Add Unique Key.
*
* @param string $key
*
* @return array
*/
abstract public function addUniqueKey($key);
/**
* Create Namespace.
*
* @param string $namespace
*
* @return bool
*/
abstract public function createNamespace($namespace);
/**
* Delete Namespace.
*
* @param string $namespace
*
* @return bool
*/
abstract public function deleteNamespace($namespace);
/**
* Filter.
*
* Filter data sets using chosen queries
*
* @param array $options
* @param array $filterTypes
*
* @return array
*/
abstract public function getCollection(array $options, array $filterTypes = []);
/**
* @param array $options
*
* @return int
*/
abstract public function getCount(array $options);
/**
* Last Modified.
*
* Return Unix timestamp of last time a node queried in corrent session has been changed
*
* @return int
*/
abstract public function lastModified();
/**
* Get Debug Data.
*
* @return array
*/
abstract public function getDebug();
}

View file

@ -1,986 +0,0 @@
<?php
namespace Appwrite\Database\Adapter;
use Appwrite\Database\Adapter;
use Appwrite\Database\Exception\Duplicate;
use Appwrite\Database\Validator\Authorization;
use Exception;
use PDO;
use Redis;
class MySQL extends Adapter
{
const DATA_TYPE_STRING = 'string';
const DATA_TYPE_INTEGER = 'integer';
const DATA_TYPE_FLOAT = 'float';
const DATA_TYPE_BOOLEAN = 'boolean';
const DATA_TYPE_OBJECT = 'object';
const DATA_TYPE_DICTIONARY = 'dictionary';
const DATA_TYPE_ARRAY = 'array';
const DATA_TYPE_NULL = 'null';
const OPTIONS_LIMIT_ATTRIBUTES = 1000;
/**
* Last modified.
*
* Read node with most recent changes
*
* @var int
*/
protected $lastModified = -1;
/**
* @var array
*/
protected $debug = [];
/**
* @var PDO
*/
protected $pdo;
/**
* @var Redis
*/
protected $redis;
/**
* Constructor.
*
* Set connection and settings
*
* @param PDO $pdo
* @param Redis $redis
*/
public function __construct($pdo, Redis $redis)
{
$this->pdo = $pdo;
$this->redis = $redis;
}
/**
* Get Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function getDocument($id)
{
// Get fields abstraction
$st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.documents` a
WHERE a.uid = :uid AND a.status = 0
ORDER BY a.updatedAt DESC LIMIT 10;
');
$st->bindValue(':uid', $id, PDO::PARAM_STR);
$st->execute();
$document = $st->fetch();
if (empty($document)) { // Not Found
return [];
}
// Get fields abstraction
$st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.properties` a
WHERE a.documentUid = :documentUid AND a.documentRevision = :documentRevision
ORDER BY `order`
');
$st->bindParam(':documentUid', $document['uid'], PDO::PARAM_STR, 32);
$st->bindParam(':documentRevision', $document['revision'], PDO::PARAM_STR, 32);
$st->execute();
$properties = $st->fetchAll();
$output = [
'$id' => null,
'$collection' => null,
'$permissions' => (!empty($document['permissions'])) ? \json_decode($document['permissions'], true) : [],
];
foreach ($properties as &$property) {
\settype($property['value'], $property['primitive']);
if ($property['array']) {
$output[$property['key']][] = $property['value'];
} else {
$output[$property['key']] = $property['value'];
}
}
// Get fields abstraction
$st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.relationships` a
WHERE a.start = :start AND revision = :revision
ORDER BY `order`
');
$st->bindParam(':start', $document['uid'], PDO::PARAM_STR, 32);
$st->bindParam(':revision', $document['revision'], PDO::PARAM_STR, 32);
$st->execute();
$output['temp-relations'] = $st->fetchAll();
return $output;
}
/**
* Create Document.
*
* @param array $data
*
* @throws \Exception
*
* @return array
*/
public function createDocument(array $data = [], array $unique = [])
{
$order = 0;
$data = \array_merge(['$id' => null, '$permissions' => []], $data); // Merge data with default params
$signature = \md5(\json_encode($data));
$revision = \uniqid('', true);
$data['$id'] = (empty($data['$id'])) ? null : $data['$id'];
/*
* When updating node, check if there are any changes to update
* by comparing data md5 signatures
*/
if (null !== $data['$id']) {
$st = $this->getPDO()->prepare('SELECT signature FROM `'.$this->getNamespace().'.database.documents` a
WHERE a.uid = :uid AND a.status = 0
ORDER BY a.updatedAt DESC LIMIT 1;
');
$st->bindValue(':uid', $data['$id'], PDO::PARAM_STR);
$st->execute();
$result = $st->fetch();
if ($result && isset($result['signature'])) {
$oldSignature = $result['signature'];
if ($signature === $oldSignature) {
return $data;
}
}
}
/**
* Check Unique Keys
*/
foreach ($unique as $key => $value) {
$st = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.unique`
SET `key` = :key;
');
$st->bindValue(':key', \md5($data['$collection'].':'.$key.'='.$value), PDO::PARAM_STR);
if (!$st->execute()) {
throw new Duplicate('Duplicated Property: '.$key.'='.$value);
}
}
// Add or update fields abstraction level
$st1 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.documents`
SET uid = :uid, createdAt = :createdAt, updatedAt = :updatedAt, signature = :signature, revision = :revision, permissions = :permissions, status = 0
ON DUPLICATE KEY UPDATE uid = :uid, updatedAt = :updatedAt, signature = :signature, revision = :revision, permissions = :permissions;
');
// Adding fields properties
if (null === $data['$id'] || !isset($data['$id'])) { // Get new fields UID
$data['$id'] = $this->getId();
}
$st1->bindValue(':uid', $data['$id'], PDO::PARAM_STR);
$st1->bindValue(':revision', $revision, PDO::PARAM_STR);
$st1->bindValue(':signature', $signature, PDO::PARAM_STR);
$st1->bindValue(':createdAt', \date('Y-m-d H:i:s', \time()), PDO::PARAM_STR);
$st1->bindValue(':updatedAt', \date('Y-m-d H:i:s', \time()), PDO::PARAM_STR);
$st1->bindValue(':permissions', \json_encode($data['$permissions']), PDO::PARAM_STR);
$st1->execute();
// Delete old properties
$rms1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.properties` WHERE documentUid = :documentUid AND documentRevision != :documentRevision');
$rms1->bindValue(':documentUid', $data['$id'], PDO::PARAM_STR);
$rms1->bindValue(':documentRevision', $revision, PDO::PARAM_STR);
$rms1->execute();
// Delete old relationships
$rms2 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.relationships` WHERE start = :start AND revision != :revision');
$rms2->bindValue(':start', $data['$id'], PDO::PARAM_STR);
$rms2->bindValue(':revision', $revision, PDO::PARAM_STR);
$rms2->execute();
// Create new properties
$st2 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.properties`
(`documentUid`, `documentRevision`, `key`, `value`, `primitive`, `array`, `order`)
VALUES (:documentUid, :documentRevision, :key, :value, :primitive, :array, :order)');
$props = [];
foreach ($data as $key => $value) { // Prepare properties data
if (\in_array($key, ['$permissions'])) {
continue;
}
$type = $this->getDataType($value);
// Handle array of relations
if (self::DATA_TYPE_ARRAY === $type) {
if (!is_array($value)) { // Property should be of type array, if not = skip
continue;
}
foreach ($value as $i => $child) {
if (self::DATA_TYPE_DICTIONARY !== $this->getDataType($child)) { // not dictionary
$props[] = [
'type' => $this->getDataType($child),
'key' => $key,
'value' => $child,
'array' => true,
'order' => $order++,
];
continue;
}
$data[$key][$i] = $this->createDocument($child);
$this->createRelationship($revision, $data['$id'], $data[$key][$i]['$id'], $key, true, $i);
}
continue;
}
// Handle relation
if (self::DATA_TYPE_DICTIONARY === $type) {
$value = $this->createDocument($value);
$this->createRelationship($revision, $data['$id'], $value['$id'], $key); //xxx
continue;
}
// Handle empty values
if (self::DATA_TYPE_NULL === $type) {
continue;
}
$props[] = [
'type' => $type,
'key' => $key,
'value' => $value,
'array' => false,
'order' => $order++,
];
}
foreach ($props as $prop) {
if (\is_array($prop['value'])) {
throw new Exception('Value can\'t be an array: '.\json_encode($prop['value']));
}
if (\is_bool($prop['value'])) {
$prop['value'] = (int) $prop['value'];
}
$st2->bindValue(':documentUid', $data['$id'], PDO::PARAM_STR);
$st2->bindValue(':documentRevision', $revision, PDO::PARAM_STR);
$st2->bindValue(':key', $prop['key'], PDO::PARAM_STR);
$st2->bindValue(':value', $prop['value'], PDO::PARAM_STR);
$st2->bindValue(':primitive', $prop['type'], PDO::PARAM_STR);
$st2->bindValue(':array', $prop['array'], PDO::PARAM_BOOL);
$st2->bindValue(':order', $prop['order'], PDO::PARAM_STR);
$st2->execute();
}
// TODO remove this dependency (check if related to nested documents)
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
return $data;
}
/**
* Update Document.
*
* @param array $data
*
* @return array
*
* @throws Exception
*/
public function updateDocument(array $data = [])
{
return $this->createDocument($data);
}
/**
* Delete Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function deleteDocument(string $id)
{
$st1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.documents`
WHERE uid = :id
');
$st1->bindValue(':id', $id, PDO::PARAM_STR);
$st1->execute();
$st2 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.properties`
WHERE documentUid = :id
');
$st2->bindValue(':id', $id, PDO::PARAM_STR);
$st2->execute();
$st3 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.relationships`
WHERE start = :id OR end = :id
');
$st3->bindValue(':id', $id, PDO::PARAM_STR);
$st3->execute();
return [];
}
/**
* Delete Unique Key.
*
* @param string $key
*
* @return array
*
* @throws Exception
*/
public function deleteUniqueKey($key)
{
$st1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.unique` WHERE `key` = :key');
$st1->bindValue(':key', $key, PDO::PARAM_STR);
$st1->execute();
return [];
}
/**
* Add Unique Key.
*
* @param string $key
*
* @return array
*
* @throws Exception
*/
public function addUniqueKey($key)
{
$st = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.unique`
SET `key` = :key;
');
$st->bindValue(':key', $key, PDO::PARAM_STR);
if (!$st->execute()) {
throw new Duplicate('Duplicated Property: '.$key);
}
return [];
}
/**
* Create Relation.
*
* Adds a new relationship between different nodes
*
* @param string $revision
* @param int $start
* @param int $end
* @param string $key
* @param bool $isArray
* @param int $order
*
* @return array
*
* @throws Exception
*/
protected function createRelationship($revision, $start, $end, $key, $isArray = false, $order = 0)
{
$st2 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.relationships`
(`revision`, `start`, `end`, `key`, `array`, `order`)
VALUES (:revision, :start, :end, :key, :array, :order)');
$st2->bindValue(':revision', $revision, PDO::PARAM_STR);
$st2->bindValue(':start', $start, PDO::PARAM_STR);
$st2->bindValue(':end', $end, PDO::PARAM_STR);
$st2->bindValue(':key', $key, PDO::PARAM_STR);
$st2->bindValue(':array', $isArray, PDO::PARAM_INT);
$st2->bindValue(':order', $order, PDO::PARAM_INT);
$st2->execute();
return [];
}
/**
* Create Namespace.
*
* @param $namespace
*
* @throws Exception
*
* @return bool
*/
public function createNamespace($namespace)
{
if (empty($namespace)) {
throw new Exception('Empty namespace');
}
$documents = 'app_'.$namespace.'.database.documents';
$properties = 'app_'.$namespace.'.database.properties';
$relationships = 'app_'.$namespace.'.database.relationships';
$unique = 'app_'.$namespace.'.database.unique';
$audit = 'app_'.$namespace.'.audit.audit';
$abuse = 'app_'.$namespace.'.abuse.abuse';
try {
$this->getPDO()->prepare('CREATE TABLE `'.$documents.'` LIKE `template.database.documents`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$properties.'` LIKE `template.database.properties`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$relationships.'` LIKE `template.database.relationships`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$unique.'` LIKE `template.database.unique`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$audit.'` LIKE `template.audit.audit`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$abuse.'` LIKE `template.abuse.abuse`;')->execute();
} catch (Exception $e) {
throw $e;
}
return true;
}
/**
* Delete Namespace.
*
* @param $namespace
*
* @throws Exception
*
* @return bool
*/
public function deleteNamespace($namespace)
{
if (empty($namespace)) {
throw new Exception('Empty namespace');
}
$unique = 'app_'.$namespace.'.database.unique';
$documents = 'app_'.$namespace.'.database.documents';
$properties = 'app_'.$namespace.'.database.properties';
$relationships = 'app_'.$namespace.'.database.relationships';
$audit = 'app_'.$namespace.'.audit.audit';
$abuse = 'app_'.$namespace.'.abuse.abuse';
try {
$this->getPDO()->prepare('DROP TABLE `'.$unique.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$documents.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$properties.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$relationships.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$audit.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$abuse.'`;')->execute();
} catch (Exception $e) {
throw $e;
}
return true;
}
/**
* Get Collection.
*
* @param array $options
* @param array $filterTypes
*
* @throws Exception
*
* @return array
*/
public function getCollection(array $options, array $filterTypes = [])
{
$start = \microtime(true);
$orderCastMap = [
'int' => 'UNSIGNED',
'string' => 'CHAR',
'date' => 'DATE',
'time' => 'TIME',
'datetime' => 'DATETIME',
];
$orderTypeMap = ['DESC', 'ASC'];
$options['orderField'] = (empty($options['orderField'])) ? '' : $options['orderField']; // Set default order field
$options['orderCast'] = (empty($options['orderCast'])) ? 'string' : $options['orderCast']; // Set default order field
if (!\array_key_exists($options['orderCast'], $orderCastMap)) {
throw new Exception('Invalid order cast');
}
if (!\in_array($options['orderType'], $orderTypeMap)) {
throw new Exception('Invalid order type');
}
$where = [];
$join = [];
$sorts = [];
$search = '';
// Filters
foreach ($options['filters'] as $i => $filter) {
$filter = $this->parseFilter($filter);
$key = $filter['key'];
$value = $filter['value'];
$operator = $filter['operator'];
$path = \explode('.', $key);
$original = $path;
if (1 < \count($path)) {
$key = \array_pop($path);
} else {
$path = [];
}
//$path = implode('.', $path);
if (array_key_exists($key, $filterTypes) && $filterTypes[$key] === 'numeric') {
$value = (float) $value;
} else {
$value = $this->getPDO()->quote($value, PDO::PARAM_STR);
}
$key = $this->getPDO()->quote($key, PDO::PARAM_STR);
//$path = $this->getPDO()->quote($path, PDO::PARAM_STR);
$options['offset'] = (int) $options['offset'];
$options['limit'] = (int) $options['limit'];
if (empty($path)) {
//if($path == "''") { // Handle direct attributes queries
$where[] = 'JOIN `'.$this->getNamespace().".database.properties` b{$i} ON a.uid IS NOT NULL AND b{$i}.documentUid = a.uid AND (b{$i}.key = {$key} AND b{$i}.value {$operator} {$value})";
} else { // Handle direct child attributes queries
$len = \count($original);
$prev = 'c'.$i;
foreach ($original as $y => $part) {
$part = $this->getPDO()->quote($part, PDO::PARAM_STR);
if (0 === $y) { // First key
$join[$i] = 'JOIN `'.$this->getNamespace().".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$part}";
} elseif ($y == $len - 1) { // Last key
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.properties` e{$i} ON e{$i}.documentUid = {$prev}.end AND e{$i}.key = {$part} AND e{$i}.value {$operator} {$value}";
} else {
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.relationships` d{$i}{$y} ON d{$i}{$y}.start = {$prev}.end AND d{$i}{$y}.key = {$part}";
$prev = 'd'.$i.$y;
}
}
//$join[] = "JOIN `" . $this->getNamespace() . ".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$path}
// JOIN `" . $this->getNamespace() . ".database.properties` d{$i} ON d{$i}.documentUid = c{$i}.end AND d{$i}.key = {$key} AND d{$i}.value {$operator} {$value}";
}
}
// Sorting
if (!empty($options['orderField'])) {
$orderPath = \explode('.', $options['orderField']);
$len = \count($orderPath);
$orderKey = 'order_b';
$part = $this->getPDO()->quote(\implode('', $orderPath), PDO::PARAM_STR);
$orderSelect = "CASE WHEN {$orderKey}.key = {$part} THEN CAST({$orderKey}.value AS {$orderCastMap[$options['orderCast']]}) END AS sort_ff";
if (1 === $len) {
//if($path == "''") { // Handle direct attributes queries
$sorts[] = 'LEFT JOIN `'.$this->getNamespace().".database.properties` order_b ON a.uid IS NOT NULL AND order_b.documentUid = a.uid AND (order_b.key = {$part})";
} else { // Handle direct child attributes queries
$prev = 'c';
$orderKey = 'order_e';
foreach ($orderPath as $y => $part) {
$part = $this->getPDO()->quote($part, PDO::PARAM_STR);
$x = $y - 1;
if (0 === $y) { // First key
$sorts[] = 'JOIN `'.$this->getNamespace().".database.relationships` order_c{$y} ON a.uid IS NOT NULL AND order_c{$y}.start = a.uid AND order_c{$y}.key = {$part}";
} elseif ($y == $len - 1) { // Last key
$sorts[] .= 'JOIN `'.$this->getNamespace().".database.properties` order_e ON order_e.documentUid = order_{$prev}{$x}.end AND order_e.key = {$part}";
} else {
$sorts[] .= 'JOIN `'.$this->getNamespace().".database.relationships` order_d{$y} ON order_d{$y}.start = order_{$prev}{$x}.end AND order_d{$y}.key = {$part}";
$prev = 'd';
}
}
}
} else {
$orderSelect = 'a.uid AS sort_ff';
}
/*
* Workaround for a MySQL bug as reported here:
* https://bugs.mysql.com/bug.php?id=78485
*/
$options['search'] = ($options['search'] === '*') ? '' : $options['search'];
// Search
if (!empty($options['search'])) { // Handle free search
$where[] = 'LEFT JOIN `'.$this->getNamespace().".database.properties` b_search ON a.uid IS NOT NULL AND b_search.documentUid = a.uid AND b_search.primitive = 'string'
LEFT JOIN
`".$this->getNamespace().'.database.relationships` c_search ON c_search.start = b_search.documentUid
LEFT JOIN
`'.$this->getNamespace().".database.properties` d_search ON d_search.documentUid = c_search.end AND d_search.primitive = 'string'
\n";
$search = "AND (MATCH (b_search.value) AGAINST ({$this->getPDO()->quote($options['search'], PDO::PARAM_STR)} IN BOOLEAN MODE)
OR MATCH (d_search.value) AGAINST ({$this->getPDO()->quote($options['search'], PDO::PARAM_STR)} IN BOOLEAN MODE)
)";
}
$select = 'DISTINCT a.uid';
$where = \implode("\n", $where);
$join = \implode("\n", $join);
$sorts = \implode("\n", $sorts);
$range = "LIMIT {$options['offset']}, {$options['limit']}";
$roles = [];
foreach (Authorization::getRoles() as $role) {
$roles[] = 'JSON_CONTAINS(REPLACE(a.permissions, \'{self}\', a.uid), \'"'.$role.'"\', \'$.read\')';
}
if (false === Authorization::$status) { // FIXME temporary solution (hopefully)
$roles = ['1=1'];
}
$query = "SELECT %s, {$orderSelect}
FROM `".$this->getNamespace().".database.documents` a {$where}{$join}{$sorts}
WHERE status = 0
{$search}
AND (".\implode('||', $roles).")
ORDER BY sort_ff {$options['orderType']} %s";
$st = $this->getPDO()->prepare(\sprintf($query, $select, $range));
$st->execute();
$results = ['data' => []];
// Get entire fields data for each id
foreach ($st->fetchAll() as $node) {
$results['data'][] = $node['uid'];
}
$count = $this->getPDO()->prepare(\sprintf($query, 'count(DISTINCT a.uid) as sum', ''));
$count->execute();
$count = $count->fetch();
$this->resetDebug();
$this
->setDebug('query', \preg_replace('/\s+/', ' ', \sprintf($query, $select, $range)))
->setDebug('time', \microtime(true) - $start)
->setDebug('filters', \count($options['filters']))
->setDebug('joins', \substr_count($query, 'JOIN'))
->setDebug('count', \count($results['data']))
->setDebug('sum', (int) $count['sum'])
;
return $results['data'];
}
/**
* Get Collection.
*
* @param array $options
*
* @throws Exception
*
* @return int
*/
public function getCount(array $options)
{
$start = \microtime(true);
$where = [];
$join = [];
$options = array_merge([
'attribute' => '',
'filters' => [],
], $options);
// Filters
foreach ($options['filters'] as $i => $filter) {
$filter = $this->parseFilter($filter);
$key = $filter['key'];
$value = $filter['value'];
$operator = $filter['operator'];
$path = \explode('.', $key);
$original = $path;
if (1 < \count($path)) {
$key = \array_pop($path);
} else {
$path = [];
}
$key = $this->getPDO()->quote($key, PDO::PARAM_STR);
$value = $this->getPDO()->quote($value, PDO::PARAM_STR);
if (empty($path)) {
//if($path == "''") { // Handle direct attributes queries
$where[] = 'JOIN `'.$this->getNamespace().".database.properties` b{$i} ON a.uid IS NOT NULL AND b{$i}.documentUid = a.uid AND (b{$i}.key = {$key} AND b{$i}.value {$operator} {$value})";
} else { // Handle direct child attributes queries
$len = \count($original);
$prev = 'c'.$i;
foreach ($original as $y => $part) {
$part = $this->getPDO()->quote($part, PDO::PARAM_STR);
if (0 === $y) { // First key
$join[$i] = 'JOIN `'.$this->getNamespace().".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$part}";
} elseif ($y == $len - 1) { // Last key
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.properties` e{$i} ON e{$i}.documentUid = {$prev}.end AND e{$i}.key = {$part} AND e{$i}.value {$operator} {$value}";
} else {
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.relationships` d{$i}{$y} ON d{$i}{$y}.start = {$prev}.end AND d{$i}{$y}.key = {$part}";
$prev = 'd'.$i.$y;
}
}
}
}
$where = \implode("\n", $where);
$join = \implode("\n", $join);
$attribute = $this->getPDO()->quote($options['attribute'], PDO::PARAM_STR);
$func = 'JOIN `'.$this->getNamespace().".database.properties` b_func ON a.uid IS NOT NULL
AND a.uid = b_func.documentUid
AND (b_func.key = {$attribute})";
$roles = [];
foreach (Authorization::getRoles() as $role) {
$roles[] = 'JSON_CONTAINS(REPLACE(a.permissions, \'{self}\', a.uid), \'"'.$role.'"\', \'$.read\')';
}
if (false === Authorization::$status) { // FIXME temporary solution (hopefully)
$roles = ['1=1'];
}
$query = "SELECT SUM(b_func.value) as result
FROM `".$this->getNamespace().".database.documents` a {$where}{$join}{$func}
WHERE status = 0
AND (".\implode('||', $roles).')';
$st = $this->getPDO()->prepare(\sprintf($query));
$st->execute();
$result = $st->fetch();
$this->resetDebug();
$this
->setDebug('query', \preg_replace('/\s+/', ' ', \sprintf($query)))
->setDebug('time', \microtime(true) - $start)
->setDebug('filters', \count($options['filters']))
->setDebug('joins', \substr_count($query, 'JOIN'))
;
return (isset($result['result'])) ? (int)$result['result'] : 0;
}
/**
* Get Unique Document ID.
*
* @return string
*/
public function getId(): string
{
$unique = \uniqid();
$attempts = 5;
for ($i = 1; $i <= $attempts; ++$i) {
$document = $this->getDocument($unique);
if (empty($document) || $document['$id'] !== $unique) {
return $unique;
}
}
throw new Exception('Failed to create a unique ID ('.$attempts.' attempts)');
}
/**
* Last Modified.
*
* Return Unix timestamp of last time a node queried in corrent session has been changed
*
* @return int
*/
public function lastModified()
{
return $this->lastModified;
}
/**
* Parse Filter.
*
* @param string $filter
*
* @return array
*
* @throws Exception
*/
protected function parseFilter($filter)
{
$operatorsMap = ['!=', '>=', '<=', '=', '>', '<']; // Do not edit order of this array
//FIXME bug with >= <= operators
$operator = null;
foreach ($operatorsMap as $node) {
if (\strpos($filter, $node) !== false) {
$operator = $node;
break;
}
}
if (empty($operator)) {
throw new Exception('Invalid operator');
}
$filter = \explode($operator, $filter);
if (\count($filter) != 2) {
throw new Exception('Invalid filter expression');
}
return [
'key' => $filter[0],
'value' => $filter[1],
'operator' => $operator,
];
}
/**
* Get Data Type.
*
* Check value data type. return value can be on of the following:
* string, integer, float, boolean, object, list or null
*
* @param $value
*
* @return string
*
* @throws \Exception
*/
protected function getDataType($value)
{
switch (\gettype($value)) {
case 'string':
return self::DATA_TYPE_STRING;
break;
case 'integer':
return self::DATA_TYPE_INTEGER;
break;
case 'double':
return self::DATA_TYPE_FLOAT;
break;
case 'boolean':
return self::DATA_TYPE_BOOLEAN;
break;
case 'array':
if ((bool) \count(\array_filter(\array_keys($value), 'is_string'))) {
return self::DATA_TYPE_DICTIONARY;
}
return self::DATA_TYPE_ARRAY;
break;
case 'NULL':
return self::DATA_TYPE_NULL;
break;
}
throw new Exception('Unknown data type: '.$value.' ('.\gettype($value).')');
}
/**
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function setDebug(string $key, $value): self
{
$this->debug[$key] = $value;
return $this;
}
/**
* @return array
*/
public function getDebug(): array
{
return $this->debug;
}
/**
* return $this;.
*
* @return void
*/
public function resetDebug(): void
{
$this->debug = [];
}
/**
* @return PDO
*
* @throws Exception
*/
protected function getPDO()
{
return $this->pdo;
}
/**
* @throws Exception
*
* @return Redis
*/
protected function getRedis(): Redis
{
return $this->redis;
}
}

View file

@ -1,300 +0,0 @@
<?php
namespace Appwrite\Database\Adapter;
use Appwrite\Database\Adapter;
use Exception;
use Redis as Client;
class Redis extends Adapter
{
/**
* @var Client
*/
protected $redis;
/**
* @var Adapter
*/
protected $adapter;
/**
* Redis constructor.
*
* @param Adapter $adapter
* @param Client $redis
*/
public function __construct(Adapter $adapter, Client $redis)
{
$this->redis = $redis;
$this->adapter = $adapter;
}
/**
* Get Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function getDocument($id)
{
$output = \json_decode($this->getRedis()->get($this->getNamespace().':document-'.$id), true);
if (!$output) {
$output = $this->adapter->getDocument($id);
$this->getRedis()->set($this->getNamespace().':document-'.$id, \json_encode($output, JSON_UNESCAPED_UNICODE));
}
$output = $this->parseRelations($output);
return $output;
}
/**
* @param $output
*
* @return mixed
*
* @throws Exception
*/
protected function parseRelations($output)
{
$keys = [];
if (empty($output) || !isset($output['temp-relations'])) {
return $output;
}
foreach ($output['temp-relations'] as $relationship) {
$keys[] = $this->getNamespace().':document-'.$relationship['end'];
}
$nodes = (!empty($keys)) ? $this->getRedis()->mget($keys) : [];
foreach ($output['temp-relations'] as $i => $relationship) {
$node = $relationship['end'];
$node = (!empty($nodes[$i])) ? $this->parseRelations(\json_decode($nodes[$i], true)) : $this->getDocument($node);
if (empty($node)) {
continue;
}
if ($relationship['array']) {
$output[$relationship['key']][] = $node;
} else {
$output[$relationship['key']] = $node;
}
}
unset($output['temp-relations']);
return $output;
}
/**
* Create Document.
*
* @param array $data
*
* @return array
*
* @throws Exception
*/
public function createDocument(array $data = [], array $unique = [])
{
$data = $this->adapter->createDocument($data, $unique);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
return $data;
}
/**
* Update Document.
*
* @param array $data
*
* @return array
*
* @throws Exception
*/
public function updateDocument(array $data = [])
{
$data = $this->adapter->updateDocument($data);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
return $data;
}
/**
* Delete Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function deleteDocument(string $id)
{
$data = $this->adapter->deleteDocument($id);
$this->getRedis()->expire($this->getNamespace().':document-'.$id, 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$id, 0);
return $data;
}
/**
* Delete Unique Key.
*
* @param $key
*
* @return array
*
* @throws Exception
*/
public function deleteUniqueKey($key)
{
$data = $this->adapter->deleteUniqueKey($key);
return $data;
}
/**
* Add Unique Key.
*
* @param $key
*
* @return array
*
* @throws Exception
*/
public function addUniqueKey($key)
{
$data = $this->adapter->addUniqueKey($key);
return $data;
}
/**
* Create Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function createNamespace($namespace)
{
return $this->adapter->createNamespace($namespace);
}
/**
* Delete Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function deleteNamespace($namespace)
{
return $this->adapter->deleteNamespace($namespace);
}
/**
* @param array $options
* @param array $filterTypes
*
* @return array
*
* @throws Exception
*/
public function getCollection(array $options, array $filterTypes = [])
{
$data = $this->adapter->getCollection($options, $filterTypes);
$keys = [];
foreach ($data as $node) {
$keys[] = $this->getNamespace().':document-'.$node;
}
$nodes = (!empty($keys)) ? $this->getRedis()->mget($keys) : [];
foreach ($data as $i => &$node) {
$temp = (!empty($nodes[$i])) ? $this->parseRelations(\json_decode($nodes[$i], true)) : $this->getDocument($node);
if (!empty($temp)) {
$node = $temp;
}
}
return $data;
}
/**
* @param array $options
*
* @return int
*
* @throws Exception
*/
public function getCount(array $options)
{
return $this->adapter->getCount($options);
}
/**
* Last Modified.
*
* Return Unix timestamp of last time a node queried in current session has been changed
*
* @return int
*/
public function lastModified()
{
return 0;
}
/**
* @return array
*/
public function getDebug()
{
return $this->adapter->getDebug();
}
/**
* @throws Exception
*
* @return Client
*/
protected function getRedis(): Client
{
return $this->redis;
}
/**
* Set Namespace.
*
* Set namespace to divide different scope of data sets
*
* @param $namespace
*
* @return bool
*
* @throws Exception
*/
public function setNamespace($namespace)
{
$this->adapter->setNamespace($namespace);
return parent::setNamespace($namespace);
}
}

View file

@ -1,628 +0,0 @@
<?php
namespace Appwrite\Database;
use Exception;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Database\Validator\Structure;
use Appwrite\Database\Exception\Authorization as AuthorizationException;
use Appwrite\Database\Exception\Structure as StructureException;
class Database
{
// System Core
const SYSTEM_COLLECTION_COLLECTIONS = 0;
const SYSTEM_COLLECTION_RULES = 'rules';
// Project
const SYSTEM_COLLECTION_PROJECTS = 'projects';
const SYSTEM_COLLECTION_WEBHOOKS = 'webhooks';
const SYSTEM_COLLECTION_KEYS = 'keys';
const SYSTEM_COLLECTION_TASKS = 'tasks';
const SYSTEM_COLLECTION_PLATFORMS = 'platforms';
const SYSTEM_COLLECTION_USAGES = 'usages'; // TODO add structure
const SYSTEM_COLLECTION_DOMAINS = 'domains';
const SYSTEM_COLLECTION_CERTIFICATES = 'certificates';
const SYSTEM_COLLECTION_RESERVED = 'reserved';
// Auth, Account and Users (private to user)
const SYSTEM_COLLECTION_USERS = 'users';
const SYSTEM_COLLECTION_SESSIONS = 'sessions';
const SYSTEM_COLLECTION_TOKENS = 'tokens';
// Teams (shared among team members)
const SYSTEM_COLLECTION_MEMBERSHIPS = 'memberships';
const SYSTEM_COLLECTION_TEAMS = 'teams';
// Storage
const SYSTEM_COLLECTION_FILES = 'files';
// Functions
const SYSTEM_COLLECTION_FUNCTIONS = 'functions';
const SYSTEM_COLLECTION_TAGS = 'tags';
const SYSTEM_COLLECTION_EXECUTIONS = 'executions';
// Realtime
const SYSTEM_COLLECTION_CONNECTIONS = 'connections';
// Var Types
const SYSTEM_VAR_TYPE_TEXT = 'text';
const SYSTEM_VAR_TYPE_NUMERIC = 'numeric';
const SYSTEM_VAR_TYPE_BOOLEAN = 'boolean';
const SYSTEM_VAR_TYPE_DOCUMENT = 'document';
const SYSTEM_VAR_TYPE_WILDCARD = 'wildcard';
const SYSTEM_VAR_TYPE_EMAIL = 'email';
const SYSTEM_VAR_TYPE_IP = 'ip';
const SYSTEM_VAR_TYPE_URL = 'url';
const SYSTEM_VAR_TYPE_KEY = 'key';
/**
* @var array
*/
protected static $filters = [];
/**
* @var bool
*/
protected static $statusFilters = true;
/**
* @var array
*/
protected $mocks = [];
/**
* @var Adapter
*/
protected $adapter;
/**
* Set Adapter.
*
* @param Adapter $adapter
*
* @return $this
*/
public function setAdapter(Adapter $adapter)
{
$this->adapter = $adapter;
return $this;
}
/**
* Set Namespace.
*
* Set namespace to divide different scope of data sets
*
* @param $namespace
*
* @return $this
*
* @throws Exception
*/
public function setNamespace($namespace)
{
$this->adapter->setNamespace($namespace);
return $this;
}
/**
* Get Namespace.
*
* Get namespace of current set scope
*
* @return string
*
* @throws Exception
*/
public function getNamespace()
{
return $this->adapter->getNamespace();
}
/**
* Create Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function createNamespace($namespace)
{
return $this->adapter->createNamespace($namespace);
}
/**
* Delete Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function deleteNamespace($namespace)
{
return $this->adapter->deleteNamespace($namespace);
}
/**
* @param array $options
* @param array $filterTypes
*
* @return Document[]
*/
public function getCollection(array $options, array $filterTypes = [])
{
$options = \array_merge([
'offset' => 0,
'limit' => 15,
'search' => '',
'relations' => true,
'orderField' => '',
'orderType' => 'ASC',
'orderCast' => 'int',
'filters' => [],
], $options);
$results = $this->adapter->getCollection($options, $filterTypes);
foreach ($results as &$node) {
$node = $this->decode(new Document($node));
}
return $results;
}
/**
* @param array $options
*
* @return Document
*/
public function getCollectionFirst(array $options)
{
$results = $this->getCollection($options);
return \reset($results);
}
/**
* @param array $options
*
* @return Document
*/
public function getCollectionLast(array $options)
{
$results = $this->getCollection($options);
return \end($results);
}
/**
* @param string $id
* @param bool $mock is mocked data allowed?
* @param bool $decode enable decoding?
*
* @return Document
*/
public function getDocument($id, bool $mock = true, bool $decode = true)
{
if (\is_null($id)) {
return new Document();
}
$document = new Document((isset($this->mocks[$id]) && $mock) ? $this->mocks[$id] : $this->adapter->getDocument($id));
$validator = new Authorization($document, 'read');
if (!$validator->isValid($document->getPermissions())) { // Check if user has read access to this document
return new Document();
}
$document = ($decode) ? $this->decode($document) : $document;
return $document;
}
/**
* @param array $data
*
* @return Document
*
* @throws AuthorizationException
* @throws StructureException
*/
public function createDocument(array $data, array $unique = [])
{
$document = new Document($data);
$validator = new Authorization($document, 'write');
if (!$validator->isValid($document->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription());
}
$validator = new Structure($this);
$document = $this->encode($document);
if (!$validator->isValid($document)) {
throw new StructureException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$document = new Document($this->adapter->createDocument($document->getArrayCopy(), $unique));
$document = $this->decode($document);
return $document;
}
/**
* @param array $data
*
* @return Document|false
*
* @throws Exception
*/
public function updateDocument(array $data)
{
if (!isset($data['$id'])) {
throw new Exception('Must define $id attribute');
}
$document = $this->getDocument($data['$id']); // TODO make sure user don\'t need read permission for write operations
// Make sure reserved keys stay constant
$data['$id'] = $document->getId();
$data['$collection'] = $document->getCollection();
$validator = new Authorization($document, 'write');
if (!$validator->isValid($document->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = new Document($data);
if (!$validator->isValid($new->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = $this->encode($new);
$validator = new Structure($this);
if (!$validator->isValid($new)) { // Make sure updated structure still apply collection rules (if any)
throw new StructureException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = new Document($this->adapter->updateDocument($new->getArrayCopy()));
$new = $this->decode($new);
return $new;
}
/**
* @param array $data
*
* @return Document|false
*
* @throws Exception
*/
public function overwriteDocument(array $data)
{
if (!isset($data['$id'])) {
throw new Exception('Must define $id attribute');
}
$document = $this->getDocument($data['$id']); // TODO make sure user don\'t need read permission for write operations
$validator = new Authorization($document, 'write');
if (!$validator->isValid($document->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = new Document($data);
if (!$validator->isValid($new->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = $this->encode($new);
$validator = new Structure($this);
if (!$validator->isValid($new)) { // Make sure updated structure still apply collection rules (if any)
throw new StructureException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = new Document($this->adapter->updateDocument($new->getArrayCopy()));
$new = $this->decode($new);
return $new;
}
/**
* @param string $id
*
* @return Document|false
*
* @throws AuthorizationException
*/
public function deleteDocument(string $id)
{
$document = $this->getDocument($id);
$validator = new Authorization($document, 'write');
if (!$validator->isValid($document->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription());
}
return new Document($this->adapter->deleteDocument($id));
}
/**
* @param int $key
*
* @return Document|false
*
* @throws AuthorizationException
*/
public function deleteUniqueKey($key)
{
return new Document($this->adapter->deleteUniqueKey($key));
}
/**
* @param int $key
*
* @return Document|false
*
* @throws AuthorizationException
*/
public function addUniqueKey($key)
{
return new Document($this->adapter->addUniqueKey($key));
}
/**
* @return array
*/
public function getDebug()
{
return $this->adapter->getDebug();
}
/**
* @return int
*/
public function getSum()
{
$debug = $this->getDebug();
return (isset($debug['sum'])) ? $debug['sum'] : 0;
}
/**
* @param array $options
*
* @return int
*/
public function getCount(array $options)
{
$options = \array_merge([
'filters' => [],
], $options);
$results = $this->adapter->getCount($options);
return $results;
}
/**
* @param string $key
* @param string $value
*
* @return self
*/
public function setMock($key, $value): self
{
$this->mocks[$key] = $value;
return $this;
}
/**
* @param array $mocks
*
* @return self
*/
public function setMocks(array $mocks): self
{
$this->mocks = $mocks;
return $this;
}
/**
* @return array
*/
public function getMocks()
{
return $this->mocks;
}
/**
* Add Attribute Filter
*
* @param string $name
* @param callable $encode
* @param callable $decode
*
* @return void
*/
public static function addFilter(string $name, callable $encode, callable $decode): void
{
self::$filters[$name] = [
'encode' => $encode,
'decode' => $decode,
];
}
/**
* Disable Attribute decoding
*
* @return void
*/
public static function disableFilters(): void
{
self::$statusFilters = false;
}
/**
* Enable Attribute decoding
*
* @return void
*/
public static function enableFilters(): void
{
self::$statusFilters = true;
}
public function encode(Document $document):Document
{
if (!self::$statusFilters) {
return $document;
}
$collection = $this->getDocument($document->getCollection(), true, false);
$rules = $collection->getAttribute('rules', []);
foreach ($rules as $key => $rule) {
$key = $rule->getAttribute('key', null);
$type = $rule->getAttribute('type', null);
$array = $rule->getAttribute('array', false);
$filters = $rule->getAttribute('filter', []);
$value = $document->getAttribute($key, null);
if (($value !== null)) {
if ($type === self::SYSTEM_VAR_TYPE_DOCUMENT) {
if ($array) {
$list = [];
foreach ($value as $child) {
$list[] = $this->encode($child);
}
$document->setAttribute($key, $list);
} else {
$document->setAttribute($key, $this->encode($value));
}
} else {
foreach ($filters as $filter) {
$value = $this->encodeAttribute($filter, $value);
$document->setAttribute($key, $value);
}
}
}
}
return $document;
}
public function decode(Document $document):Document
{
if (!self::$statusFilters) {
return $document;
}
$collection = $this->getDocument($document->getCollection(), true, false);
$rules = $collection->getAttribute('rules', []);
foreach ($rules as $key => $rule) {
$key = $rule->getAttribute('key', null);
$type = $rule->getAttribute('type', null);
$array = $rule->getAttribute('array', false);
$filters = $rule->getAttribute('filter', []);
$value = $document->getAttribute($key, null);
if (($value !== null)) {
if ($type === self::SYSTEM_VAR_TYPE_DOCUMENT) {
if ($array) {
$list = [];
foreach ($value as $child) {
$list[] = $this->decode($child);
}
$document->setAttribute($key, $list);
} else {
$document->setAttribute($key, $this->decode($value));
}
} else {
foreach (array_reverse($filters) as $filter) {
$value = $this->decodeAttribute($filter, $value);
$document->setAttribute($key, $value);
}
}
}
}
return $document;
}
/**
* Encode Attribute
*
* @param string $name
* @param mixed $value
*/
protected static function encodeAttribute(string $name, $value)
{
if (!isset(self::$filters[$name])) {
return $value;
throw new Exception("Filter '{$name}' not found");
}
try {
$value = self::$filters[$name]['encode']($value);
} catch (\Throwable $th) {
$value = null;
}
return $value;
}
/**
* Decode Attribute
*
* @param string $name
* @param mixed $value
*/
protected static function decodeAttribute(string $name, $value)
{
if (!isset(self::$filters[$name])) {
return $value;
throw new Exception("Filter '{$name}' not found");
}
try {
$value = self::$filters[$name]['decode']($value);
} catch (\Throwable $th) {
$value = null;
}
return $value;
}
/**
* Get Last Modified.
*
* Return Unix timestamp of last time a node queried in current session has been changed
*
* @return int
*/
public function lastModified()
{
return $this->adapter->lastModified();
}
}

View file

@ -1,274 +0,0 @@
<?php
namespace Appwrite\Database;
use ArrayObject;
class Document extends ArrayObject
{
const SET_TYPE_ASSIGN = 'assign';
const SET_TYPE_PREPEND = 'prepend';
const SET_TYPE_APPEND = 'append';
/**
* Construct.
*
* Construct a new fields object
*
* @see ArrayObject::__construct
*
* @param array $input
* @param int $flags
* @param string $iterator_class
*/
public function __construct($input = [], $flags = 0, $iterator_class = 'ArrayIterator')
{
foreach ($input as $key => &$value) {
if (\is_array($value)) {
if ((isset($value['$id']) || isset($value['$collection'])) && (!$value instanceof self)) {
$input[$key] = new self($value);
} else {
foreach ($value as $childKey => $child) {
if ((isset($child['$id']) || isset($child['$collection'])) && (!$child instanceof self)) {
$value[$childKey] = new self($child);
}
}
}
}
}
parent::__construct($input, $flags, $iterator_class);
}
/**
* @return string|null
*/
public function getId()
{
return $this->getAttribute('$id', null);
}
/**
* @return string
*/
public function getCollection()
{
return $this->getAttribute('$collection', null);
}
/**
* @return array
*/
public function getPermissions()
{
return $this->getAttribute('$permissions', []);
}
/**
* Get Attribute.
*
* Method for getting a specific fields attribute. If $name is not found $default value will be returned.
*
* @param string $name
* @param mixed $default
*
* @return mixed
*/
public function getAttribute($name, $default = null)
{
$name = \explode('.', $name);
$temp = &$this;
foreach ($name as $key) {
if (!isset($temp[$key])) {
return $default;
}
$temp = &$temp[$key];
}
return $temp;
}
/**
* Get Document Attributes
*
* @return array
*/
public function getAttributes(): array
{
$attributes = [];
foreach ($this as $attribute => $value) {
if(array_key_exists($attribute, ['$id' => true, '$permissions' => true, '$collection' => true, '$execute' => []])) {
continue;
}
$attributes[$attribute] = $value;
}
return $attributes;
}
/**
* Set Attribute.
*
* Method for setting a specific field attribute
*
* @param string $key
* @param mixed $value
* @param string $type
*
* @return mixed
*/
public function setAttribute($key, $value, $type = self::SET_TYPE_ASSIGN)
{
switch ($type) {
case self::SET_TYPE_ASSIGN:
$this[$key] = $value;
break;
case self::SET_TYPE_APPEND:
$this[$key] = (!isset($this[$key]) || !\is_array($this[$key])) ? [] : $this[$key];
\array_push($this[$key], $value);
break;
case self::SET_TYPE_PREPEND:
$this[$key] = (!isset($this[$key]) || !\is_array($this[$key])) ? [] : $this[$key];
\array_unshift($this[$key], $value);
break;
}
return $this;
}
/**
* Remove Attribute.
*
* Method for removing a specific field attribute
*
* @param string $key
* @param mixed $value
* @param string $type
*
* @return mixed
*/
public function removeAttribute($key)
{
if (isset($this[$key])) {
unset($this[$key]);
}
return $this;
}
/**
* Search.
*
* Get array child by key and value match
*
* @param $key
* @param $value
* @param array|null $scope
*
* @return Document|Document[]|mixed|null|array
*/
public function search($key, $value, $scope = null)
{
$array = (!\is_null($scope)) ? $scope : $this;
if (\is_array($array) || $array instanceof self) {
if (isset($array[$key]) && $array[$key] == $value) {
return $array;
}
foreach ($array as $k => $v) {
if ((\is_array($v) || $v instanceof self) && (!empty($v))) {
$result = $this->search($key, $value, $v);
if (!empty($result)) {
return $result;
}
} else {
if ($k === $key && $v === $value) {
return $array;
}
}
}
}
if ($array === $value) {
return $array;
}
return;
}
/**
* Checks if document has data.
*
* @return bool
*/
public function isEmpty()
{
return empty($this->getId());
}
/**
* Checks if a document key is set.
*
* @param string $key
*
* @return bool
*/
public function isSet($key)
{
return isset($this[$key]);
}
/**
* Get Array Copy.
*
* Outputs entity as a PHP array
*
* @param array $whitelist
* @param array $blacklist
*
* @return array
*/
public function getArrayCopy(array $whitelist = [], array $blacklist = []): array
{
$array = parent::getArrayCopy();
$output = [];
foreach ($array as $key => &$value) {
if (!empty($whitelist) && !\in_array($key, $whitelist)) { // Export only whitelisted fields
continue;
}
if (!empty($blacklist) && \in_array($key, $blacklist)) { // Don't export blacklisted fields
continue;
}
if ($value instanceof self) {
$output[$key] = $value->getArrayCopy($whitelist, $blacklist);
} elseif (\is_array($value)) {
foreach ($value as $childKey => &$child) {
if ($child instanceof self) {
$output[$key][$childKey] = $child->getArrayCopy($whitelist, $blacklist);
} else {
$output[$key][$childKey] = $child;
}
}
if (empty($value)) {
$output[$key] = $value;
}
} else {
$output[$key] = $value;
}
}
return $output;
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace Appwrite\Database\Exception;
class Authorization extends \Exception
{
}

View file

@ -1,7 +0,0 @@
<?php
namespace Appwrite\Database\Exception;
class Duplicate extends \Exception
{
}

View file

@ -1,7 +0,0 @@
<?php
namespace Appwrite\Database\Exception;
class Structure extends \Exception
{
}

View file

@ -1,215 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Document;
use Utopia\Validator;
class Authorization extends Validator
{
/**
* @var array
*/
public static $roles = ['*' => true];
/**
* @var Document
*/
protected $document;
/**
* @var string
*/
protected $action = '';
/**
* @var string
*/
protected $message = 'Authorization Error';
/**
* Structure constructor.
*
* @param Document $document
* @param string $action
*/
public function __construct(Document $document, $action)
{
$this->document = $document;
$this->action = $action;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $permissions
*
* @return bool
*/
public function isValid($permissions): bool
{
if (!self::$status) {
return true;
}
if (!isset($permissions[$this->action])) {
$this->message = 'Missing action key: "'.$this->action.'"';
return false;
}
$permission = null;
foreach ($permissions[$this->action] as $permission) {
$permission = \str_replace(':{self}', ':'.$this->document->getId(), $permission);
if (\array_key_exists($permission, self::$roles)) {
return true;
}
}
$this->message = 'Missing "'.$this->action.'" permission for role "'.$permission.'". Only this scopes "'.\json_encode(self::getRoles()).'" are given and only this are allowed "'.\json_encode($permissions[$this->action]).'".';
return false;
}
/**
* @param string $role
*
* @return void
*/
public static function setRole(string $role): void
{
self::$roles[$role] = true;
}
/**
* @param string $role
*
* @return void
*/
public static function unsetRole(string $role): void
{
unset(self::$roles[$role]);
}
/**
* @return array
*/
public static function getRoles(): array
{
return \array_keys(self::$roles);
}
/**
* @return void
*/
public static function cleanRoles(): void
{
self::$roles = [];
}
/**
* @param string $role
*
* @return bool
*/
public static function isRole(string $role): bool
{
return (\array_key_exists($role, self::$roles));
}
/**
* @var bool
*/
public static $status = true;
/**
* Default value in case we need
* to reset Authorization status
*
* @var bool
*/
public static $statusDefault = true;
/**
* Change default status.
* This will be used for the
* value set on the self::reset() method
*
* @return void
*/
public static function setDefaultStatus($status): void
{
self::$statusDefault = $status;
self::$status = $status;
}
/**
* Enable Authorization checks
*
* @return void
*/
public static function enable(): void
{
self::$status = true;
}
/**
* Disable Authorization checks
*
* @return void
*/
public static function disable(): void
{
self::$status = false;
}
/**
* Disable Authorization checks
*
* @return void
*/
public static function reset(): void
{
self::$status = self::$statusDefault;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_ARRAY;
}
}

View file

@ -1,62 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class Collection extends Structure
{
/**
* @var array
*/
protected $collections = [];
/**
* @var array
*/
protected $merge = [];
/**
* @param Database $database
* @param array $collections
* @param array $merge
*/
public function __construct(Database $database, array $collections, array $merge = [])
{
$this->collections = $collections;
$this->merge = $merge;
return parent::__construct($database);
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $document
*
* @return bool
*/
public function isValid($document): bool
{
$document = new Document(
\array_merge($this->merge, ($document instanceof Document) ? $document->getArrayCopy() : $document)
);
if (\is_null($document->getCollection())) {
$this->message = 'Missing collection attribute $collection';
return false;
}
if (!\in_array($document->getCollection(), $this->collections)) {
$this->message = 'Collection is not allowed';
return false;
}
return parent::isValid($document);
}
}

View file

@ -1,105 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Utopia\Validator;
class DocumentId extends Validator
{
/**
* @var string
*/
protected $message = 'Document not found.';
/**
* @var Database
*/
protected $database;
/**
* @var string
*/
protected $collection = '';
/**
* Structure constructor.
*
* @param Database $database
* @param string $collection
*/
public function __construct(Database $database, string $collection = '')
{
$this->database = $database;
$this->collection = $collection;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param $value
*
* @return bool
*/
public function isValid($id): bool
{
$document = $this->database->getDocument($id);
if (!$document) {
return false;
}
if (!$document instanceof Document) {
return false;
}
if (!$document->getId()) {
return false;
}
if ($document->getCollection() !== $this->collection) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Utopia\Validator;
class Key extends Validator
{
/**
* @var string
*/
protected $message = 'Parameter must contain only letters with no spaces or special chars and be shorter than 32 chars';
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param $value
*
* @return bool
*/
public function isValid($value): bool
{
if (!\is_string($value)) {
return false;
}
if (\preg_match('/[^A-Za-z0-9\-\_]/', $value)) {
return false;
}
if (\mb_strlen($value) > 32) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -1,100 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Document;
use Utopia\Validator;
class Permissions extends Validator
{
/**
* @var string
*/
protected $message = 'Permissions Error';
/**
* @var Document
*/
protected $document;
/**
* Structure constructor.
*
* @param Document $document
*/
public function __construct(Document $document)
{
$this->document = $document;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if (!\is_array($value) && !empty($value)) {
$this->message = 'Invalid permissions data structure';
return false;
}
foreach ($value as $action => $roles) {
if (!\in_array($action, ['read', 'write', 'execute'])) {
$this->message = 'Unknown action ("'.$action.'")';
return false;
}
foreach ($roles as $role) {
if (!\is_string($role)) {
$this->message = 'Permissions role must be of type string.';
return false;
}
}
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_ARRAY;
}
}

View file

@ -1,313 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Network\Validator as NetworkValidator;
use Utopia\Validator;
class Structure extends Validator
{
const RULE_TYPE_ID = 'id';
const RULE_TYPE_PERMISSIONS = 'permissions';
const RULE_TYPE_KEY = 'key';
const RULE_TYPE_TEXT = 'text';
const RULE_TYPE_MARKDOWN = 'markdown';
const RULE_TYPE_NUMERIC = 'numeric';
const RULE_TYPE_BOOLEAN = 'boolean';
const RULE_TYPE_EMAIL = 'email';
const RULE_TYPE_URL = 'url';
const RULE_TYPE_IP = 'ip';
const RULE_TYPE_WILDCARD = 'wildcard';
const RULE_TYPE_DOCUMENT = 'document';
const RULE_TYPE_DOCUMENTID = 'documentId';
const RULE_TYPE_FILEID = 'fileId';
/**
* @var Database
*/
protected $database;
/**
* @var string
*/
protected $id = '';
/**
* Basic rules to apply on all documents.
*
* @var array
*/
protected $rules = [
[
'label' => '$id',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$id',
'type' => 'id',
'default' => null,
'required' => false,
'array' => false,
],
[
'label' => '$collection',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$collection',
'type' => 'id',
'default' => null,
'required' => true,
'array' => false,
],
[
'label' => '$permissions',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$permissions',
'type' => 'permissions',
'default' => null,
'required' => true,
'array' => false,
],
[
'label' => '$createdAt',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$createdAt',
'type' => 'numeric',
'default' => null,
'required' => false,
'array' => false,
],
[
'label' => '$updatedAt',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$updatedAt',
'type' => 'numeric',
'default' => null,
'required' => false,
'array' => false,
],
];
/**
* @var string
*/
protected $message = 'General Error';
/**
* Structure constructor.
*
* @param Database $database
*/
public function __construct(Database $database)
{
$this->database = $database;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Invalid document structure: '.$this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $document
*
* @return bool
*/
public function isValid($document): bool
{
$document = (\is_array($document)) ? new Document($document) : $document;
$this->id = $document->getId();
if (\is_null($document->getCollection())) {
$this->message = 'Missing collection attribute $collection';
return false;
}
$collection = $this->getCollection($document->getCollection());
if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
$this->message = 'Collection not found';
return false;
}
$array = $document->getArrayCopy();
$rules = \array_merge($this->rules, $collection->getAttribute('rules', []));
foreach ($rules as $rule) { // Check all required keys are set
if (isset($rule['key']) && !isset($array[$rule['key']])
&& isset($rule['required']) && true == $rule['required']) {
$this->message = 'Missing required key "'.$rule['key'].'"';
return false;
}
}
foreach ($array as $key => $value) {
$rule = $collection->search('key', $key, $rules);
if (!$rule) {
continue;
}
$ruleType = $rule['type'] ?? '';
$ruleRequired = $rule['required'] ?? true;
$ruleArray = $rule['array'] ?? false;
$validator = null;
switch ($ruleType) {
case self::RULE_TYPE_ID:
$validator = new UID();
break;
case self::RULE_TYPE_PERMISSIONS:
$validator = new Permissions($document); //$validator = ($this->forcePermissions) ? new Authorization($original, 'write') : new Validator\Mock();
break;
case self::RULE_TYPE_KEY:
$validator = new Key();
break;
case self::RULE_TYPE_TEXT:
case self::RULE_TYPE_MARKDOWN:
$validator = new Validator\Text(0);
break;
case self::RULE_TYPE_NUMERIC:
$validator = new Validator\Numeric();
break;
case self::RULE_TYPE_BOOLEAN:
$validator = new Validator\Boolean();
break;
case self::RULE_TYPE_EMAIL:
$validator = new NetworkValidator\Email();
break;
case self::RULE_TYPE_URL:
$validator = new NetworkValidator\URL();
break;
case self::RULE_TYPE_IP:
$validator = new NetworkValidator\IP();
break;
case self::RULE_TYPE_WILDCARD:
$validator = new Validator\Wildcard();
break;
case self::RULE_TYPE_DOCUMENT:
$validator = new Collection($this->database, (isset($rule['list'])) ? $rule['list'] : []);
$value = $document->getAttribute($key);
break;
case self::RULE_TYPE_DOCUMENTID:
$validator = new DocumentId($this->database, (isset($rule['list']) && isset($rule['list'][0])) ? $rule['list'][0] : '');
$value = $document->getAttribute($key);
break;
case self::RULE_TYPE_FILEID:
$validator = new DocumentId($this->database, Database::SYSTEM_COLLECTION_FILES);
$value = $document->getAttribute($key);
break;
}
if (empty($validator)) { // Error creating validator for property
$this->message = 'Unknown rule type "'.$ruleType.'" for property "'.\htmlspecialchars($key, ENT_QUOTES, 'UTF-8').'"';
if (empty($ruleType)) {
$this->message = 'Unknown property "'.$key.'" type'.
'. Make sure to follow '.\strtolower($collection->getAttribute('name', 'unknown')).' collection structure';
}
return false;
}
if ($ruleRequired && ('' === $value || null === $value)) {
$this->message = 'Required property "'.$key.'" has no value';
return false;
}
if (!$ruleRequired && empty($value)) {
unset($array[$key]);
unset($rule);
continue;
}
if ($ruleArray) { // Array of values validation
if (!\is_array($value)) {
$this->message = 'Property "'.$key.'" must be an array';
return false;
}
// TODO add is required check here
foreach ($value as $node) {
if (!$validator->isValid($node)) { // Check if property is valid, if not required can also be empty
$this->message = 'Property "'.$key.'" has invalid input. '.$validator->getDescription();
return false;
}
}
} else { // Single value validation
if ((!$validator->isValid($value)) && !('' === $value && !$ruleRequired)) { // Error when value is not valid, and is not optional and empty
$this->message = 'Property "'.$key.'" has invalid input. '.$validator->getDescription();
return false;
}
}
unset($array[$key]);
unset($rule);
}
if (!empty($array)) { // No fields should be left unvalidated
$this->message = 'Unknown properties are not allowed ('.\implode(', ', \array_keys($array)).') for this collection'.
'. Make sure to follow '.\strtolower($collection->getAttribute('name', 'unknown')).' collection structure';
return false;
}
return true;
}
/**
* Get Collection
*
* Get Collection by unique ID
*
* @return Document
*/
protected function getCollection($id): Document
{
return $this->database->getDocument($id);
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_OBJECT;
}
}

View file

@ -1,70 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Utopia\Validator;
class UID extends Validator
{
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Invalid UID format';
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if ($value === 0) { // TODO Deprecate confition when we get the chance.
return true;
}
if (!is_string($value)) {
return false;
}
if (mb_strlen($value) > 32) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -2,107 +2,58 @@
namespace Appwrite\Migration;
use Appwrite\Database\Document as OldDocument;
use Appwrite\Database\Database as OldDatabase;
use PDO;
use Redis;
use Swoole\Runtime;
use Utopia\Database\Document;
use Utopia\Database\Database;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Exception;
abstract class Migration
{
/**
* @var array
*/
protected array $options;
/**
* @var PDO
*/
protected PDO $db;
/**
* @var Redis
*/
protected Redis $cache;
/**
* @var int
*/
protected int $limit = 500;
protected int $limit = 100;
/**
* @var OldDocument
* @var Document
*/
protected OldDocument $project;
protected Document $project;
/**
* @var OldDatabase
* @var Database
*/
protected OldDatabase $oldProjectDB;
protected Database $projectDB;
/**
* @var OldDatabase
* @var Database
*/
protected OldDatabase $oldConsoleDB;
protected Database $consoleDB;
/**
* @var array
*/
public static array $versions = [
'0.6.0' => 'V05',
'0.7.0' => 'V06',
'0.8.0' => 'V07',
'0.9.0' => 'V08',
'0.9.1' => 'V08',
'0.9.2' => 'V08',
'0.9.3' => 'V08',
'0.9.4' => 'V08',
'0.10.0' => 'V09',
'0.10.1' => 'V09',
'0.10.2' => 'V09',
'0.10.3' => 'V09',
'0.10.4' => 'V09',
'0.11.0' => 'V10',
'0.12.0' => 'V11',
'0.12.1' => 'V11',
'0.13.0' => 'V12',
];
/**
* Migration constructor.
*
* @param PDO $db
* @param Redis|null $cache
* @param array $options
* @return void
*/
public function __construct(PDO $db, Redis $cache = null, array $options = [])
{
$this->options = $options;
$this->db = $db;
if (!is_null($cache)) {
$this->cache = $cache;
}
}
/**
* Set project for migration.
*
* @param OldDocument $project
* @param OldDatabase $projectDB
* @param OldDatabase $oldConsoleDB
* @param Document $project
* @param Database $projectDB
* @param Database $oldConsoleDB
*
* @return self
*/
public function setProject(OldDocument $project, OldDatabase $projectDB, OldDatabase $oldConsoleDB): self
public function setProject(Document $project, Database $projectDB, Database $consoleDB): self
{
$this->project = $project;
$this->projectDB = $projectDB;
$this->projectDB->setNamespace('_project_' . $this->project->getId());
$this->oldProjectDB = $projectDB;
$this->oldProjectDB->setNamespace('app_' . $project->getId());
$this->oldConsoleDB = $oldConsoleDB;
$this->consoleDB = $consoleDB;
return $this;
}
@ -114,52 +65,72 @@ abstract class Migration
*/
public function forEachDocument(callable $callback): void
{
$sum = $this->limit;
$offset = 0;
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
while ($sum >= $this->limit) {
$all = $this->projectDB->getCollection([
'limit' => $this->limit,
'offset' => $offset,
'orderType' => 'DESC',
]);
/** @var array $collections */
$collections = Config::getParam('collections', []);
$sum = \count($all);
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
foreach ($collections as $collection) {
$sum = 0;
$nextDocument = null;
$collectionCount = $this->projectDB->count($collection['$id']);
Console::log('Migrating Collection ' . $collection['$id'] . ':');
Console::log('Migrating: ' . $offset . ' / ' . $this->projectDB->getSum());
\Co\run(function () use ($all, $callback) {
foreach ($all as $document) {
go(function () use ($document, $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
if ($document->getCollection() !== 0) {
Console::warning('Skipped Document due to missing ID or Collection.');
do {
$documents = $this->projectDB->find($collection['$id'], limit: $this->limit, cursor: $nextDocument);
$count = count($documents);
$sum += $count;
Console::log($sum . ' / ' . $collectionCount);
\Co\run(function (array $documents, callable $callback) {
foreach ($documents as $document) {
go(function (Document $document, callable $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
return;
}
return;
}
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
if (!$this->check_diff_multi($new->getArrayCopy(), $old)) {
return;
}
foreach ($document as &$attr) {
if ($attr instanceof Document) {
$attr = call_user_func($callback, $attr);
}
try {
$new = $this->projectDB->overwriteDocument($new->getArrayCopy());
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
if ($document && $new->getId() !== $document->getId()) {
throw new Exception('Duplication Error');
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = call_user_func($callback, $child);
}
}
}
}
}
});
if (!$this->check_diff_multi($new->getArrayCopy(), $old)) {
return;
}
try {
$new = $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
if ($document && $new->getId() !== $document->getId()) {
throw new Exception('Duplication Error');
}
}
}, $document, $callback);
}
}, $documents, $callback);
if ($count !== $this->limit) {
$nextDocument = null;
} else {
$nextDocument = end($documents);
}
});
$offset += $this->limit;
} while (!is_null($nextDocument));
}
}

View file

@ -1,126 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V05 extends Migration
{
public function execute(): void
{
$db = $this->db;
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
// Update all documents $uid -> $id
$this->forEachDocument([$this, 'fixDocument']);
$schema = $_SERVER['_APP_DB_SCHEMA'] ?? '';
try {
$statement = $db->prepare("
CREATE TABLE IF NOT EXISTS `template.database.unique` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `index1` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `{$schema}`.`app_{$project->getId()}.database.unique` LIKE `template.database.unique`;
ALTER TABLE `{$schema}`.`app_{$project->getId()}.audit.audit` DROP COLUMN IF EXISTS `userType`;
ALTER TABLE `{$schema}`.`app_{$project->getId()}.audit.audit` DROP INDEX IF EXISTS `index_1`;
ALTER TABLE `{$schema}`.`app_{$project->getId()}.audit.audit` ADD INDEX IF NOT EXISTS `index_1` (`userId` ASC);
");
$statement->closeCursor();
$statement->execute();
} catch (\Exception $e) {
Console::error('Failed to alter table for project: ' . $project->getId() . ' with message: ' . $e->getMessage() . '/');
}
}
protected function fixDocument(Document $document)
{
$providers = Config::getParam('providers');
switch ($document->getAttribute('$collection')) {
case Database::SYSTEM_COLLECTION_PROJECTS:
foreach ($providers as $key => $provider) {
if (!empty($document->getAttribute('usersOauth' . \ucfirst($key) . 'Appid'))) {
$document
->setAttribute('usersOauth2' . \ucfirst($key) . 'Appid', $document->getAttribute('usersOauth' . \ucfirst($key) . 'Appid', ''))
->removeAttribute('usersOauth' . \ucfirst($key) . 'Appid');
}
if (!empty($document->getAttribute('usersOauth' . \ucfirst($key) . 'Secret'))) {
$document
->setAttribute('usersOauth2' . \ucfirst($key) . 'Secret', $document->getAttribute('usersOauth' . \ucfirst($key) . 'Secret', ''))
->removeAttribute('usersOauth' . \ucfirst($key) . 'Secret');
}
}
$document->setAttribute('security', $document->getAttribute('security') ? true : false);
break;
case Database::SYSTEM_COLLECTION_TASKS:
$document->setAttribute('security', $document->getAttribute('security') ? true : false);
break;
case Database::SYSTEM_COLLECTION_USERS:
foreach ($providers as $key => $provider) {
if (!empty($document->getAttribute('oauth' . \ucfirst($key)))) {
$document
->setAttribute('oauth2' . \ucfirst($key), $document->getAttribute('oauth' . \ucfirst($key), ''))
->removeAttribute('oauth' . \ucfirst($key));
}
if (!empty($document->getAttribute('oauth' . \ucfirst($key) . 'AccessToken'))) {
$document
->setAttribute('oauth2' . \ucfirst($key) . 'AccessToken', $document->getAttribute('oauth' . \ucfirst($key) . 'AccessToken', ''))
->removeAttribute('oauth' . \ucfirst($key) . 'AccessToken');
}
}
if ($document->getAttribute('confirm', null) !== null) {
$document
->setAttribute('emailVerification', $document->getAttribute('confirm', $document->getAttribute('emailVerification', false)))
->removeAttribute('confirm');
}
break;
case Database::SYSTEM_COLLECTION_PLATFORMS:
if ($document->getAttribute('url', null) !== null) {
$document
->setAttribute('hostname', \parse_url($document->getAttribute('url', $document->getAttribute('hostname', '')), PHP_URL_HOST))
->removeAttribute('url');
}
break;
}
$document
->setAttribute('$id', $document->getAttribute('$uid', $document->getAttribute('$id')))
->removeAttribute('$uid');
foreach ($document as &$attr) { // Handle child documents
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,58 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Utopia\App;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Migration\Migration;
use Appwrite\OpenSSL\OpenSSL;
class V06 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->projectDB->disableFilters();
$this->forEachDocument([$this, 'fixDocument']);
$this->projectDB->enableFilters();
}
protected function fixDocument(Document $document)
{
switch ($document->getAttribute('$collection')) {
case Database::SYSTEM_COLLECTION_USERS:
if ($document->isSet('password-update')) {
$document
->setAttribute('passwordUpdate', $document->getAttribute('password-update', $document->getAttribute('passwordUpdate', '')))
->removeAttribute('password-update');
}
break;
case Database::SYSTEM_COLLECTION_KEYS:
if ($document->getAttribute('secret', null)) {
$json = \json_decode($document->getAttribute('secret'), true);
if (is_array($json)) {
Console::log('Secret already encrypted. Skipped: ' . $document->getId());
break;
}
$key = App::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$tag = null;
$document->setAttribute('secret', json_encode([
'data' => OpenSSL::encrypt($document->getAttribute('secret'), OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
'method' => OpenSSL::CIPHER_AES_128_GCM,
'iv' => \bin2hex($iv),
'tag' => \bin2hex($tag ?? ''),
'version' => '1',
]));
}
break;
}
return $document;
}
}

View file

@ -1,68 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V07 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
$providers = Config::getParam('providers');
switch ($document->getAttribute('$collection')) {
case Database::SYSTEM_COLLECTION_USERS:
/**
* Remove deprecated OAuth2 properties in the Users Documents.
*/
foreach ($providers as $key => $provider) {
if (!empty($document->getAttribute('oauth2' . \ucfirst($key)))) {
$document->removeAttribute('oauth2' . \ucfirst($key));
}
if (!empty($document->getAttribute('oauth2' . \ucfirst($key) . 'AccessToken'))) {
$document->removeAttribute('oauth2' . \ucfirst($key) . 'AccessToken');
}
}
/**
* Invalidate all Login Tokens, since they can't be migrated to the new structure.
* Reason for it is the missing distinction between E-Mail and OAuth2 tokens.
*/
$tokens = array_filter($document->getAttribute('tokens', []), function ($token) {
return ($token->getAttribute('type') != Auth::TOKEN_TYPE_LOGIN);
});
$document->setAttribute('tokens', array_values($tokens));
break;
}
foreach ($document as &$attr) { // Handle child documents
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,60 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V08 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
switch ($document->getAttribute('$collection')) {
/**
* Rename env attribute to runtime.
*/
case Database::SYSTEM_COLLECTION_FUNCTIONS:
if ($document->isSet('env')) {
$document
->setAttribute('runtime', $document->getAttribute('env', $document->getAttribute('env', '')))
->removeAttribute('env');
}
break;
/**
* Add version reference to database.
*/
case Database::SYSTEM_COLLECTION_PROJECTS:
$document->setAttribute('version', '0.9.0');
break;
}
foreach ($document as &$attr) {
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V09 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
switch ($document->getAttribute('$collection')) {
/**
* Add version reference to database.
*/
case Database::SYSTEM_COLLECTION_PROJECTS:
$document->setAttribute('version', '0.10.0');
break;
}
foreach ($document as &$attr) {
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V10 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
switch ($document->getAttribute('$collection')) {
/**
* Add version reference to database.
*/
case Database::SYSTEM_COLLECTION_PROJECTS:
$document->setAttribute('version', '0.11.0');
break;
}
foreach ($document as &$attr) {
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,821 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Database\Database as OldDatabase;
use Appwrite\Database\Document as OldDocument;
use Appwrite\Migration\Migration;
use Exception;
use PDO;
use Redis;
use Swoole\Runtime;
use Throwable;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Limit;
use Utopia\Database\Exception\Authorization as ExceptionAuthorization;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
global $register;
class V11 extends Migration
{
protected Database $dbProject;
protected Database $dbConsole;
protected array $oldCollections;
protected array $newCollections;
public function __construct(PDO $db, Redis $cache = null, array $options = [])
{
parent::__construct($db, $cache, $options);
$this->options = array_map(fn ($option) => $option === 'yes' ? true : false, $this->options);
if (!is_null($cache)) {
$this->cache->flushAll();
$cacheAdapter = new Cache(new RedisCache($this->cache));
$this->dbProject = new Database(new MariaDB($this->db), $cacheAdapter); // namespace is set on execution
$this->dbConsole = new Database(new MariaDB($this->db), $cacheAdapter);
$this->dbProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$this->dbConsole->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$this->dbConsole->setNamespace('_project_console');
}
$this->newCollections = Config::getParam('collections', []);
$this->oldCollections = Config::getParam('collectionsold', []);
}
public function execute(): void
{
Authorization::disable();
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
$oldProject = $this->project;
$this->dbProject->setNamespace('_project_' . $oldProject->getId());
$this->dbConsole->setNamespace('_project_console');
Console::info('');
Console::info('------------------------------------');
Console::info('Migrating project ' . $oldProject->getAttribute('name'));
Console::info('------------------------------------');
/**
* Create internal/external structure for projects and skip the console project.
*/
if ($oldProject->getId() !== 'console') {
try {
$project = $this->dbConsole->getDocument(collection: 'projects', id: $oldProject->getId());
} catch (\Throwable $th) {
Console::error($th->getTraceAsString());
}
/**
* Migrate Project Document.
*/
if ($project->isEmpty()) {
$newProject = $this->fixDocument($oldProject);
$newProject->setAttribute('version', '0.12.0');
$project = $this->dbConsole->createDocument('projects', $newProject);
Console::log('Created project document: ' . $oldProject->getAttribute('name') . ' (' . $oldProject->getId() . ')');
}
/**
* Create internal tables
*/
try {
Console::log('Created internal tables for : ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->dbProject->createMetadata();
} catch (\Throwable $th) {
}
/**
* Create Audit tables
*/
if ($this->dbProject->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($this->dbProject);
$audit->setup();
Console::log('Created audit tables for : ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
}
/**
* Create Abuse tables
*/
if ($this->dbProject->getCollection(TimeLimit::COLLECTION)->isEmpty()) {
$adapter = new TimeLimit("", 0, 1, $this->dbProject);
$adapter->setup();
Console::log('Created abuse tables for : ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
}
/**
* Create internal collections for Project
*/
foreach ($this->newCollections as $key => $collection) {
if (!$this->dbProject->getCollection($key)->isEmpty()) continue; // Skip if project collection already exists
$attributes = [];
$indexes = [];
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => $attribute['$id'],
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
]);
}
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document([
'$id' => $index['$id'],
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
$this->dbProject->createCollection($key, $attributes, $indexes);
}
if ($this->options['migrateCollections']) {
$this->migrateExternalCollections();
}
} else {
Console::log('Skipped console project migration.');
}
$sum = $this->limit;
$offset = 0;
$total = 0;
/**
* Migrate internal documents
*/
while ($sum >= $this->limit) {
$all = $this->oldProjectDB->getCollection([
'limit' => $this->limit,
'offset' => $offset,
'orderType' => 'DESC',
'filters' => [
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_COLLECTIONS,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_RULES,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_TASKS,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_PROJECTS,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_CONNECTIONS,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_RESERVED,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_TOKENS,
]
]);
$sum = \count($all);
Console::log('Migrating Internal Documents: ' . $offset . ' / ' . $this->oldProjectDB->getSum());
foreach ($all as $document) {
if (
!array_key_exists($document->getCollection(), $this->oldCollections)
) {
continue;
}
$new = $this->fixDocument($document);
if (is_null($new) || empty($new->getId())) {
Console::warning('Skipped Document due to missing ID.');
continue;
}
try {
if ($this->dbProject->getDocument($new->getCollection(), $new->getId())->isEmpty()) {
$this->dbProject->createDocument($new->getCollection(), $new);
}
} catch (\Throwable $th) {
Console::error("Failed to migrate document ({$new->getId()}) from collection ({$new->getCollection()}): " . $th->getMessage());
continue;
}
}
$offset += $this->limit;
$total += $sum;
}
Console::log('Migrated ' . $total . ' Internal Documents.');
}
/**
* Migrate external collections for Project
*
* @return void
* @throws Exception
* @throws Throwable
* @throws Limit
* @throws ExceptionAuthorization
* @throws Structure
*/
protected function migrateExternalCollections(): void
{
$sum = $this->limit;
$offset = 0;
while ($sum >= $this->limit) {
$databaseCollections = $this->oldProjectDB->getCollection([
'limit' => $this->limit,
'offset' => $offset,
'orderType' => 'DESC',
'filters' => [
'$collection=' . OldDatabase::SYSTEM_COLLECTION_COLLECTIONS,
]
]);
$sum = \count($databaseCollections);
Console::log('Migrating Collections: ' . $offset . ' / ' . $this->oldProjectDB->getSum());
foreach ($databaseCollections as $oldCollection) {
$id = $oldCollection->getId();
$permissions = $oldCollection->getPermissions();
$name = $oldCollection->getAttribute('name');
$newCollection = $this->dbProject->getCollection('collection_' . $id);
if ($newCollection->isEmpty()) {
$this->dbProject->createCollection('collection_' . $id);
/**
* Migrate permissions
*/
$read = $this->migrateWildcardPermissions($permissions['read'] ?? []);
$write = $this->migrateWildcardPermissions($permissions['write'] ?? []);
/**
* Suffix collection name with a subsequent number to make it unique if possible.
*/
$suffix = 1;
while ($this->dbProject->findOne('collections', [
new Query('name', Query::TYPE_EQUAL, [$name])
])) {
$name .= ' - ' . $suffix++;
}
$this->dbProject->createDocument('collections', new Document([
'$id' => $id,
'$read' => [],
'$write' => [],
'permission' => 'document',
'dateCreated' => time(),
'dateUpdated' => time(),
'name' => substr($name, 0, 256),
'enabled' => true,
'search' => implode(' ', [$id, $name]),
]));
} else {
Console::warning('Skipped Collection ' . $newCollection->getId() . ' from ' . $newCollection->getCollection());
}
/**
* Migrate collection rules to attributes
*/
$attributes = $this->getCollectionAttributes($oldCollection);
foreach ($attributes as $attribute) {
try {
$this->dbProject->createAttribute(
collection: 'collection_' . $attribute['$collection'],
id: $attribute['$id'],
type: $attribute['type'],
size: $attribute['size'],
required: $attribute['required'],
default: $attribute['default'],
signed: $attribute['signed'],
array: $attribute['array'],
format: $attribute['format'] ?? null,
formatOptions: $attribute['formatOptions'] ?? [],
filters: $attribute['filters']
);
$this->dbProject->createDocument('attributes', new Document([
'$id' => $attribute['$collection'] . '_' . $attribute['$id'],
'key' => $attribute['$id'],
'collectionId' => $attribute['$collection'],
'type' => $attribute['type'],
'status' => 'available',
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'default' => $attribute['default'],
'array' => $attribute['array'],
'format' => $attribute['format'] ?? null,
'formatOptions' => $attribute['formatOptions'] ?? null,
'filters' => $attribute['filters']
]));
Console::log('Created "' . $attribute['$id'] . '" attribute in collection: ' . $name);
} catch (\Throwable $th) {
Console::log($th->getMessage() . ' - ("' . $attribute['$id'] . '" attribute in collection ' . $name . ')');
}
}
if ($this->options['migrateDocuments']) {
$this->migrateExternalDocuments(collection: $id);
}
}
$offset += $this->limit;
}
}
/**
* Migrate all external documents
*
* @return void
* @throws Exception
* @throws Throwable
* @throws ExceptionAuthorization
* @throws Structure
*/
protected function migrateExternalDocuments(string $collection): void
{
$sum = $this->limit;
$offset = 0;
while ($sum >= $this->limit) {
$allDocs = $this->oldProjectDB->getCollection([
'limit' => $this->limit,
'offset' => $offset,
'orderType' => 'DESC',
'filters' => [
'$collection=' . $collection
]
]);
$sum = \count($allDocs);
Console::log('Migrating External Documents for Collection ' . $collection . ': ' . $offset . ' / ' . $this->oldProjectDB->getSum());
foreach ($allDocs as $document) {
if (!$this->dbProject->getDocument('collection_' . $collection, $document->getId())->isEmpty()) {
continue;
}
go(function ($document) {
foreach ($document as $key => $attr) {
/**
* Convert nested Document to JSON strings.
*/
if ($document->getAttribute($key) instanceof OldDocument) {
$document[$key] = json_encode($this->fixDocument($attr)->getArrayCopy());
}
/**
* Convert numeric Attributes to float.
*/
if (!is_string($attr) && is_numeric($attr)) {
$document[$key] = floatval($attr);
}
if (\is_array($attr)) {
foreach ($attr as $index => $child) {
/**
* Convert array of nested Document to array JSON strings.
*/
if ($document->getAttribute($key)[$index] instanceof OldDocument) {
$document[$key][$index] = json_encode($this->fixDocument($child)->getArrayCopy());
}
/**
* Convert array of numeric Attributes to array float.
*/
if (!is_string($child) && is_numeric($child)) {
$document[$key][$index] = floatval($child); // Convert any numeric to float
}
}
}
}
}, $document);
$document = new Document($document->getArrayCopy());
$document = $this->migratePermissions($document);
try {
$this->dbProject->createDocument('collection_' . $collection, $document);
} catch (\Throwable $th) {
Console::error("Failed to migrate document ({$document->getId()}): " . $th->getMessage());
continue;
}
}
$offset += $this->limit;
}
}
/**
* Migrates single docuemnt.
*
* @param OldDocument $oldDocument
* @return Document|null
* @throws Exception
*/
protected function fixDocument(OldDocument $oldDocument): Document|null
{
$document = new Document($oldDocument->getArrayCopy());
$document = $this->migratePermissions($document);
/**
* Check attributes and set their default values.
*/
if (array_key_exists($document->getCollection(), $this->oldCollections)) {
foreach ($this->newCollections[$document->getCollection()]['attributes'] ?? [] as $attr) {
if (
(!$attr['array'] ||
($attr['array'] && array_key_exists('filter', $attr)
&& in_array('json', $attr['filter'])))
&& empty($document->getAttribute($attr['$id'], null))
) {
$document->setAttribute($attr['$id'], $attr['default'] ?? null);
}
}
}
switch ($document->getAttribute('$collection')) {
case OldDatabase::SYSTEM_COLLECTION_PROJECTS:
$newProviders = [];
$newAuths = [];
$providers = Config::getParam('providers', []);
$auths = Config::getParam('auth', []);
/**
* Remove Tasks
*/
$document->removeAttribute('tasks');
/*
* Add enabled OAuth2 providers to default data rules
*/
foreach ($providers as $index => $provider) {
$appId = $document->getAttribute('usersOauth2' . \ucfirst($index) . 'Appid');
$appSecret = $document->getAttribute('usersOauth2' . \ucfirst($index) . 'Secret');
if (!is_null($appId) || !is_null($appId)) {
$newProviders[$appId] = $appSecret;
}
$document
->removeAttribute('usersOauth2' . \ucfirst($index) . 'Appid')
->removeAttribute('usersOauth2' . \ucfirst($index) . 'Secret');
}
$document->setAttribute('providers', $newProviders);
/*
* Migrate User providers settings
*/
$oldAuths = [
'email-password' => 'usersAuthEmailPassword',
'magic-url' => 'usersAuthMagicURL',
'anonymous' => 'usersAuthAnonymous',
'invites' => 'usersAuthInvites',
'jwt' => 'usersAuthJWT',
'phone' => 'usersAuthPhone'
];
foreach ($oldAuths as $index => $auth) {
$enabled = $document->getAttribute($auth, true);
$newAuths['auth' . \ucfirst($auths[$index]['key'])] = $enabled;
$document->removeAttribute($auth);
}
if (!empty($document->getAttribute('usersAuthLimit'))) {
$newAuths['limit'] = $document->getAttribute('usersAuthLimit');
}
$document->removeAttribute('usersAuthLimit');
$document->setAttribute('auths', $newProviders);
break;
case OldDatabase::SYSTEM_COLLECTION_PLATFORMS:
$projectId = $this->getProjectIdFromReadPermissions($document);
if (is_null($projectId)) {
return null;
}
/**
* Set Project ID
*/
if ($document->getAttribute('projectId') === null) {
$document->setAttribute('projectId', $projectId);
}
/**
* Set empty key and store if null
*/
if ($document->getAttribute('key') === null) {
$document->setAttribute('key', '');
}
if ($document->getAttribute('store') === null) {
$document->setAttribute('store', '');
}
/**
* Reset Permissions
*/
$document->setAttribute('$read', ['role:all']);
$document->setAttribute('$write', ['role:all']);
break;
case OldDatabase::SYSTEM_COLLECTION_CERTIFICATES:
/**
* Replace certificateId attribute.
*/
if ($document->getAttribute('certificateId') !== null) {
$document->setAttribute('$id', $document->getAttribute('certificateId'));
$document->removeAttribute('certificateId');
}
break;
case OldDatabase::SYSTEM_COLLECTION_DOMAINS:
$projectId = $this->getProjectIdFromReadPermissions($document);
if (is_null($projectId)) {
return null;
}
/**
* Set Project ID
*/
if ($document->getAttribute('projectId') === null) {
$document->setAttribute('projectId', $projectId);
}
/**
* Set empty verification if null
*/
if ($document->getAttribute('verification') === null) {
$document->setAttribute('verification', false);
}
/**
* Reset Permissions
*/
$document->setAttribute('$read', ['role:all']);
$document->setAttribute('$write', ['role:all']);
break;
case OldDatabase::SYSTEM_COLLECTION_KEYS:
$projectId = $this->getProjectIdFromReadPermissions($document);
if (is_null($projectId)) {
return null;
}
/**
* Set Project ID
*/
if ($document->getAttribute('projectId') === null) {
$document->setAttribute('projectId', $projectId);
}
/**
* Set scopes if empty
*/
if (empty($document->getAttribute('scopes', []))) {
$document->setAttribute('scopes', []);
}
/**
* Reset Permissions
*/
$document->setAttribute('$read', ['role:all']);
$document->setAttribute('$write', ['role:all']);
break;
case OldDatabase::SYSTEM_COLLECTION_FUNCTIONS:
$document->setAttribute('events', $document->getAttribute('events', []));
break;
case OldDatabase::SYSTEM_COLLECTION_WEBHOOKS:
$projectId = $this->getProjectIdFromReadPermissions($document);
if (is_null($projectId)) {
return null;
}
/**
* Set Project ID
*/
if ($document->getAttribute('projectId') === null) {
$document->setAttribute('projectId', $projectId);
}
$document->setAttribute('events', $document->getAttribute('events', []));
/**
* Reset Permissions
*/
$document->setAttribute('$read', ['role:all']);
$document->setAttribute('$write', ['role:all']);
break;
case OldDatabase::SYSTEM_COLLECTION_USERS:
/**
* Set deleted attribute to false
*/
if ($document->getAttribute('deleted') === null) {
$document->setAttribute('deleted', false);
}
/**
* Remove deprecated user status 0 and replace with boolean.
*/
if ($document->getAttribute('status') === 2) {
$document->setAttribute('status', false);
} else {
$document->setAttribute('status', true);
}
/**
* Set default values for arrays if not set.
*/
if (empty($document->getAttribute('prefs', []))) {
$document->setAttribute('prefs', new \stdClass());
}
if (empty($document->getAttribute('sessions', []))) {
$document->setAttribute('sessions', []);
}
if (empty($document->getAttribute('tokens', []))) {
$document->setAttribute('tokens', []);
}
if (empty($document->getAttribute('memberships', []))) {
$document->setAttribute('memberships', []);
}
/**
* Replace user:{self} with user:USER_ID
*/
$write = $document->getWrite();
$document->setAttribute('$write', str_replace('user:{self}', "user:{$document->getId()}", $write));
break;
case OldDatabase::SYSTEM_COLLECTION_TEAMS:
/**
* Replace team:{self} with team:TEAM_ID
*/
$read = $document->getWrite();
$write = $document->getWrite();
$document->setAttribute('$read', str_replace('team:{self}', "team:{$document->getId()}", $read));
$document->setAttribute('$write', str_replace('team:{self}', "team:{$document->getId()}", $write));
break;
case OldDatabase::SYSTEM_COLLECTION_FILES:
/**
* Migrating breakind changes on Files.
*/
if (!empty($document->getAttribute('fileOpenSSLVersion', null))) {
$document
->setAttribute('openSSLVersion', $document->getAttribute('fileOpenSSLVersion'))
->removeAttribute('fileOpenSSLVersion');
}
if (!empty($document->getAttribute('fileOpenSSLCipher', null))) {
$document
->setAttribute('openSSLCipher', $document->getAttribute('fileOpenSSLCipher'))
->removeAttribute('fileOpenSSLCipher');
}
if (!empty($document->getAttribute('fileOpenSSLTag', null))) {
$document
->setAttribute('openSSLTag', $document->getAttribute('fileOpenSSLTag'))
->removeAttribute('fileOpenSSLTag');
}
if (!empty($document->getAttribute('fileOpenSSLIV', null))) {
$document
->setAttribute('openSSLIV', $document->getAttribute('fileOpenSSLIV'))
->removeAttribute('fileOpenSSLIV');
}
/**
* Remove deprecated attributes.
*/
$document->removeAttribute('folderId');
$document->removeAttribute('token');
break;
}
return $document;
}
/**
* Migrates $permissions to independent $read and $write.
* @param Document $document
* @return Document
*/
protected function migratePermissions(Document $document): Document
{
if ($document->isSet('$permissions')) {
$permissions = $document->getAttribute('$permissions', []);
$read = $this->migrateWildcardPermissions($permissions['read'] ?? []);
$write = $this->migrateWildcardPermissions($permissions['write'] ?? []);
$document->setAttribute('$read', $read);
$document->setAttribute('$write', $write);
$document->removeAttribute('$permissions');
}
return $document;
}
/**
* Takes a permissions array and replaces wildcard * with role:all.
* @param array $permissions
* @return array
*/
protected function migrateWildcardPermissions(array $permissions): array
{
return array_map(function ($permission) {
if ($permission === '*') return 'role:all';
return $permission;
}, $permissions);
}
/**
* Get new collection attributes from old collection rules.
* @param OldDocument $collection
* @return array
*/
protected function getCollectionAttributes(OldDocument $collection): array
{
$attributes = [];
foreach ($collection->getAttribute('rules', []) as $key => $value) {
$collectionId = $collection->getId();
$id = $value['key'];
$array = $value['array'] ?? false;
$required = $value['required'] ?? false;
$default = $value['default'] ?? null;
$default = match ($value['type']) {
OldDatabase::SYSTEM_VAR_TYPE_NUMERIC => floatval($default),
default => $default
};
$type = match ($value['type']) {
OldDatabase::SYSTEM_VAR_TYPE_TEXT => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_EMAIL => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_DOCUMENT => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_IP => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_URL => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_WILDCARD => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_NUMERIC => Database::VAR_FLOAT,
OldDatabase::SYSTEM_VAR_TYPE_BOOLEAN => Database::VAR_BOOLEAN,
default => Database::VAR_STRING
};
$size = $type === Database::VAR_STRING ? 65_535 : 0; // Max size of text in MariaDB
if ($required) {
$default = null;
}
$attributes[$key] = [
'$collection' => $collectionId,
'$id' => $id,
'type' => $type,
'size' => $size,
'required' => $required,
'default' => $default,
'array' => $array,
'signed' => true,
'filters' => []
];
if ($type === Database::VAR_FLOAT) {
$attributes[$key]['format'] = APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
$attributes[$key]['formatOptions'] = [];
$attributes[$key]['formatOptions']['min'] = -PHP_FLOAT_MAX;
$attributes[$key]['formatOptions']['max'] = PHP_FLOAT_MAX;
}
}
return $attributes;
}
/**
* @param Document $document
* @return string|null
* @throws Exception
*/
protected function getProjectIdFromReadPermissions(Document $document): string|null
{
$readPermissions = $document->getRead();
$teamId = str_replace('team:', '', reset($readPermissions));
$project = $this->oldConsoleDB->getCollectionFirst([
'filters' => [
'$collection=' . OldDatabase::SYSTEM_COLLECTION_PROJECTS,
'teamId=' . $teamId
]
]);
if (!$project) {
return null;
}
return $project->getId();
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\CLI\Console;
use Utopia\Database\Document;
class V12 extends Migration
{
public function execute(): void
{
Console::log('Migrating project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
switch ($document->getCollection()) {
case 'projects':
/**
* Bump Project version number.
*/
$document->setAttribute('version', '0.13.0');
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'name'], $document));
}
break;
case 'users':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'email', 'name'], $document));
}
break;
case 'teams':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'name'], $document));
}
break;
case 'files':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'name'], $document));
}
break;
case 'functions':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'name', 'runtime'], $document));
}
break;
case 'tags':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'command'], $document));
}
break;
case 'executions':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'functionId'], $document));
}
break;
}
return $document;
}
/**
* Builds a search string for a fulltext index.
*
* @param array $values
* @param Document $document
* @return string
*/
private function buildSearchAttribute(array $values, Document $document): string
{
$values = array_filter(array_map(fn (string $value) => $document->getAttribute($value) ?? '', $values));
return implode(' ', $values);
}
}

View file

@ -275,7 +275,7 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = false;
break;
case 'Utopia\Database\Validator\UID':
case 'Appwrite\Database\Validator\CustomId':
case 'Appwrite\Utopia\Database\Validator\CustomId':
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']';
break;
@ -325,9 +325,14 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = $validator->getMin();
break;
case 'Utopia\Validator\Numeric':
case 'Utopia\Validator\Integer':
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'int32';
break;
case 'Utopia\Validator\FloatValidator':
$node['schema']['type'] = 'number';
$node['schema']['format'] = 'float';
break;
case 'Utopia\Validator\Length':
$node['schema']['type'] = $validator->getType();
break;

View file

@ -263,7 +263,7 @@ class Swagger2 extends Format
$node['x-example'] = false;
break;
case 'Utopia\Database\Validator\UID':
case 'Appwrite\Database\Validator\CustomId':
case 'Appwrite\Utopia\Database\Validator\CustomId':
$node['type'] = $validator->getType();
$node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']';
break;
@ -319,6 +319,10 @@ class Swagger2 extends Format
$node['type'] = $validator->getType();
$node['format'] = 'int32';
break;
case 'Utopia\Validator\FloatValidator':
$node['type'] = 'number';
$node['format'] = 'float';
break;
case 'Utopia\Validator\Length':
$node['type'] = $validator->getType();
break;

Some files were not shown because too many files have changed in this diff Show more