diff --git a/Dockerfile b/Dockerfile index c19e7b432e..b24fd94918 100755 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT RUN npm ci RUN npm run build -FROM appwrite/base:0.3.1 as final +FROM appwrite/base:0.4.2 as final LABEL maintainer="team@appwrite.io" diff --git a/app/config/errors.php b/app/config/errors.php index f884e49a42..a55cefbb17 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -195,6 +195,21 @@ return [ 'description' => 'Missing ID from OAuth2 provider.', 'code' => 400, ], + Exception::USER_OAUTH2_BAD_REQUEST => [ + 'name' => Exception::USER_OAUTH2_BAD_REQUEST, + 'description' => 'OAuth2 provider rejected the bad request.', + 'code' => 400, + ], + Exception::USER_OAUTH2_UNAUTHORIZED => [ + 'name' => Exception::USER_OAUTH2_UNAUTHORIZED, + 'description' => 'OAuth2 provider rejected the unauthorized request.', + 'code' => 401, + ], + Exception::USER_OAUTH2_PROVIDER_ERROR => [ + 'name' => Exception::USER_OAUTH2_PROVIDER_ERROR, + 'description' => 'OAuth2 provider returned some error.', + 'code' => 424, + ], /** Teams */ Exception::TEAM_NOT_FOUND => [ diff --git a/app/config/variables.php b/app/config/variables.php index 978a853ef6..4498f6371b 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -413,7 +413,7 @@ return [ 'variables' => [ [ 'name' => '_APP_SMS_PROVIDER', - 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.", + 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.", 'introduction' => '0.15.0', 'default' => '', 'required' => false, diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index dd5ac4a2da..52b621e109 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2,6 +2,7 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; +use Appwrite\Auth\OAuth2\Exception as OAuth2Exception; use Appwrite\Auth\Validator\Password; use Appwrite\Auth\Validator\Phone; use Appwrite\Detector\Detector; @@ -326,17 +327,19 @@ App::get('/v1/account/sessions/oauth2/:provider') App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 Callback') - ->groups(['api', 'account']) + ->groups(['account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') ->label('scope', 'public') ->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(2048), 'OAuth2 code.') + ->param('code', '', new Text(2048, 0), 'OAuth2 code.', true) ->param('state', '', new Text(2048), 'Login state params.', true) + ->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true) + ->param('error_description', '', new Text(2048, 0), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.', true) ->inject('request') ->inject('response') - ->action(function (string $projectId, string $provider, string $code, string $state, Request $request, Response $response) { + ->action(function (string $projectId, string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response) { $domain = $request->getHostname(); $protocol = $request->getProtocol(); @@ -345,23 +348,31 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') ->addHeader('Pragma', 'no-cache') ->redirect($protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?' - . \http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); + . \http_build_query([ + 'project' => $projectId, + 'code' => $code, + 'state' => $state, + 'error' => $error, + 'error_description' => $error_description + ])); }); App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 Callback') - ->groups(['api', 'account']) + ->groups(['account']) ->label('error', __DIR__ . '/../../views/general/error.phtml') ->label('scope', 'public') ->label('origin', '*') ->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(2048), 'OAuth2 code.') + ->param('code', '', new Text(2048, 0), 'OAuth2 code.', true) ->param('state', '', new Text(2048), 'Login state params.', true) + ->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true) + ->param('error_description', '', new Text(2048, 0), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.', true) ->inject('request') ->inject('response') - ->action(function (string $projectId, string $provider, string $code, string $state, Request $request, Response $response) { + ->action(function (string $projectId, string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response) { $domain = $request->getHostname(); $protocol = $request->getProtocol(); @@ -370,7 +381,13 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') ->addHeader('Pragma', 'no-cache') ->redirect($protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?' - . \http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); + . \http_build_query([ + 'project' => $projectId, + 'code' => $code, + 'state' => $state, + 'error' => $error, + 'error_description' => $error_description + ])); }); App::get('/v1/account/sessions/oauth2/:provider/redirect') @@ -388,8 +405,10 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->label('usage.metric', 'sessions.{scope}.requests.create') ->label('usage.params', ['provider:{request.provider}']) ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.') - ->param('code', '', new Text(2048), 'OAuth2 code.') + ->param('code', '', new Text(2048, 0), 'OAuth2 code.', true) ->param('state', '', new Text(2048), 'OAuth2 state params.', true) + ->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true) + ->param('error_description', '', new Text(2048, 0), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.', true) ->inject('request') ->inject('response') ->inject('project') @@ -397,7 +416,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->inject('dbForProject') ->inject('geodb') ->inject('events') - ->action(function (string $provider, string $code, string $state, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $events) use ($oauthDefaultSuccess) { + ->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $events) use ($oauthDefaultSuccess) { $protocol = $request->getProtocol(); $callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId(); @@ -407,27 +426,22 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}'; $providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false; - if (!$providerEnabled) { - throw new Exception(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.'); - } - - if (!empty($appSecret) && isset($appSecret['version'])) { - $key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']); - $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); - } - $className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider); if (!\class_exists($className)) { throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); } + $providers = Config::getParam('providers'); + $providerName = $providers[$provider]['name'] ?? ''; + + /** @var Appwrite\Auth\OAuth2 $oauth2 */ $oauth2 = new $className($appId, $appSecret, $callback); if (!empty($state)) { try { $state = \array_merge($defaultState, $oauth2->parseState($state)); - } catch (\Exception$exception) { + } catch (\Exception $exception) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to parse login state params as passed from OAuth2 provider'); } } else { @@ -441,27 +455,66 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) { throw new Exception(Exception::PROJECT_INVALID_FAILURE_URL); } - - $accessToken = $oauth2->getAccessToken($code); - $refreshToken = $oauth2->getRefreshToken($code); - $accessTokenExpiry = $oauth2->getAccessTokenExpiry($code); - - if (empty($accessToken)) { - if (!empty($state['failure'])) { - $response->redirect($state['failure'], 301, 0); + $failure = []; + if (!empty($state['failure'])) { + $failure = URLParser::parse($state['failure']); + } + $failureRedirect = (function (string $type, ?string $message = null, ?int $code = null) use ($failure, $response) { + $exception = new Exception($type, $message, $code); + if (!empty($failure)) { + $query = URLParser::parseQuery($failure['query']); + $query['error'] = json_encode([ + 'message' => $exception->getMessage(), + 'type' => $exception->getType(), + 'code' => !\is_null($code) ? $code : $exception->getCode(), + ]); + $failure['query'] = URLParser::unparseQuery($query); + $response->redirect(URLParser::unparse($failure), 301); } - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to obtain access token'); + throw $exception; + }); + + if (!$providerEnabled) { + $failureRedirect(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.'); + } + + if (!empty($error)) { + $message = 'The ' . $providerName . ' OAuth2 provider returned an error: ' . $error; + if (!empty($error_description)) { + $message .= ': ' . $error_description; + } + $failureRedirect(Exception::USER_OAUTH2_PROVIDER_ERROR, $message); + } + + if (empty($code)) { + $failureRedirect(Exception::USER_OAUTH2_PROVIDER_ERROR, 'Missing OAuth2 code. Please contact the Appwrite team for additional support.'); + } + + if (!empty($appSecret) && isset($appSecret['version'])) { + $key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); + } + + $accessToken = ''; + $refreshToken = ''; + $accessTokenExpiry = 0; + + try { + $accessToken = $oauth2->getAccessToken($code); + $refreshToken = $oauth2->getRefreshToken($code); + $accessTokenExpiry = $oauth2->getAccessTokenExpiry($code); + } catch (OAuth2Exception $ex) { + $failureRedirect( + $ex->getType(), + 'Failed to obtain access token. The ' . $providerName . ' OAuth2 provider returned an error: ' . $ex->getMessage(), + $ex->getCode(), + ); } $oauth2ID = $oauth2->getUserID($accessToken); - if (empty($oauth2ID)) { - if (!empty($state['failure'])) { - $response->redirect($state['failure'], 301, 0); - } - - throw new Exception(Exception::USER_MISSING_ID); + $failureRedirect(Exception::USER_MISSING_ID); } $sessions = $user->getAttribute('sessions', []); @@ -501,7 +554,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $total = $dbForProject->count('users', max: APP_LIMIT_USERS); if ($total >= $limit) { - throw new Exception(Exception::USER_COUNT_EXCEEDED); + $failureRedirect(Exception::USER_COUNT_EXCEEDED); } } @@ -535,13 +588,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'search' => implode(' ', [$userId, $email, $name]) ]))); } catch (Duplicate $th) { - throw new Exception(Exception::USER_ALREADY_EXISTS); + $failureRedirect(Exception::USER_ALREADY_EXISTS); } } } if (false === $user->getAttribute('status')) { // Account is blocked - throw new Exception(Exception::USER_BLOCKED); // User is in status blocked + $failureRedirect(Exception::USER_BLOCKED); // User is in status blocked } // Create session token, verify user account and update OAuth2 ID and Access Token diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index cd217afe31..21c84e8a07 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -8,10 +8,10 @@ use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Network\Validator\Email; use Appwrite\Utopia\Database\Validator\CustomId; -use Appwrite\Utopia\Database\Validator\Queries; +use Utopia\Database\Validator\Queries; use Appwrite\Utopia\Database\Validator\Queries\Users; -use Appwrite\Utopia\Database\Validator\Query\Limit; -use Appwrite\Utopia\Database\Validator\Query\Offset; +use Utopia\Database\Validator\Query\Limit; +use Utopia\Database\Validator\Query\Offset; use Appwrite\Utopia\Response; use Utopia\App; use Utopia\Audit\Audit; @@ -28,6 +28,7 @@ use Utopia\Database\Validator\UID; use Utopia\Database\Database; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Validator\ArrayList; use Utopia\Validator\Assoc; use Utopia\Validator\WhiteList; use Utopia\Validator\Text; @@ -65,6 +66,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'phone' => $phone, 'phoneVerification' => false, 'status' => true, + 'labels' => [], 'password' => $password, 'passwordHistory' => is_null($password) && $passwordHistory === 0 ? [] : [$password], 'passwordUpdate' => (!empty($password)) ? DateTime::now() : null, @@ -386,7 +388,7 @@ App::get('/v1/users') } // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE); + $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); $cursor = reset($cursor); if ($cursor) { /** @var Query $cursor */ @@ -557,7 +559,7 @@ App::get('/v1/users/:userId/logs') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_LOG_LIST) ->param('userId', '', new UID(), 'User ID.') - ->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) ->inject('response') ->inject('dbForProject') ->inject('locale') @@ -577,7 +579,7 @@ App::get('/v1/users/:userId/logs') $audit = new Audit($dbForProject); - $logs = $audit->getLogsByUser($user->getId(), $limit, $offset); + $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset); $output = []; @@ -663,27 +665,28 @@ App::patch('/v1/users/:userId/status') $response->dynamic($user, Response::MODEL_USER); }); -App::patch('/v1/users/:userId/verification') - ->desc('Update Email Verification') +App::put('/v1/users/:userId/labels') + ->desc('Update User Labels') ->groups(['api', 'users']) - ->label('event', 'users.[userId].update.verification') + ->label('event', 'users.[userId].update.labels') ->label('scope', 'users.write') - ->label('audits.event', 'verification.update') + ->label('audits.event', 'user.update') ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') ->label('usage.metric', 'users.{scope}.requests.update') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'users') - ->label('sdk.method', 'updateEmailVerification') - ->label('sdk.description', '/docs/references/users/update-user-email-verification.md') + ->label('sdk.method', 'updateLabels') + ->label('sdk.description', '/docs/references/users/update-user-labels.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USER) ->param('userId', '', new UID(), 'User ID.') - ->param('emailVerification', false, new Boolean(), 'User email verification status.') + ->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), 5), 'Array of user labels. Replaces the previous labels. Maximum of 5 labels are allowed, each up to 36 alphanumeric characters long.') ->inject('response') ->inject('dbForProject') ->inject('events') - ->action(function (string $userId, bool $emailVerification, Response $response, Database $dbForProject, Event $events) { + ->action(function (string $userId, array $labels, Response $response, Database $dbForProject, Event $events) { $user = $dbForProject->getDocument('users', $userId); @@ -691,7 +694,9 @@ App::patch('/v1/users/:userId/verification') throw new Exception(Exception::USER_NOT_FOUND); } - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification)); + $user->setAttribute('labels', (array) \array_values(\array_unique($labels))); + + $user = $dbForProject->updateDocument('users', $user->getId(), $user); $events ->setParam('userId', $user->getId()); @@ -764,10 +769,7 @@ App::patch('/v1/users/:userId/name') throw new Exception(Exception::USER_NOT_FOUND); } - $user - ->setAttribute('name', $name) - ->setAttribute('search', \implode(' ', [$user->getId(), $user->getAttribute('email', ''), $name, $user->getAttribute('phone', '')])); - ; + $user->setAttribute('name', $name); $user = $dbForProject->updateDocument('users', $user->getId(), $user); @@ -869,7 +871,8 @@ App::patch('/v1/users/:userId/email') $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) - ->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name', ''), $user->getAttribute('phone', '')])); + ; + try { $user = $dbForProject->updateDocument('users', $user->getId(), $user); @@ -913,7 +916,6 @@ App::patch('/v1/users/:userId/phone') $user ->setAttribute('phone', $number) ->setAttribute('phoneVerification', false) - ->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $number])); ; try { diff --git a/app/controllers/general.php b/app/controllers/general.php index fe25f5c273..46a57437d1 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -551,6 +551,7 @@ App::error() ->setParam('projectName', $project->getAttribute('name')) ->setParam('projectURL', $project->getAttribute('url')) ->setParam('message', $error->getMessage()) + ->setParam('type', $type) ->setParam('code', $code) ->setParam('trace', $trace) ; diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index d5a11246f7..450dcf8973 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -1,5 +1,6 @@ getParam('development', false); +$type = $this->getParam('type', 'general_server_error'); $code = $this->getParam('code', 500); $errorID = $this->getParam('errorID', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); $message = $this->getParam('message', ''); @@ -13,25 +14,114 @@ $title = $this->getParam('title', '') - - - - <?php echo $title; ?> + + + + + + + + + + + + + + + + <?php echo $this->print($title, self::FILTER_ESCAPE); ?> + + +
+
+
+

Error print($code, self::FILTER_ESCAPE); ?>

+

print($message, self::FILTER_ESCAPE); ?>

+
+

Type

+

print($type, self::FILTER_ESCAPE); ?>

+
+ +

Error Trace

+ +
+
+ + + $value) : ?> + + + + -
-

