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', '')
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ print($title, self::FILTER_ESCAPE); ?>
+
+
+
+
+
+
Error print($code, self::FILTER_ESCAPE); ?>
+
print($message, self::FILTER_ESCAPE); ?>
+
+
Type
+
print($type, self::FILTER_ESCAPE); ?>
+
+
+
Error Trace
+
+
+
+
+
+
+
+
+
\ 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