mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.9.x' into feat-audit-user-type-distinction
This commit is contained in:
+5
-1
@@ -329,17 +329,20 @@ $setResource('bus', function (Registry $register) use ($cli) {
|
||||
|
||||
$setResource('telemetry', fn () => new NoTelemetry(), []);
|
||||
|
||||
$exitCode = 0;
|
||||
|
||||
$cli
|
||||
->error()
|
||||
->inject('error')
|
||||
->inject('logError')
|
||||
->action(function (Throwable $error, callable $logError) use ($taskName) {
|
||||
->action(function (Throwable $error, callable $logError) use ($taskName, &$exitCode) {
|
||||
call_user_func_array($logError, [
|
||||
$error,
|
||||
'Task',
|
||||
$taskName,
|
||||
]);
|
||||
|
||||
$exitCode = 1;
|
||||
Timer::clearAll();
|
||||
});
|
||||
|
||||
@@ -348,3 +351,4 @@ $cli->shutdown()->action(fn () => Timer::clearAll());
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
run($cli->run(...));
|
||||
Console::exit($exitCode);
|
||||
|
||||
@@ -207,7 +207,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
|
||||
}
|
||||
|
||||
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
|
||||
|
||||
// Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
|
||||
$oauthProvider = null;
|
||||
@@ -345,7 +345,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
->setProperty('secret', $sessionSecret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
@@ -353,8 +353,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
|
||||
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
|
||||
@@ -719,7 +719,9 @@ Http::delete('/v1/account/sessions')
|
||||
->inject('queueForDeletes')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
@@ -728,7 +730,7 @@ Http::delete('/v1/account/sessions')
|
||||
foreach ($sessions as $session) {/** @var Document $session */
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
|
||||
}
|
||||
|
||||
@@ -741,8 +743,8 @@ Http::delete('/v1/account/sessions')
|
||||
|
||||
// If current session delete the cookies too
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
|
||||
// Use current session for events.
|
||||
$currentSession = $session;
|
||||
@@ -849,7 +851,9 @@ Http::delete('/v1/account/sessions/:sessionId')
|
||||
->inject('queueForDeletes')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$sessionId = ($sessionId === 'current')
|
||||
@@ -875,13 +879,13 @@ Http::delete('/v1/account/sessions/:sessionId')
|
||||
->setAttribute('current', true)
|
||||
->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')));
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
|
||||
}
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
@@ -1035,8 +1039,10 @@ Http::post('/v1/account/sessions/email')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
|
||||
$email = \strtolower($email);
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
@@ -1110,15 +1116,15 @@ Http::post('/v1/account/sessions/email')
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
;
|
||||
|
||||
@@ -1184,8 +1190,10 @@ Http::post('/v1/account/sessions/anonymous')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -1276,15 +1284,15 @@ Http::post('/v1/account/sessions/anonymous')
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
;
|
||||
|
||||
@@ -1339,7 +1347,9 @@ Http::post('/v1/account/sessions/token')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('authorization')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action($createSession);
|
||||
|
||||
Http::get('/v1/account/sessions/oauth2/:provider')
|
||||
@@ -1538,8 +1548,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('plan')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
@@ -2055,7 +2067,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
@@ -2068,14 +2080,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
// TODO: Remove this deprecated workaround - support only token
|
||||
if ($state['success']['path'] == $oauthDefaultSuccess) {
|
||||
$query['project'] = $project->getId();
|
||||
$query['domain'] = Config::getParam('cookieDomain');
|
||||
$query['domain'] = $cookieDomain;
|
||||
$query['key'] = $store->getKey();
|
||||
$query['secret'] = $encoded;
|
||||
}
|
||||
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
}
|
||||
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
|
||||
@@ -2886,11 +2898,13 @@ Http::put('/v1/account/sessions/magic-url')
|
||||
->inject('queueForMails')
|
||||
->inject('store')
|
||||
->inject('proofForCode')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) {
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $domainVerification, $cookieDomain, $authorization) use ($createSession) {
|
||||
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
|
||||
$proofForToken->setHash(new Sha());
|
||||
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization);
|
||||
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $domainVerification, $cookieDomain, $authorization);
|
||||
});
|
||||
|
||||
Http::put('/v1/account/sessions/phone')
|
||||
@@ -2936,6 +2950,8 @@ Http::put('/v1/account/sessions/phone')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->inject('authorization')
|
||||
->action($createSession);
|
||||
|
||||
@@ -3727,7 +3743,9 @@ Http::patch('/v1/account/status')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store) {
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, bool $domainVerification, ?string $cookieDomain) {
|
||||
|
||||
$user->setAttribute('status', false);
|
||||
|
||||
@@ -3737,14 +3755,14 @@ Http::patch('/v1/account/status')
|
||||
->setParam('userId', $user->getId())
|
||||
->setPayload($response->output($user, Response::MODEL_ACCOUNT));
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
|
||||
}
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$response
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
;
|
||||
|
||||
$response->dynamic($user, Response::MODEL_ACCOUNT);
|
||||
@@ -3937,7 +3955,7 @@ Http::post('/v1/account/recovery')
|
||||
->setParam('userId', $profile->getId())
|
||||
->setParam('tokenId', $recovery->getId())
|
||||
->setUser($profile)
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -4038,7 +4056,7 @@ Http::put('/v1/account/recovery')
|
||||
$queueForEvents
|
||||
->setParam('userId', $profile->getId())
|
||||
->setParam('tokenId', $recoveryDocument->getId())
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response->dynamic($recoveryDocument, Response::MODEL_TOKEN);
|
||||
});
|
||||
@@ -4268,7 +4286,7 @@ Http::post('/v1/account/verifications/email')
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('tokenId', $verification->getId())
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -4360,7 +4378,7 @@ Http::put('/v1/account/verifications/email')
|
||||
$queueForEvents
|
||||
->setParam('userId', $userId)
|
||||
->setParam('tokenId', $verification->getId())
|
||||
->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
|
||||
|
||||
$response->dynamic($verification, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
+17
-30
@@ -61,8 +61,6 @@ use Utopia\System\System;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
@@ -868,7 +866,7 @@ Http::init()
|
||||
* Request format
|
||||
*/
|
||||
$route = $utopia->getRoute();
|
||||
Request::setRoute($route);
|
||||
$request->setRoute($route);
|
||||
|
||||
if ($route === null) {
|
||||
$response->setStatusCode(404);
|
||||
@@ -904,40 +902,16 @@ Http::init()
|
||||
$locale->setDefault($localeParam);
|
||||
}
|
||||
|
||||
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
|
||||
$selfDomain = new Domain($request->getHostname());
|
||||
$endDomain = new Domain((string)$origin);
|
||||
Config::setParam(
|
||||
'domainVerification',
|
||||
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
|
||||
$endDomain->getRegisterable() !== ''
|
||||
);
|
||||
|
||||
$localHosts = ['localhost','localhost:'.$request->getPort()];
|
||||
|
||||
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
|
||||
if (!empty($migrationHost)) {
|
||||
// Treat the migration host like localhost because internal migration and
|
||||
// CI traffic may use it before a public domain is configured.
|
||||
$localHosts[] = $migrationHost;
|
||||
$localHosts[] = $migrationHost.':'.$request->getPort();
|
||||
}
|
||||
|
||||
$isLocalHost = in_array($request->getHostname(), $localHosts);
|
||||
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
|
||||
|
||||
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
|
||||
$isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
|
||||
|
||||
Config::setParam(
|
||||
'cookieDomain',
|
||||
$isLocalHost || $isIpAddress
|
||||
? null
|
||||
: (
|
||||
$isConsoleProject && $isConsoleRootSession
|
||||
? '.' . $selfDomain->getRegisterable()
|
||||
: '.' . $request->getHostname()
|
||||
)
|
||||
);
|
||||
|
||||
$warnings = [];
|
||||
|
||||
/*
|
||||
@@ -1019,7 +993,7 @@ Http::init()
|
||||
return;
|
||||
}
|
||||
$route = $request->getRoute();
|
||||
if ($route->getLabel('origin', false) === '*') {
|
||||
if ($route?->getLabel('origin', false) === '*') {
|
||||
return;
|
||||
}
|
||||
if (!$originValidator->isValid($origin)) {
|
||||
@@ -1493,6 +1467,19 @@ Http::error()
|
||||
'type' => $type,
|
||||
];
|
||||
|
||||
// Add CORS headers to error responses so browsers can read the error.
|
||||
// Wrapped in try-catch: if the error itself is a DB failure, resolving
|
||||
// the cors resource (which depends on rule -> DB) would cascade.
|
||||
// Uses override:true to avoid duplicate headers if init() already set them.
|
||||
try {
|
||||
$cors = $utopia->getResource('cors');
|
||||
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
|
||||
$response->addHeader($name, $value, override: true);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// Degrade gracefully - error response without CORS is no worse than before.
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
|
||||
@@ -53,6 +53,7 @@ use Utopia\Database\DateTime as DatabaseDateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
@@ -249,6 +250,52 @@ Http::setResource('allowedSchemes', function (array $platform, Document $project
|
||||
return array_unique($allowed);
|
||||
}, ['platform', 'project']);
|
||||
|
||||
/**
|
||||
* Whether the request origin is verified against the request hostname.
|
||||
*/
|
||||
Http::setResource('domainVerification', function (Request $request) {
|
||||
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
|
||||
$selfDomain = new Domain($request->getHostname());
|
||||
$endDomain = new Domain((string) $origin);
|
||||
|
||||
return ($selfDomain->getRegisterable() === $endDomain->getRegisterable())
|
||||
&& $endDomain->getRegisterable() !== '';
|
||||
}, ['request']);
|
||||
|
||||
/**
|
||||
* Cookie domain for the current request.
|
||||
*/
|
||||
Http::setResource('cookieDomain', function (Request $request, Document $project) {
|
||||
$localHosts = ['localhost', 'localhost:' . $request->getPort()];
|
||||
|
||||
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
|
||||
if (!empty($migrationHost)) {
|
||||
// Treat the migration host like localhost because internal migration and CI
|
||||
// traffic may use it before a public domain is configured.
|
||||
$localHosts[] = $migrationHost;
|
||||
$localHosts[] = $migrationHost . ':' . $request->getPort();
|
||||
}
|
||||
|
||||
$hostname = $request->getHostname();
|
||||
$isLocalHost = \in_array($hostname, $localHosts, true);
|
||||
$isIpAddress = \filter_var($hostname, FILTER_VALIDATE_IP) !== false;
|
||||
|
||||
if ($isLocalHost || $isIpAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
|
||||
$isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
|
||||
|
||||
if ($isConsoleProject && $isConsoleRootSession) {
|
||||
$domain = new Domain($hostname);
|
||||
|
||||
return '.' . $domain->getRegisterable();
|
||||
}
|
||||
|
||||
return '.' . $hostname;
|
||||
}, ['request', 'project']);
|
||||
|
||||
/**
|
||||
* Rule associated with a request origin.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Export documents to a JSON file from your Appwrite database. This endpoint allows you to export documents to a JSON file stored in a secure internal bucket. You'll receive an email with a download link when the export is complete.
|
||||
@@ -0,0 +1 @@
|
||||
Import documents from a JSON file into your Appwrite database. This endpoint allows you to import documents from a JSON file uploaded to Appwrite Storage bucket.
|
||||
@@ -130,10 +130,25 @@ class Create extends CollectionAction
|
||||
$indexes[] = new Document($index);
|
||||
}
|
||||
try {
|
||||
if (!$dbForDatabases->exists(null, Database::METADATA)) {
|
||||
// Bootstrap the database metadata without a separate existence
|
||||
// check to avoid races when multiple first collections are created
|
||||
// concurrently for the same VectorsDB database.
|
||||
for ($attempt = 0; $attempt < 5; $attempt++) {
|
||||
try {
|
||||
$dbForDatabases->create();
|
||||
break;
|
||||
} catch (DuplicateException) {
|
||||
break;
|
||||
} catch (\Throwable $e) {
|
||||
if ($dbForDatabases->exists(null, Database::METADATA)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($attempt === 4) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
\usleep(100_000);
|
||||
}
|
||||
}
|
||||
$dbForDatabases->createCollection(
|
||||
|
||||
@@ -74,10 +74,12 @@ class Update extends Action
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('domainVerification')
|
||||
->inject('cookieDomain')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken)
|
||||
public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, bool $domainVerification, ?string $cookieDomain)
|
||||
{
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
@@ -162,7 +164,7 @@ class Update extends Action
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
if (!$domainVerification) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
}
|
||||
|
||||
@@ -172,7 +174,7 @@ class Update extends Action
|
||||
value: $encoded,
|
||||
expire: (new \DateTime($expire))->getTimestamp(),
|
||||
path: '/',
|
||||
domain: Config::getParam('cookieDomain'),
|
||||
domain: $cookieDomain,
|
||||
secure: ('https' === $protocol),
|
||||
httponly: true
|
||||
)
|
||||
@@ -181,7 +183,7 @@ class Update extends Action
|
||||
value: $encoded,
|
||||
expire: (new \DateTime($expire))->getTimestamp(),
|
||||
path: '/',
|
||||
domain: Config::getParam('cookieDomain'),
|
||||
domain: $cookieDomain,
|
||||
secure: ('https' === $protocol),
|
||||
httponly: true,
|
||||
sameSite: Config::getParam('cookieSamesite')
|
||||
|
||||
@@ -210,6 +210,25 @@ abstract class Format
|
||||
return $this->services;
|
||||
}
|
||||
|
||||
protected function getDescriptionContents(?string $description): string
|
||||
{
|
||||
if ($description === null || $description === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!\str_ends_with($description, '.md')) {
|
||||
return $description;
|
||||
}
|
||||
|
||||
$contents = @\file_get_contents($description);
|
||||
|
||||
if ($contents === false) {
|
||||
throw new \RuntimeException('Documentation file not found or unreadable: ' . $description);
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
protected function getRequestEnumName(string $service, string $method, string $param): ?string
|
||||
{
|
||||
/* `$service` is `$namespace` */
|
||||
|
||||
@@ -125,10 +125,7 @@ class OpenAPI3 extends Format
|
||||
|
||||
$namespace = $sdk->getNamespace() ?? 'default';
|
||||
|
||||
if ($desc === null) {
|
||||
$desc = '';
|
||||
}
|
||||
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
|
||||
$descContents = $this->getDescriptionContents($desc);
|
||||
|
||||
$temp = [
|
||||
'summary' => $route->getDesc(),
|
||||
@@ -193,7 +190,7 @@ class OpenAPI3 extends Format
|
||||
'parameters' => [],
|
||||
'required' => [],
|
||||
'responses' => [],
|
||||
'description' => ($desc) ? \file_get_contents($desc) : '',
|
||||
'description' => $this->getDescriptionContents($desc),
|
||||
'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md',
|
||||
'public' => $methodObj->isPublic(),
|
||||
];
|
||||
|
||||
@@ -126,10 +126,7 @@ class Swagger2 extends Format
|
||||
$sdkPlatforms = array_values(array_unique($sdkPlatforms));
|
||||
$namespace = $sdk->getNamespace() ?? 'default';
|
||||
|
||||
if ($desc === null) {
|
||||
$desc = '';
|
||||
}
|
||||
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
|
||||
$descContents = $this->getDescriptionContents($desc);
|
||||
|
||||
$temp = [
|
||||
'summary' => $route->getDesc(),
|
||||
@@ -201,7 +198,7 @@ class Swagger2 extends Format
|
||||
'parameters' => [],
|
||||
'required' => [],
|
||||
'responses' => [],
|
||||
'description' => ($desc) ? \file_get_contents($desc) : '',
|
||||
'description' => $this->getDescriptionContents($desc),
|
||||
'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md',
|
||||
'public' => $methodObj->isPublic(),
|
||||
];
|
||||
|
||||
@@ -17,7 +17,7 @@ class Request extends UtopiaRequest
|
||||
* @var array<Filter>
|
||||
*/
|
||||
private array $filters = [];
|
||||
private static ?Route $route = null;
|
||||
private ?Route $route = null;
|
||||
|
||||
public function __construct(SwooleRequest $request)
|
||||
{
|
||||
@@ -34,11 +34,11 @@ class Request extends UtopiaRequest
|
||||
{
|
||||
$parameters = parent::getParams();
|
||||
|
||||
if (!$this->hasFilters() || !self::hasRoute()) {
|
||||
if (!$this->hasFilters() || !$this->hasRoute()) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
$methods = self::getRoute()->getLabel('sdk', null);
|
||||
$methods = $this->getRoute()?->getLabel('sdk', null);
|
||||
|
||||
if (empty($methods)) {
|
||||
return $parameters;
|
||||
@@ -131,9 +131,9 @@ class Request extends UtopiaRequest
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function setRoute(?Route $route): void
|
||||
public function setRoute(?Route $route): void
|
||||
{
|
||||
self::$route = $route;
|
||||
$this->route = $route;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,9 +141,9 @@ class Request extends UtopiaRequest
|
||||
*
|
||||
* @return Route|null
|
||||
*/
|
||||
public static function getRoute(): ?Route
|
||||
public function getRoute(): ?Route
|
||||
{
|
||||
return self::$route;
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,9 +151,9 @@ class Request extends UtopiaRequest
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasRoute(): bool
|
||||
public function hasRoute(): bool
|
||||
{
|
||||
return self::$route !== null;
|
||||
return $this->route !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -299,7 +299,7 @@ class Response extends SwooleResponse
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected static bool $showSensitive = false;
|
||||
protected bool $showSensitive = false;
|
||||
|
||||
/**
|
||||
* @var array<string, Model>
|
||||
@@ -509,7 +509,7 @@ class Response extends SwooleResponse
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
if ((!$isPrivilegedUser && !$isAppUser) && !self::$showSensitive) {
|
||||
if ((!$isPrivilegedUser && !$isAppUser) && !$this->showSensitive) {
|
||||
$data->setAttribute($key, '');
|
||||
}
|
||||
}
|
||||
@@ -659,18 +659,20 @@ class Response extends SwooleResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Static wrapper to show sensitive data in response
|
||||
* Wrapper to show sensitive data in response
|
||||
*
|
||||
* @param callable(): array $callback The callback to show sensitive information for
|
||||
* @return array
|
||||
*/
|
||||
public static function showSensitive(callable $callback): array
|
||||
public function showSensitive(callable $callback): array
|
||||
{
|
||||
$previous = $this->showSensitive;
|
||||
|
||||
try {
|
||||
self::$showSensitive = true;
|
||||
$this->showSensitive = true;
|
||||
return $callback();
|
||||
} finally {
|
||||
self::$showSensitive = false;
|
||||
$this->showSensitive = $previous;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,24 @@ trait FunctionsBase
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]));
|
||||
$this->assertNotEquals(401, $function['headers']['status-code'], 'Auth failed while polling function activation');
|
||||
|
||||
if (
|
||||
($function['body']['deploymentId'] ?? '') !== $deploymentId
|
||||
&& ($function['body']['latestDeploymentId'] ?? '') === $deploymentId
|
||||
&& ($function['body']['latestDeploymentStatus'] ?? '') === 'ready'
|
||||
) {
|
||||
$activation = $this->updateFunctionDeployment($functionId, $deploymentId);
|
||||
$this->assertContains(
|
||||
$activation['headers']['status-code'],
|
||||
[200, 409],
|
||||
'Deployment activation request failed: ' . json_encode($activation['body'], JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
if ($activation['headers']['status-code'] === 200) {
|
||||
$function = $activation;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertEquals($deploymentId, $function['body']['deploymentId'] ?? '', 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
|
||||
}, 120000, 500);
|
||||
}
|
||||
|
||||
@@ -147,6 +147,21 @@ class RequestTest extends TestCase
|
||||
$this->assertSame('unexpected', $params['extra']);
|
||||
}
|
||||
|
||||
public function testRouteIsScopedToRequestInstance(): void
|
||||
{
|
||||
$firstRequest = new Request(new SwooleRequest());
|
||||
$secondRequest = new Request(new SwooleRequest());
|
||||
|
||||
$firstRoute = new Route(Request::METHOD_GET, '/first');
|
||||
$secondRoute = new Route(Request::METHOD_GET, '/second');
|
||||
|
||||
$firstRequest->setRoute($firstRoute);
|
||||
$secondRequest->setRoute($secondRoute);
|
||||
|
||||
$this->assertSame($firstRoute, $firstRequest->getRoute());
|
||||
$this->assertSame($secondRoute, $secondRequest->getRoute());
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to attach a route with multiple SDK methods to the request.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Tests\Unit\Utopia;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionProperty;
|
||||
use Swoole\Http\Response as SwooleResponse;
|
||||
use Tests\Unit\Utopia\Response\Filters\First;
|
||||
use Tests\Unit\Utopia\Response\Filters\Second;
|
||||
@@ -176,4 +177,26 @@ class ResponseTest extends TestCase
|
||||
$this->assertArrayHasKey('required', $single);
|
||||
$this->assertArrayNotHasKey('hidden', $singleFromArray);
|
||||
}
|
||||
|
||||
public function testShowSensitiveRestoresPreviousState(): void
|
||||
{
|
||||
$isShowingSensitive = new ReflectionProperty(Response::class, 'showSensitive');
|
||||
|
||||
$this->assertFalse($isShowingSensitive->getValue($this->response));
|
||||
|
||||
$payload = $this->response->showSensitive(function () use ($isShowingSensitive) {
|
||||
return [
|
||||
'outer' => $isShowingSensitive->getValue($this->response),
|
||||
'inner' => $this->response->showSensitive(fn () => [
|
||||
'state' => $isShowingSensitive->getValue($this->response),
|
||||
]),
|
||||
'afterInner' => $isShowingSensitive->getValue($this->response),
|
||||
];
|
||||
});
|
||||
|
||||
$this->assertTrue($payload['outer']);
|
||||
$this->assertTrue($payload['inner']['state']);
|
||||
$this->assertTrue($payload['afterInner']);
|
||||
$this->assertFalse($isShowingSensitive->getValue($this->response));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user