Error

+ +
+
print($key, self::FILTER_ESCAPE); ?> + + +
print(var_export($value, true), self::FILTER_ESCAPE); ?>
+ +
print($value, self::FILTER_ESCAPE); ?>
+ +
+
+
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/app/workers/deletes.php b/app/workers/deletes.php index f27bc4feb9..59f84f5c31 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -280,6 +280,7 @@ class DeletesV1 extends Worker foreach ($projects as $project) { $this->deleteProject($project); + $dbForConsole->deleteDocument('projects', $project->getId()); } } diff --git a/composer.json b/composer.json index 072a09db42..28c032b705 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "utopia-php/preloader": "0.2.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "0.14.*", - "utopia-php/swoole": "0.5.*", + "utopia-php/swoole": "0.8.*", "utopia-php/websocket": "0.1.*", "utopia-php/transfer": "dev-feat-improve-features", "resque/php-resque": "1.3.6", @@ -87,8 +87,8 @@ "appwrite/sdk-generator": "0.32.*", "ext-fileinfo": "*", "phpunit/phpunit": "9.5.20", - "squizlabs/php_codesniffer": "^3.6", - "swoole/ide-helper": "4.8.9", + "squizlabs/php_codesniffer": "^3.7", + "swoole/ide-helper": "5.0.2", "textalk/websocket": "1.5.7" }, "provide": { diff --git a/composer.lock b/composer.lock index 045524f753..edb44a37a6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c57d2e44484b37a709868463f2278c1b", + "content-hash": "abdadbedcaeec7a030b6d16c61753ce7", "packages": [ { "name": "adhocore/jwt", @@ -648,16 +648,16 @@ }, { "name": "guzzlehttp/promises", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6" + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/3a494dc7dc1d7d12e511890177ae2d0e6c107da6", - "reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6", + "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", "shasum": "" }, "require": { @@ -711,7 +711,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.0" + "source": "https://github.com/guzzle/promises/tree/2.0.1" }, "funding": [ { @@ -727,20 +727,20 @@ "type": "tidelift" } ], - "time": "2023-05-21T13:50:22+00:00" + "time": "2023-08-03T15:11:55+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.5.0", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", - "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/8bd7c33a0734ae1c5d074360512beb716bef3f77", + "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77", "shasum": "" }, "require": { @@ -827,7 +827,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.5.0" + "source": "https://github.com/guzzle/psr7/tree/2.6.0" }, "funding": [ { @@ -843,7 +843,7 @@ "type": "tidelift" } ], - "time": "2023-04-17T16:11:26+00:00" + "time": "2023-08-03T15:06:02+00:00" }, { "name": "influxdb/influxdb-php", @@ -2781,28 +2781,28 @@ }, { "name": "utopia-php/swoole", - "version": "0.5.0", + "version": "0.8.0", "source": { "type": "git", "url": "https://github.com/utopia-php/swoole.git", - "reference": "c2a3a4f944a2f22945af3cbcb95b13f0769628b1" + "reference": "5b60e7f730641cc182bc36b1f9939d4a76d3439b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/swoole/zipball/c2a3a4f944a2f22945af3cbcb95b13f0769628b1", - "reference": "c2a3a4f944a2f22945af3cbcb95b13f0769628b1", + "url": "https://api.github.com/repos/utopia-php/swoole/zipball/5b60e7f730641cc182bc36b1f9939d4a76d3439b", + "reference": "5b60e7f730641cc182bc36b1f9939d4a76d3439b", "shasum": "" }, "require": { "ext-swoole": "*", "php": ">=8.0", - "utopia-php/framework": "0.*.*" + "utopia-php/framework": "0.28.*" }, "require-dev": { "laravel/pint": "1.2.*", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.3", - "swoole/ide-helper": "4.8.3", - "vimeo/psalm": "4.15.0" + "swoole/ide-helper": "5.0.2" }, "type": "library", "autoload": { @@ -2826,9 +2826,9 @@ ], "support": { "issues": "https://github.com/utopia-php/swoole/issues", - "source": "https://github.com/utopia-php/swoole/tree/0.5.0" + "source": "https://github.com/utopia-php/swoole/tree/0.8.0" }, - "time": "2022-10-19T22:19:07+00:00" + "time": "2023-07-25T10:29:58+00:00" }, { "name": "utopia-php/system", @@ -2893,12 +2893,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/transfer.git", - "reference": "7362d0ee225b977bab92a0731edd1e0841b0aaae" + "reference": "ba56752691fd18e82c65fc53647b3831e3866801" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/transfer/zipball/7362d0ee225b977bab92a0731edd1e0841b0aaae", - "reference": "7362d0ee225b977bab92a0731edd1e0841b0aaae", + "url": "https://api.github.com/repos/utopia-php/transfer/zipball/ba56752691fd18e82c65fc53647b3831e3866801", + "reference": "ba56752691fd18e82c65fc53647b3831e3866801", "shasum": "" }, "require": { @@ -2955,7 +2955,7 @@ "source": "https://github.com/utopia-php/transfer/tree/feat-improve-features", "issues": "https://github.com/utopia-php/transfer/issues" }, - "time": "2023-07-26T12:56:15+00:00" + "time": "2023-08-04T01:59:28+00:00" }, { "name": "utopia-php/websocket", @@ -3896,16 +3896,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.23.0", + "version": "1.23.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "a2b24135c35852b348894320d47b3902a94bc494" + "reference": "846ae76eef31c6d7790fac9bc399ecee45160b26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a2b24135c35852b348894320d47b3902a94bc494", - "reference": "a2b24135c35852b348894320d47b3902a94bc494", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/846ae76eef31c6d7790fac9bc399ecee45160b26", + "reference": "846ae76eef31c6d7790fac9bc399ecee45160b26", "shasum": "" }, "require": { @@ -3937,9 +3937,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.23.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.23.1" }, - "time": "2023-07-23T22:17:56+00:00" + "time": "2023-08-03T16:32:59+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4869,16 +4869,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "bde739e7565280bda77be70044ac1047bc007e34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", "shasum": "" }, "require": { @@ -4921,7 +4921,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" }, "funding": [ { @@ -4929,7 +4929,7 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-08-02T09:26:13+00:00" }, { "name": "sebastian/lines-of-code", @@ -5386,16 +5386,16 @@ }, { "name": "swoole/ide-helper", - "version": "4.8.9", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/swoole/ide-helper.git", - "reference": "8f82ba3b6af04a5bccb97c1654af992d1ee8b0fe" + "reference": "16cfee44a6ec92254228c39bcab2fb8ae74cc2ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swoole/ide-helper/zipball/8f82ba3b6af04a5bccb97c1654af992d1ee8b0fe", - "reference": "8f82ba3b6af04a5bccb97c1654af992d1ee8b0fe", + "url": "https://api.github.com/repos/swoole/ide-helper/zipball/16cfee44a6ec92254228c39bcab2fb8ae74cc2ea", + "reference": "16cfee44a6ec92254228c39bcab2fb8ae74cc2ea", "shasum": "" }, "type": "library", @@ -5412,19 +5412,9 @@ "description": "IDE help files for Swoole.", "support": { "issues": "https://github.com/swoole/ide-helper/issues", - "source": "https://github.com/swoole/ide-helper/tree/4.8.9" + "source": "https://github.com/swoole/ide-helper/tree/5.0.2" }, - "funding": [ - { - "url": "https://gitee.com/swoole/swoole?donate=true", - "type": "custom" - }, - { - "url": "https://github.com/swoole", - "type": "github" - } - ], - "time": "2022-04-18T20:38:04+00:00" + "time": "2023-03-20T06:05:55+00:00" }, { "name": "symfony/polyfill-ctype", diff --git a/public/images/vcs/animation-building.gif b/public/images/vcs/animation-building.gif new file mode 100644 index 0000000000..935d657903 Binary files /dev/null and b/public/images/vcs/animation-building.gif differ diff --git a/public/images/vcs/state-failed.png b/public/images/vcs/state-failed.png new file mode 100644 index 0000000000..0c4cb90adb Binary files /dev/null and b/public/images/vcs/state-failed.png differ diff --git a/public/images/vcs/state-success.png b/public/images/vcs/state-success.png new file mode 100644 index 0000000000..2a759b01d1 Binary files /dev/null and b/public/images/vcs/state-success.png differ diff --git a/public/images/vcs/state-waiting.png b/public/images/vcs/state-waiting.png new file mode 100644 index 0000000000..505f981ac1 Binary files /dev/null and b/public/images/vcs/state-waiting.png differ diff --git a/src/Appwrite/Auth/OAuth2.php b/src/Appwrite/Auth/OAuth2.php index d7c1c99546..c737e183f8 100644 --- a/src/Appwrite/Auth/OAuth2.php +++ b/src/Appwrite/Auth/OAuth2.php @@ -2,6 +2,8 @@ namespace Appwrite\Auth; +use Appwrite\Auth\OAuth2\Exception; + abstract class OAuth2 { /** @@ -73,6 +75,13 @@ abstract class OAuth2 */ abstract public function refreshTokens(string $refreshToken): array; + /** + * @param string $accessToken + * + * @return string + */ + abstract public function getUserID(string $accessToken): string; + /** * @param string $accessToken * @@ -148,11 +157,11 @@ abstract class OAuth2 * * @return string */ - public function getAccessTokenExpiry(string $code): string + public function getAccessTokenExpiry(string $code): int { $tokens = $this->getTokens($code); - return $tokens['expires_in'] ?? ''; + return $tokens['expires_in'] ?? 0; } // The parseState function was designed specifically for Amazon OAuth2 Adapter to override. @@ -195,8 +204,14 @@ abstract class OAuth2 // Send the request & save response to $response $response = \curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + \curl_close($ch); + if ($code != 200) { + throw new Exception($response, $code); + } + return (string)$response; } } diff --git a/src/Appwrite/Auth/OAuth2/Exception.php b/src/Appwrite/Auth/OAuth2/Exception.php new file mode 100644 index 0000000000..af98fa5e9d --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/Exception.php @@ -0,0 +1,51 @@ +response = $response; + $this->message = $response; + $decoded = json_decode($response, true); + if (\is_array($decoded)) { + $this->error = $decoded['error']; + $this->errorDescription = $decoded['error_description']; + $this->message = $this->error . ': ' . $this->errorDescription; + } + $type = match ($code) { + 400 => AppwriteException::USER_OAUTH2_BAD_REQUEST, + 401 => AppwriteException::USER_OAUTH2_UNAUTHORIZED, + default => AppwriteException::USER_OAUTH2_PROVIDER_ERROR + }; + + parent::__construct($type, $this->message, $code, $previous); + } + + /** + * Get the error parameter from the response. + * + * See https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 for more information. + */ + public function getError(): string + { + return $this->error; + } + + /** + * Get the error_description parameter from the response. + * + * See https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 for more information. + */ + public function getErrorDescription(): string + { + return $this->errorDescription; + } +} diff --git a/src/Appwrite/DSN/DSN.php b/src/Appwrite/DSN/DSN.php index 5605640989..de6f0df9e0 100644 --- a/src/Appwrite/DSN/DSN.php +++ b/src/Appwrite/DSN/DSN.php @@ -55,8 +55,8 @@ class DSN } $this->scheme = $parts['scheme'] ?? null; - $this->user = $parts['user'] ?? null; - $this->password = $parts['pass'] ?? null; + $this->user = isset($parts['user']) ? \urldecode($parts['user']) : null; + $this->password = isset($parts['pass']) ? \urldecode($parts['pass']) : null; $this->host = $parts['host'] ?? null; $this->port = $parts['port'] ?? null; $this->database = $parts['path'] ?? null; @@ -124,7 +124,7 @@ class DSN } /** - * Return the query string + * Return the raw query string * * @return ?string */ diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 606655e403..8ae268454f 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -76,6 +76,9 @@ class Exception extends \Exception public const USER_PHONE_ALREADY_EXISTS = 'user_phone_already_exists'; public const USER_PHONE_NOT_FOUND = 'user_phone_not_found'; public const USER_MISSING_ID = 'user_missing_id'; + public const USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request'; + public const USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized'; + public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error'; /** Teams */ public const TEAM_NOT_FOUND = 'team_not_found'; diff --git a/tests/unit/DSN/DSNTest.php b/tests/unit/DSN/DSNTest.php index d1f5ba1197..4ff4fb9bf0 100644 --- a/tests/unit/DSN/DSNTest.php +++ b/tests/unit/DSN/DSNTest.php @@ -71,6 +71,39 @@ class DSNTest extends TestCase $this->assertNull($dsn->getPort()); $this->assertEmpty($dsn->getDatabase()); $this->assertNull($dsn->getQuery()); + + $password = 'sl/sh+$@no:her'; + $encoded = \urlencode($password); + $dsn = new DSN("sms://user:$encoded@localhost"); + $this->assertEquals("sms", $dsn->getScheme()); + $this->assertEquals("user", $dsn->getUser()); + $this->assertEquals($password, $dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertNull($dsn->getPort()); + $this->assertEmpty($dsn->getDatabase()); + $this->assertNull($dsn->getQuery()); + + $user = 'admin@example.com'; + $encoded = \urlencode($user); + $dsn = new DSN("sms://$encoded@localhost"); + $this->assertEquals("sms", $dsn->getScheme()); + $this->assertEquals($user, $dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertNull($dsn->getPort()); + $this->assertEmpty($dsn->getDatabase()); + $this->assertNull($dsn->getQuery()); + + $value = 'I am 100% value=, "right"?!'; + $encoded = \urlencode($value); + $dsn = new DSN("sms://localhost?value=$encoded"); + $this->assertEquals("sms", $dsn->getScheme()); + $this->assertNull($dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertNull($dsn->getPort()); + $this->assertEmpty($dsn->getDatabase()); + $this->assertEquals("value=$encoded", $dsn->getQuery()); } public function testFail(): void