1
0
Fork 0
mirror of synced 2024-06-13 16:24:47 +12:00

Update APIs to check X-Appwrite-Timestamp header

Appwrite will refer to the X-Appwrite-Timestamp header for when the
request was originally sent and reject the request if the Timestamp
is older than the updatedAt timestamp of the document.
This commit is contained in:
Steven Nguyen 2023-01-19 16:36:17 -08:00
parent d2c55afdcf
commit 85a2d623a6
No known key found for this signature in database
13 changed files with 201 additions and 83 deletions

View file

@ -3,11 +3,11 @@
## Features
- Password dictionary setting allows to compare user's password against command password database [4906](https://github.com/appwrite/appwrite/pull/4906)
- Password history setting allows to save user's last used password so that it may not be used again. Maximum number of history saved is 20, which can be configured. Minimum is 0 which means disabled. [#4866](https://github.com/appwrite/appwrite/pull/4866)
- Update APIs to check X-Appwrite-Timestamp header [#5024](https://github.com/appwrite/appwrite/pull/5024)
## Bugs
- Fix expire to formatTz in create account session [#4985](https://github.com/appwrite/appwrite/pull/4985)
- Fix not storing function's response on response codes 5xx [#4610](https://github.com/appwrite/appwrite/pull/4610)
- Fix expire to formatTz in create account session [#4985](https://github.com/appwrite/appwrite/pull/4985)
# Version 1.2.1
## Changes

View file

@ -408,6 +408,11 @@ return [
'description' => 'Document with the requested ID already exists.',
'code' => 409,
],
Exception::DOCUMENT_UPDATE_CONFLICT => [
'name' => Exception::DOCUMENT_UPDATE_CONFLICT,
'description' => 'Remote document is newer than local.',
'code' => 409,
],
/** Attributes */
Exception::ATTRIBUTE_NOT_FOUND => [

View file

@ -1524,15 +1524,18 @@ App::patch('/v1/account/name')
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (string $name, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (string $name, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$user = $dbForProject->updateDocument('users', $user->getId(), $user
$user
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email', ''), $user->getAttribute('phone', '')])));
->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email', ''), $user->getAttribute('phone', '')]));
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$events->setParam('userId', $user->getId());
@ -1559,12 +1562,13 @@ App::patch('/v1/account/password')
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->param('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true)
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $password, string $oldPassword, Response $response, Document $user, Document $project, Database $dbForProject, Event $events) {
->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $events) {
// Check old password only if its an existing user.
if (!empty($user->getAttribute('passwordUpdate')) && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
@ -1585,12 +1589,14 @@ App::patch('/v1/account/password')
array_slice($history, (count($history) - $historyLimit), $historyLimit);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now()))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
$user
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$events->setParam('userId', $user->getId());
@ -1617,11 +1623,12 @@ App::patch('/v1/account/email')
->label('sdk.offline.key', 'current')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
if (
@ -1642,7 +1649,7 @@ App::patch('/v1/account/email')
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $email, $user->getAttribute('phone', '')]));
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -1672,11 +1679,12 @@ App::patch('/v1/account/phone')
->label('sdk.offline.key', 'current')
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
@ -1693,7 +1701,7 @@ App::patch('/v1/account/phone')
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $phone]));
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS);
}
@ -1722,13 +1730,16 @@ App::patch('/v1/account/prefs')
->label('sdk.offline.model', '/account/prefs')
->label('sdk.offline.key', 'current')
->param('prefs', [], new Assoc(), 'Prefs key-value JSON object.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (array $prefs, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (array $prefs, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$user->setAttribute('prefs', $prefs);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$events->setParam('userId', $user->getId());
@ -1751,14 +1762,16 @@ App::patch('/v1/account/status')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->inject('request')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', false));
$user->setAttribute('status', false);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$events
->setParam('userId', $user->getId())
@ -1788,6 +1801,7 @@ App::delete('/v1/account/sessions/:sessionId')
->label('sdk.response.model', Response::MODEL_NONE)
->label('abuse-limit', 100)
->param('sessionId', '', new UID(), 'Session ID. Use the string \'current\' to delete the current device session.')
->inject('requestTimestamp')
->inject('request')
->inject('response')
->inject('user')
@ -1795,7 +1809,7 @@ App::delete('/v1/account/sessions/:sessionId')
->inject('locale')
->inject('events')
->inject('project')
->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events, Document $project) {
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events, Document $project) {
$protocol = $request->getProtocol();
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
@ -1807,9 +1821,11 @@ App::delete('/v1/account/sessions/:sessionId')
foreach ($sessions as $key => $session) {/** @var Document $session */
if ($sessionId == $session->getId()) {
unset($sessions[$key]);
$dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $session) {
return $dbForProject->deleteDocument('sessions', $session->getId());
});
$dbForProject->deleteDocument('sessions', $session->getId());
unset($sessions[$key]);
$session->setAttribute('current', false);

View file

@ -2252,11 +2252,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->param('documentId', '', new UID(), 'Document ID.')
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](/docs/permissions).', true)
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Event $events, string $mode) {
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $events, string $mode) {
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@ -2286,6 +2287,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
}
// Read permission should not be required for update
/** @var Document */
$document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
if ($document->isEmpty()) {
@ -2331,10 +2333,31 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$data['$permissions'] = $permissions;
try {
$privateCollectionId = 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId();
if ($documentSecurity && !$valid) {
$document = $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data));
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,
function () use ($dbForProject, $privateCollectionId, $document, $data) {
return $dbForProject->updateDocument(
$privateCollectionId,
$document->getId(),
new Document($data)
);
}
);
} else {
$document = Authorization::skip(fn() => $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data)));
$document = Authorization::skip(function () use ($dbForProject, $requestTimestamp, $privateCollectionId, $document, $data) {
return $dbForProject->withRequestTimestamp(
$requestTimestamp,
function () use ($dbForProject, $privateCollectionId, $document, $data) {
return $dbForProject->updateDocument(
$privateCollectionId,
$document->getId(),
new Document($data)
);
}
);
});
}
/**
@ -2385,12 +2408,13 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('documentId', '', new UID(), 'Document ID.')
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('deletes')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, Response $response, Database $dbForProject, Event $events, Delete $deletes, string $mode) {
->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $events, Delete $deletes, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -2420,17 +2444,25 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
$privateCollectionId = 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId();
if ($documentSecurity && !$valid) {
try {
$dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
$dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $privateCollectionId, $documentId) {
return $dbForProject->deleteDocument($privateCollectionId, $documentId);
});
} catch (AuthorizationException) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
} else {
Authorization::skip(fn() => $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
Authorization::skip(function () use ($dbForProject, $requestTimestamp, $privateCollectionId, $documentId) {
return $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $privateCollectionId, $documentId) {
return $dbForProject->deleteDocument($privateCollectionId, $documentId);
});
});
}
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
$dbForProject->deleteCachedDocument($privateCollectionId, $documentId);
/**
* Reset $collection attribute to remove prefix.

View file

@ -6,6 +6,7 @@ use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Phone as EventPhone;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
@ -21,7 +22,6 @@ use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@ -37,10 +37,7 @@ use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Appwrite\Event\Phone as EventPhone;
App::post('/v1/teams')
->desc('Create Team')
@ -219,10 +216,11 @@ App::put('/v1/teams/:teamId')
->label('sdk.offline.key', '{teamId}')
->param('teamId', '', new UID(), 'Team ID.')
->param('name', null, new Text(128), 'New team name. Max length: 128 chars.')
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('events')
->action(function (string $teamId, string $name, Response $response, Database $dbForProject, Event $events) {
->action(function (string $teamId, string $name, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $events) {
$team = $dbForProject->getDocument('teams', $teamId);
@ -230,9 +228,13 @@ App::put('/v1/teams/:teamId')
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$team = $dbForProject->updateDocument('teams', $team->getId(), $team
$team
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$teamId, $name])));
->setAttribute('search', implode(' ', [$teamId, $name]));
$team = $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $team) {
return $dbForProject->updateDocument('teams', $team->getId(), $team);
});
$events->setParam('teamId', $team->getId());

View file

@ -233,7 +233,7 @@ App::init()
->addHeader('Server', 'Appwrite')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma')
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $refDomain)
->addHeader('Access-Control-Allow-Credentials', 'true')
@ -384,7 +384,7 @@ App::options()
$response
->addHeader('Server', 'Appwrite')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies')
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
@ -485,6 +485,10 @@ App::error()
$error->setType(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
break;
}
} elseif ($error instanceof Utopia\Database\Exception\Conflict) {
$error = new AppwriteException(AppwriteException::DOCUMENT_UPDATE_CONFLICT, null, null, $error);
$code = $error->getCode();
$message = $error->getMessage();
}
/** Wrap all exceptions inside Appwrite\Extend\Exception */

View file

@ -5,7 +5,6 @@ use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Usage\Stats;
@ -16,7 +15,6 @@ use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;

View file

@ -1155,3 +1155,17 @@ App::setResource('schema', function ($utopia, $dbForProject) {
$params,
);
}, ['utopia', 'dbForProject']);
App::setResource('requestTimestamp', function ($request) {
// Validate x-appwrite-timestamp header
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
$requestTimestamp = null;
if (!empty($timestampHeader)) {
try {
$requestTimestamp = new \DateTime($timestampHeader);
} catch (\Throwable $e) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value');
}
}
return $requestTimestamp;
}, ['request']);

View file

@ -43,13 +43,13 @@
"ext-sockets": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-runtimes": "0.11.*",
"utopia-php/abuse": "0.18.*",
"utopia-php/abuse": "0.19.*",
"utopia-php/analytics": "0.2.*",
"utopia-php/audit": "0.20.*",
"utopia-php/audit": "0.21.*",
"utopia-php/cache": "0.8.*",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.30.*",
"utopia-php/database": "0.31.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/domains": "1.1.*",
"utopia-php/framework": "0.26.*",

74
composer.lock generated
View file

@ -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": "ac80cafdd8c2c6deaec3dfe628084655",
"content-hash": "3e00aa37bea907b7dcca7f912402a392",
"packages": [
{
"name": "adhocore/jwt",
@ -1808,29 +1808,28 @@
},
{
"name": "utopia-php/abuse",
"version": "0.18.0",
"version": "0.19.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "8496401234f73a49f8c4259d3e89ab4a7c1f9ecf"
"reference": "419b6e2e0a5dec35ea83a25758df9cd129b6c412"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/8496401234f73a49f8c4259d3e89ab4a7c1f9ecf",
"reference": "8496401234f73a49f8c4259d3e89ab4a7c1f9ecf",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/419b6e2e0a5dec35ea83a25758df9cd129b6c412",
"reference": "419b6e2e0a5dec35ea83a25758df9cd129b6c412",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.30.*"
"utopia-php/database": "0.31.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpstan/phpstan": "1.9.x-dev",
"phpunit/phpunit": "^9.4",
"vimeo/psalm": "4.0.1"
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^9.4"
},
"type": "library",
"autoload": {
@ -1852,9 +1851,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.18.0"
"source": "https://github.com/utopia-php/abuse/tree/0.19.0"
},
"time": "2023-02-14T09:56:04+00:00"
"time": "2023-02-26T03:28:48+00:00"
},
{
"name": "utopia-php/analytics",
@ -1913,22 +1912,22 @@
},
{
"name": "utopia-php/audit",
"version": "0.20.0",
"version": "0.21.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "3fce3f4ad3ea9dfcb39b79668abd76331412a5ed"
"reference": "7f9783a14718c82570c6effb35a3cb42c30b13a2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/3fce3f4ad3ea9dfcb39b79668abd76331412a5ed",
"reference": "3fce3f4ad3ea9dfcb39b79668abd76331412a5ed",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/7f9783a14718c82570c6effb35a3cb42c30b13a2",
"reference": "7f9783a14718c82570c6effb35a3cb42c30b13a2",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.30.*"
"utopia-php/database": "0.31.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@ -1956,9 +1955,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.20.0"
"source": "https://github.com/utopia-php/audit/tree/0.21.0"
},
"time": "2023-02-14T09:46:54+00:00"
"time": "2023-02-26T03:28:30+00:00"
},
{
"name": "utopia-php/cache",
@ -2115,16 +2114,16 @@
},
{
"name": "utopia-php/database",
"version": "0.30.1",
"version": "0.31.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "1cea72c1217357bf0747ae4f28ebef57e9dc0e65"
"reference": "61f9f4743a317f1d78558a5f981adf74e7fdc931"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/1cea72c1217357bf0747ae4f28ebef57e9dc0e65",
"reference": "1cea72c1217357bf0747ae4f28ebef57e9dc0e65",
"url": "https://api.github.com/repos/utopia-php/database/zipball/61f9f4743a317f1d78558a5f981adf74e7fdc931",
"reference": "61f9f4743a317f1d78558a5f981adf74e7fdc931",
"shasum": ""
},
"require": {
@ -2163,9 +2162,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.30.1"
"source": "https://github.com/utopia-php/database/tree/0.31.0"
},
"time": "2023-02-14T06:25:03+00:00"
"time": "2023-02-23T09:49:44+00:00"
},
{
"name": "utopia-php/domains",
@ -3742,23 +3741,23 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.24",
"version": "9.2.25",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2cf940ebc6355a9d430462811b5aaa308b174bed"
"reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2cf940ebc6355a9d430462811b5aaa308b174bed",
"reference": "2cf940ebc6355a9d430462811b5aaa308b174bed",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0e2b40518197a8c0d4b08bc34dfff1c99c508954",
"reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.14",
"nikic/php-parser": "^4.15",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@ -3807,7 +3806,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.24"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.25"
},
"funding": [
{
@ -3815,7 +3814,7 @@
"type": "github"
}
],
"time": "2023-01-26T08:26:55+00:00"
"time": "2023-02-25T05:32:00+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -5127,16 +5126,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.7.1",
"version": "3.7.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
"shasum": ""
},
"require": {
@ -5172,14 +5171,15 @@
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
"standards",
"static analysis"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2022-06-18T07:21:10+00:00"
"time": "2023-02-22T23:07:41+00:00"
},
{
"name": "swoole/ide-helper",

View file

@ -135,6 +135,7 @@ class Exception extends \Exception
public const DOCUMENT_INVALID_STRUCTURE = 'document_invalid_structure';
public const DOCUMENT_MISSING_PAYLOAD = 'document_missing_payload';
public const DOCUMENT_ALREADY_EXISTS = 'document_already_exists';
public const DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict';
/** Attribute */
public const ATTRIBUTE_NOT_FOUND = 'attribute_not_found';

View file

@ -25,7 +25,7 @@ class HTTPTest extends Scope
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals('Appwrite', $response['headers']['server']);
$this->assertEquals('GET, POST, PUT, PATCH, DELETE', $response['headers']['access-control-allow-methods']);
$this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies', $response['headers']['access-control-allow-headers']);
$this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies', $response['headers']['access-control-allow-headers']);
$this->assertEquals('X-Fallback-Cookies', $response['headers']['access-control-expose-headers']);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
$this->assertEquals('true', $response['headers']['access-control-allow-credentials']);

View file

@ -2,7 +2,9 @@
namespace Tests\E2E\Services\Databases;
use Appwrite\Extend\Exception;
use Tests\E2E\Client;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@ -1576,6 +1578,50 @@ trait DatabasesBase
$this->assertEquals($document['body']['title'], 'Thor: Ragnarok');
$this->assertEquals($document['body']['releaseYear'], 2017);
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-timestamp' => DateTime::formatTz(DateTime::now()),
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
/**
* Test for failure
*/
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-timestamp' => 'invalid',
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
],
]);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals('Invalid X-Appwrite-Timestamp header value', $response['body']['message']);
$this->assertEquals(Exception::GENERAL_ARGUMENT_INVALID, $response['body']['type']);
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-timestamp' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -1000)),
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
],
]);
$this->assertEquals(409, $response['headers']['status-code']);
$this->assertEquals('Remote document is newer than local.', $response['body']['message']);
$this->assertEquals(Exception::DOCUMENT_UPDATE_CONFLICT, $response['body']['type']);
return [];
}