diff --git a/.env b/.env index feeb74d849..ad551e705a 100644 --- a/.env +++ b/.env @@ -91,7 +91,7 @@ _APP_GRAPHQL_MAX_DEPTH=3 _APP_DOCKER_HUB_USERNAME= _APP_DOCKER_HUB_PASSWORD= _APP_VCS_GITHUB_APP_NAME= -_APP_VCS_GITHUB_PRIVATE_KEY="" +_APP_VCS_GITHUB_PRIVATE_KEY=disabled _APP_VCS_GITHUB_APP_ID= _APP_VCS_GITHUB_CLIENT_ID= _APP_VCS_GITHUB_CLIENT_SECRET= diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ed9d7d1f19..842d61ff1c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -98,6 +98,7 @@ jobs: Teams, Users, Webhooks, + VCS, ] steps: diff --git a/.gitmodules b/.gitmodules index 6570e2a620..fc1a92efee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "app/console"] path = app/console url = https://github.com/appwrite/console - branch = 3.2.4 + branch = 3.2.5 diff --git a/CHANGES.md b/CHANGES.md index ac405184df..32ecd4f277 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,29 @@ +# Version 1.4.8 + +## Notable changes + +* Fix certificate emails and add support for variables in email template subject in [#6495](https://github.com/appwrite/appwrite/)pull/6495 +* Bump console to version 3.2.5 in [#7027](https://github.com/appwrite/appwrite/pull/7027) +* Bump utopia database and storage versions in [#7002](https://github.com/appwrite/appwrite/pull/7002) + +## Bug fixes + +* Fixes cookie headers not being passed properly by router in [#7024](https://github.com/appwrite/appwrite/pull/7024) +* Fix permission problem in deletes worker in [#7013](https://github.com/appwrite/appwrite/pull/7013) + +## Miscellaneous + +* Improve error handling in the realtime service in [#6998](https://github.com/appwrite/appwrite/pull/6998) +* Update the error code for unsupported protocol in [#7006](https://github.com/appwrite/appwrite/pull/7006) +* Improve CI tests by executing them in parallel in [#6198](https://github.com/appwrite/appwrite/pull/6198) +* Update README.md to add links to orchestration tools in [#7011](https://github.com/appwrite/appwrite/pull/7011) +* Update gitpod setup to install instead of update dependencies in [#6938](https://github.com/appwrite/appwrite/pull/6938) +* Remove analytics from install script in [#7017](https://github.com/appwrite/appwrite/pull/7017) +* Improve database logging in [#7003](https://github.com/appwrite/appwrite/pull/7003) +* Add VCS tests in [#6894](https://github.com/appwrite/appwrite/pull/6894) +* Improve error messages in [#6487](https://github.com/appwrite/appwrite/pull/6487) +* Add command to delete orphaned projects in [#7015](https://github.com/appwrite/appwrite/pull/7015) + # Version 1.4.7 ## Fixes diff --git a/Dockerfile b/Dockerfile index 8cea481543..33b1434659 100755 --- a/Dockerfile +++ b/Dockerfile @@ -101,6 +101,7 @@ RUN chmod +x /usr/local/bin/hamster && \ chmod +x /usr/local/bin/volume-sync && \ chmod +x /usr/local/bin/patch-delete-schedule-updated-at-attribute && \ chmod +x /usr/local/bin/patch-delete-project-collections && \ + chmod +x /usr/local/bin/delete-orphaned-projects && \ chmod +x /usr/local/bin/clear-card-cache && \ chmod +x /usr/local/bin/calc-users-stats && \ chmod +x /usr/local/bin/calc-tier-stats diff --git a/README-CN.md b/README-CN.md index 1df799f7ef..3881a8ac82 100644 --- a/README-CN.md +++ b/README-CN.md @@ -66,7 +66,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.4.7 + appwrite/appwrite:1.4.8 ``` ### Windows @@ -78,7 +78,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.4.7 + appwrite/appwrite:1.4.8 ``` #### PowerShell @@ -88,7 +88,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.4.7 + appwrite/appwrite:1.4.8 ``` 运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。 diff --git a/README.md b/README.md index 6503f6addf..9adc815bdd 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.4.7 + appwrite/appwrite:1.4.8 ``` ### Windows @@ -88,7 +88,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.4.7 + appwrite/appwrite:1.4.8 ``` #### PowerShell @@ -98,7 +98,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.4.7 + appwrite/appwrite:1.4.8 ``` Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation. diff --git a/app/cli.php b/app/cli.php index 5b5ae47bf6..643a615c46 100644 --- a/app/cli.php +++ b/app/cli.php @@ -61,7 +61,11 @@ CLI::setResource('dbForConsole', function ($pools, $cache) { ->getResource(); $dbForConsole = new Database($dbAdapter, $cache); - $dbForConsole->setNamespace('_console'); + + $dbForConsole + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console'); // Ensure tables exist $collections = Config::getParam('collections', [])['console']; @@ -111,7 +115,10 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $databases[$databaseName] = $database; - $database->setNamespace('_' . $project->getInternalId()); + $database + ->setNamespace('_' . $project->getInternalId()) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()); return $database; }; diff --git a/app/config/errors.php b/app/config/errors.php index 28fb69d118..ed1d97115c 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -355,7 +355,7 @@ return [ ], Exception::STORAGE_BUCKET_ALREADY_EXISTS => [ 'name' => Exception::STORAGE_BUCKET_ALREADY_EXISTS, - 'description' => 'A storage bucket with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.', + 'description' => 'A storage bucket with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.', 'code' => 409, ], Exception::STORAGE_BUCKET_NOT_FOUND => [ @@ -479,7 +479,7 @@ return [ ], Exception::COLLECTION_ALREADY_EXISTS => [ 'name' => Exception::COLLECTION_ALREADY_EXISTS, - 'description' => 'A collection with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.', + 'description' => 'A collection with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.', 'code' => 409, ], Exception::COLLECTION_LIMIT_EXCEEDED => [ @@ -511,7 +511,7 @@ return [ ], Exception::DOCUMENT_ALREADY_EXISTS => [ 'name' => Exception::DOCUMENT_ALREADY_EXISTS, - 'description' => 'Document with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.', + 'description' => 'Document with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.', 'code' => 409, ], Exception::DOCUMENT_UPDATE_CONFLICT => [ @@ -553,7 +553,7 @@ return [ ], Exception::ATTRIBUTE_ALREADY_EXISTS => [ 'name' => Exception::ATTRIBUTE_ALREADY_EXISTS, - 'description' => 'Attribute with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.', + 'description' => 'Attribute with the requested key already exists. Attribute keys must be unique, try again with a different key.', 'code' => 409, ], Exception::ATTRIBUTE_LIMIT_EXCEEDED => [ @@ -585,7 +585,7 @@ return [ ], Exception::INDEX_ALREADY_EXISTS => [ 'name' => Exception::INDEX_ALREADY_EXISTS, - 'description' => 'Index with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.', + 'description' => 'Index with the requested key already exists. Try again with a different key.', 'code' => 409, ], Exception::INDEX_INVALID => [ @@ -602,7 +602,7 @@ return [ ], Exception::PROJECT_ALREADY_EXISTS => [ 'name' => Exception::PROJECT_ALREADY_EXISTS, - 'description' => 'Project with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.', + 'description' => 'Project with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.', 'code' => 409, ], Exception::PROJECT_UNKNOWN => [ diff --git a/app/console b/app/console index fe835e5032..9810ce8581 160000 --- a/app/console +++ b/app/console @@ -1 +1 @@ -Subproject commit fe835e50328ed80f67c66d2d449c0f7b51ade544 +Subproject commit 9810ce85812ca26c95b7d35196848c92e8ba813d diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index a20524806b..0affc8fa9f 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1025,7 +1025,12 @@ App::post('/v1/account/sessions/magic-url') $customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? []; $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); - $message->setParam('{{body}}', $body); + $message + ->setParam('{{body}}', $body) + ->setParam('{{hello}}', $locale->getText("emails.magicSession.hello")) + ->setParam('{{footer}}', $locale->getText("emails.magicSession.footer")) + ->setParam('{{thanks}}', $locale->getText("emails.magicSession.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.magicSession.signature")); $body = $message->render(); $smtp = $project->getAttribute('smtp', []); @@ -1075,16 +1080,7 @@ App::post('/v1/account/sessions/magic-url') } $emailVariables = [ - 'subject' => $subject, - 'hello' => $locale->getText("emails.magicSession.hello"), - 'body' => $body, - 'footer' => $locale->getText("emails.magicSession.footer"), - 'thanks' => $locale->getText("emails.magicSession.thanks"), - 'signature' => $locale->getText("emails.magicSession.signature"), 'direction' => $locale->getText('settings.direction'), - 'bg-body' => '#f7f7f7', - 'bg-content' => '#ffffff', - 'text-content' => '#000000', /* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */ 'user' => '', 'team' => '', @@ -2374,7 +2370,12 @@ App::post('/v1/account/recovery') $customTemplate = $project->getAttribute('templates', [])['email.recovery-' . $locale->default] ?? []; $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); - $message->setParam('{{body}}', $body); + $message + ->setParam('{{body}}', $body) + ->setParam('{{hello}}', $locale->getText("emails.recovery.hello")) + ->setParam('{{footer}}', $locale->getText("emails.recovery.footer")) + ->setParam('{{thanks}}', $locale->getText("emails.recovery.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.recovery.signature")); $body = $message->render(); $smtp = $project->getAttribute('smtp', []); @@ -2424,16 +2425,7 @@ App::post('/v1/account/recovery') } $emailVariables = [ - 'subject' => $subject, - 'hello' => $locale->getText("emails.recovery.hello"), - 'body' => $body, - 'footer' => $locale->getText("emails.recovery.footer"), - 'thanks' => $locale->getText("emails.recovery.thanks"), - 'signature' => $locale->getText("emails.recovery.signature"), 'direction' => $locale->getText('settings.direction'), - 'bg-body' => '#f7f7f7', - 'bg-content' => '#ffffff', - 'text-content' => '#000000', /* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */ 'user' => $profile->getAttribute('name'), 'team' => '', @@ -2441,7 +2433,6 @@ App::post('/v1/account/recovery') 'redirect' => $url ]; - $queueForMails ->setRecipient($profile->getAttribute('email', '')) ->setName($profile->getAttribute('name')) @@ -2626,7 +2617,12 @@ App::post('/v1/account/verification') $customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? []; $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); - $message->setParam('{{body}}', $body); + $message + ->setParam('{{body}}', $body) + ->setParam('{{hello}}', $locale->getText("emails.verification.hello")) + ->setParam('{{footer}}', $locale->getText("emails.verification.footer")) + ->setParam('{{thanks}}', $locale->getText("emails.verification.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.verification.signature")); $body = $message->render(); $smtp = $project->getAttribute('smtp', []); @@ -2676,16 +2672,7 @@ App::post('/v1/account/verification') } $emailVariables = [ - 'subject' => $subject, - 'hello' => $locale->getText("emails.verification.hello"), - 'body' => $body, - 'footer' => $locale->getText("emails.verification.footer"), - 'thanks' => $locale->getText("emails.verification.thanks"), - 'signature' => $locale->getText("emails.verification.signature"), 'direction' => $locale->getText('settings.direction'), - 'bg-body' => '#f7f7f7', - 'bg-content' => '#ffffff', - 'text-content' => '#000000', /* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */ 'user' => $user->getAttribute('name'), 'team' => '', diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 4a96308492..d370a7109c 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -46,6 +46,7 @@ use Utopia\Validator\Boolean; use Utopia\Database\Exception\Duplicate as DuplicateException; use MaxMind\Db\Reader; use Utopia\VCS\Adapter\Git\GitHub; +use Utopia\VCS\Exception\RepositoryNotFound; include_once __DIR__ . '/../shared/api.php'; @@ -58,7 +59,14 @@ $redeployVcs = function (Request $request, Document $function, Document $project $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); $owner = $github->getOwnerName($providerInstallationId); $providerRepositoryId = $function->getAttribute('providerRepositoryId', ''); - $repositoryName = $github->getRepositoryName($providerRepositoryId); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } $providerBranch = $function->getAttribute('providerBranch', 'main'); $authorUrl = "https://github.com/$owner"; $repositoryUrl = "https://github.com/$owner/$repositoryName"; @@ -287,9 +295,8 @@ App::post('/v1/functions') $ruleModel = new Rule(); $ruleCreate = $queueForEvents - ->setClass(Event::WEBHOOK_CLASS_NAME) - ->setQueue(Event::WEBHOOK_QUEUE_NAME) - ; + ->setClass(Event::WEBHOOK_CLASS_NAME) + ->setQueue(Event::WEBHOOK_QUEUE_NAME); $ruleCreate ->setProject($project) @@ -870,8 +877,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId/download') ->setContentType('application/gzip') ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->addHeader('X-Peak', \memory_get_peak_usage()) - ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"') - ; + ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); $size = $deviceFunctions->getFileSize($path); $rangeHeader = $request->getHeader('range'); diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index fe7759a6df..b8f8ac4727 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -100,11 +100,6 @@ App::post('/v1/projects') $backups['database_db_fra1_v14x_07'] = ['from' => '00:00', 'to' => '02:00']; $databases = Config::getParam('pools-database', []); - $databaseSelfHosted = 'database_db_fra1_self_hosted_0_0'; - $selfHostedIndex = array_search($databaseSelfHosted, $databases); - if ($selfHostedIndex !== false) { - unset($databases[$selfHostedIndex]); - } /** * Remove databases from the list that are currently undergoing an backup @@ -175,16 +170,6 @@ App::post('/v1/projects') throw new Exception(Exception::PROJECT_ALREADY_EXISTS); } - /** - * Update database with self-managed db every $mod projects - */ - $mod = 20; - if ($project->getInternalId() % $mod === 0 && $selfHostedIndex !== false) { - $database = $databaseSelfHosted; - $project->setAttribute('database', $database); - $dbForConsole->updateDocument('projects', $project->getId(), $project); - } - $dbForProject = new Database($pools->get($database)->pop()->getResource(), $cache); $dbForProject->setNamespace("_{$project->getInternalId()}"); $dbForProject->create(); @@ -1718,7 +1703,6 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message ->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello")) - ->setParam('{{user}}', '') ->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer")) ->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body')) ->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks")) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 0a887908bb..2ee351f469 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -555,7 +555,12 @@ App::post('/v1/teams/:teamId/memberships') $customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? []; $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); - $message->setParam('{{body}}', $body); + $message + ->setParam('{{body}}', $body) + ->setParam('{{hello}}', $locale->getText("emails.invitation.hello")) + ->setParam('{{footer}}', $locale->getText("emails.invitation.footer")) + ->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.invitation.signature")); $body = $message->render(); $smtp = $project->getAttribute('smtp', []); @@ -606,16 +611,7 @@ App::post('/v1/teams/:teamId/memberships') $emailVariables = [ 'owner' => $user->getAttribute('name'), - 'subject' => $subject, - 'hello' => $locale->getText("emails.invitation.hello"), - 'body' => $body, - 'footer' => $locale->getText("emails.invitation.footer"), - 'thanks' => $locale->getText("emails.invitation.thanks"), - 'signature' => $locale->getText("emails.invitation.signature"), 'direction' => $locale->getText('settings.direction'), - 'bg-body' => '#f7f7f7', - 'bg-content' => '#ffffff', - 'text-content' => '#000000', /* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */ 'user' => $user->getAttribute('name'), 'team' => $team->getAttribute('name'), diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 55602136fc..8b61580b76 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -22,7 +22,6 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\UID; use Utopia\Detector\Adapter\Bun; use Utopia\Detector\Adapter\CPP; use Utopia\Detector\Adapter\Dart; @@ -36,6 +35,7 @@ use Utopia\Detector\Adapter\Ruby; use Utopia\Detector\Adapter\Swift; use Utopia\Detector\Detector; use Utopia\Validator\Boolean; +use Utopia\VCS\Exception\RepositoryNotFound; use function Swoole\Coroutine\batch; @@ -66,7 +66,14 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId } $owner = $github->getOwnerName($providerInstallationId) ?? ''; - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } if (empty($repositoryName)) { throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); @@ -154,7 +161,14 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $message = 'Authorization required for external contributor.'; $providerRepositoryId = $resource->getAttribute('providerRepositoryId'); - $repositoryName = $github->getRepositoryName($providerRepositoryId); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } $owner = $github->getOwnerName($providerInstallationId); $github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'failure', $message, $authorizeUrl, $name); continue; @@ -206,7 +220,14 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $message = 'Starting...'; $providerRepositoryId = $resource->getAttribute('providerRepositoryId'); - $repositoryName = $github->getRepositoryName($providerRepositoryId); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } $owner = $github->getOwnerName($providerInstallationId); $providerTargetUrl = $request->getProtocol() . '://' . $request->getHostname() . "/console/project-$projectId/functions/function-$functionId"; @@ -273,7 +294,7 @@ App::get('/v1/vcs/github/callback') ->label('scope', 'public') ->label('error', __DIR__ . '/../../views/general/error.phtml') ->param('installation_id', '', new Text(256, 0), 'GitHub installation ID', true) - ->param('setup_action', '', new Text(256, 0), 'GitHub setup actuon type', true) + ->param('setup_action', '', new Text(256, 0), 'GitHub setup action type', true) ->param('state', '', new Text(2048), 'GitHub state. Contains info sent when starting authorization flow.', true) ->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true) ->inject('gitHub') @@ -458,9 +479,12 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); $owner = $github->getOwnerName($providerInstallationId); - $repositoryName = $github->getRepositoryName($providerRepositoryId); - - if (empty($repositoryName)) { + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); } @@ -720,9 +744,12 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); $owner = $github->getOwnerName($providerInstallationId) ?? ''; - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - - if (empty($repositoryName)) { + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); } @@ -766,9 +793,12 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); $owner = $github->getOwnerName($providerInstallationId) ?? ''; - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - - if (empty($repositoryName)) { + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); } @@ -1090,7 +1120,14 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor $providerRepositoryId = $repository->getAttribute('providerRepositoryId'); $owner = $github->getOwnerName($providerInstallationId); - $repositoryName = $github->getRepositoryName($providerRepositoryId); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } $pullRequestResponse = $github->getPullRequest($owner, $repositoryName, $providerPullRequestId); $providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? ''; diff --git a/app/controllers/general.php b/app/controllers/general.php index 0a67148576..247669731a 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -117,12 +117,14 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques $path .= '?' . $query; } + $requestHeaders = $request->getHeaders(); + $body = \json_encode([ 'async' => false, 'body' => $swooleRequest->getContent() ?? '', 'method' => $swooleRequest->server['request_method'], 'path' => $path, - 'headers' => $swooleRequest->header + 'headers' => $requestHeaders ]); $headers = [ @@ -406,7 +408,7 @@ App::init() * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers */ if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations + if ($request->getProtocol() !== 'https' && ($requestHeaders['host'] ?? '') !== 'localhost' && ($requestHeaders['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations if ($request->getMethod() !== Request::METHOD_GET) { throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.'); } diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 593063ae84..bc4f463b5c 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -17,6 +17,9 @@ use Utopia\Validator\WhiteList; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\UID; use Utopia\Validator\Nullable; +use Utopia\VCS\Adapter\Git\GitHub; +use Utopia\Database\Helpers\Permission; +use Utopia\Database\Helpers\Role; App::get('/v1/mock/tests/foo') ->desc('Get Foo') @@ -248,8 +251,7 @@ App::get('/v1/mock/tests/general/download') ->addHeader('Content-Disposition', 'attachment; filename="test.txt"') ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->addHeader('X-Peak', \memory_get_peak_usage()) - ->send("GET:/v1/mock/tests/general/download:passed") - ; + ->send("GET:/v1/mock/tests/general/download:passed"); }); App::post('/v1/mock/tests/general/upload') @@ -331,7 +333,7 @@ App::post('/v1/mock/tests/general/upload') } if ($file['size'] !== 38756) { - throw new Exception(Exception::GENERAL_MOCK, 'Wrong file size'); + throw new Exception(Exception::GENERAL_MOCK, 'Wrong file size'); } if (\md5(\file_get_contents($file['tmp_name'])) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { @@ -506,8 +508,7 @@ App::get('/v1/mock/tests/general/502-error') $response ->setStatusCode(502) - ->text('This is a text error') - ; + ->text('This is a text error'); }); App::get('/v1/mock/tests/general/oauth2') @@ -646,6 +647,66 @@ App::patch('/v1/mock/functions-v2') $response->noContent(); }); +App::get('/v1/mock/github/callback') + ->desc('Create installation document using GitHub installation id') + ->groups(['mock', 'api', 'vcs']) + ->label('scope', 'public') + ->label('docs', false) + ->param('providerInstallationId', '', new UID(), 'GitHub installation ID') + ->param('projectId', '', new UID(), 'Project ID of the project where app is to be installed') + ->inject('gitHub') + ->inject('project') + ->inject('response') + ->inject('dbForConsole') + ->action(function (string $providerInstallationId, string $projectId, GitHub $github, Document $project, Response $response, Database $dbForConsole) { + $isDevelopment = App::getEnv('_APP_ENV', 'development') === 'development'; + + if (!$isDevelopment) { + throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED); + } + + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + $error = 'Project with the ID from state could not be found.'; + throw new Exception(Exception::PROJECT_NOT_FOUND, $error); + } + + if (!empty($providerInstallationId)) { + $privateKey = App::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = App::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + $owner = $github->getOwnerName($providerInstallationId) ?? ''; + + $projectInternalId = $project->getInternalId(); + + $teamId = $project->getAttribute('teamId', ''); + + $installation = new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + Permission::update(Role::team(ID::custom($teamId), 'owner')), + Permission::update(Role::team(ID::custom($teamId), 'developer')), + Permission::delete(Role::team(ID::custom($teamId), 'owner')), + Permission::delete(Role::team(ID::custom($teamId), 'developer')), + ], + 'providerInstallationId' => $providerInstallationId, + 'projectId' => $projectId, + 'projectInternalId' => $projectInternalId, + 'provider' => 'github', + 'organization' => $owner, + 'personal' => false + ]); + + $installation = $dbForConsole->createDocument('installations', $installation); + } + + $response->json([ + 'installationId' => $installation->getId(), + ]); + }); + App::shutdown() ->groups(['mock']) ->inject('utopia') diff --git a/app/init.php b/app/init.php index 86e4790a30..020af63761 100644 --- a/app/init.php +++ b/app/init.php @@ -109,8 +109,8 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours -const APP_CACHE_BUSTER = 514; -const APP_VERSION_STABLE = '1.4.7'; +const APP_CACHE_BUSTER = 515; +const APP_VERSION_STABLE = '1.4.8'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; @@ -1049,6 +1049,9 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons } } + $dbForProject->setMetadata('user', $user->getId()); + $dbForConsole->setMetadata('user', $user->getId()); + return $user; }, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForConsole']); @@ -1122,6 +1125,8 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, $database ->setNamespace('_' . $project->getInternalId()) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); return $database; @@ -1138,6 +1143,8 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) { $database ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); return $database; @@ -1158,6 +1165,8 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $database ->setNamespace('_' . $project->getInternalId()) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); return $database; @@ -1174,6 +1183,8 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $database ->setNamespace('_' . $project->getInternalId()) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); return $database; diff --git a/app/realtime.php b/app/realtime.php index 42fa43f575..f7fc7070a4 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -51,7 +51,10 @@ function getConsoleDB(): Database $database = new Database($dbAdapter, getCache()); - $database->setNamespace('_console'); + $database + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', '_console'); return $database; } @@ -74,7 +77,11 @@ function getProjectDB(Document $project): Database ; $database = new Database($dbAdapter, getCache()); - $database->setNamespace('_' . $project->getInternalId()); + + $database + ->setNamespace('_' . $project->getInternalId()) + ->setMetadata('host', \gethostname()) + ->setMetadata('project', $project->getId()); return $database; } diff --git a/bin/delete-orphaned-projects b/bin/delete-orphaned-projects new file mode 100644 index 0000000000..16b9e3184d --- /dev/null +++ b/bin/delete-orphaned-projects @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php delete-orphaned-projects $@ \ No newline at end of file diff --git a/composer.json b/composer.json index ef10f99a17..4ce175abea 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.3.*", - "utopia-php/messaging": "0.1.*", + "utopia-php/messaging": "0.2.*", "utopia-php/migration": "0.3.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.5.*", @@ -66,7 +66,7 @@ "utopia-php/registry": "0.5.*", "utopia-php/storage": "0.18.*", "utopia-php/swoole": "0.5.*", - "utopia-php/vcs": "0.5.*", + "utopia-php/vcs": "0.6.*", "utopia-php/websocket": "0.1.*", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", diff --git a/composer.lock b/composer.lock index cb467be2de..e8334179ff 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": "06c2610579f319495ea7d2d28f42d376", + "content-hash": "9afc62ce9c6ba587b9c028e11494e026", "packages": [ { "name": "adhocore/jwt", @@ -2270,16 +2270,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.1.1", + "version": "0.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "a75d66ddd59b834ab500a4878a2c084e6572604a" + "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/a75d66ddd59b834ab500a4878a2c084e6572604a", - "reference": "a75d66ddd59b834ab500a4878a2c084e6572604a", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/2d0f474a106bb1da285f85e105c29b46085d3a43", + "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43", "shasum": "" }, "require": { @@ -2288,8 +2288,8 @@ }, "require-dev": { "laravel/pint": "^1.2", - "phpmailer/phpmailer": "6.6.*", - "phpunit/phpunit": "9.5.*" + "phpmailer/phpmailer": "6.8.*", + "phpunit/phpunit": "9.6.*" }, "type": "library", "autoload": { @@ -2312,9 +2312,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.1.1" + "source": "https://github.com/utopia-php/messaging/tree/0.2.0" }, - "time": "2023-02-07T05:42:46+00:00" + "time": "2023-09-14T20:48:42+00:00" }, { "name": "utopia-php/migration", @@ -2904,16 +2904,16 @@ }, { "name": "utopia-php/vcs", - "version": "0.5.0", + "version": "0.6.1", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "47144f272030b7ed1b05471f2cb3aabeb8cb831c" + "reference": "d161d1156ef336d197a8d45384b531e5ec31243d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/47144f272030b7ed1b05471f2cb3aabeb8cb831c", - "reference": "47144f272030b7ed1b05471f2cb3aabeb8cb831c", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/d161d1156ef336d197a8d45384b531e5ec31243d", + "reference": "d161d1156ef336d197a8d45384b531e5ec31243d", "shasum": "" }, "require": { @@ -2947,9 +2947,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.5.0" + "source": "https://github.com/utopia-php/vcs/tree/0.6.1" }, - "time": "2023-09-13T19:05:52+00:00" + "time": "2023-10-19T07:43:31+00:00" }, { "name": "utopia-php/websocket", @@ -5822,5 +5822,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 964aef66e5..18f1a091c0 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -72,6 +72,7 @@ abstract class Migration '1.4.5' => 'V19', '1.4.6' => 'V19', '1.4.7' => 'V19', + '1.4.8' => 'V19', ]; /** diff --git a/src/Appwrite/Platform/Services/Tasks.php b/src/Appwrite/Platform/Services/Tasks.php index 00779084d4..e725ff5f3e 100644 --- a/src/Appwrite/Platform/Services/Tasks.php +++ b/src/Appwrite/Platform/Services/Tasks.php @@ -18,6 +18,7 @@ use Appwrite\Platform\Tasks\Version; use Appwrite\Platform\Tasks\VolumeSync; use Appwrite\Platform\Tasks\CalcTierStats; use Appwrite\Platform\Tasks\Upgrade; +use Appwrite\Platform\Tasks\DeleteOrphanedProjects; class Tasks extends Service { @@ -40,6 +41,8 @@ class Tasks extends Service ->addAction(VolumeSync::getName(), new VolumeSync()) ->addAction(Specs::getName(), new Specs()) ->addAction(CalcTierStats::getName(), new CalcTierStats()) + ->addAction(DeleteOrphanedProjects::getName(), new DeleteOrphanedProjects()) + ; } } diff --git a/src/Appwrite/Platform/Tasks/DeleteOrphanedProjects.php b/src/Appwrite/Platform/Tasks/DeleteOrphanedProjects.php new file mode 100644 index 0000000000..2824f4e286 --- /dev/null +++ b/src/Appwrite/Platform/Tasks/DeleteOrphanedProjects.php @@ -0,0 +1,115 @@ +desc('Get stats for projects') + ->inject('pools') + ->inject('cache') + ->inject('dbForConsole') + ->inject('register') + ->callback(function (Group $pools, Cache $cache, Database $dbForConsole, Registry $register) { + $this->action($pools, $cache, $dbForConsole, $register); + }); + } + + + public function action(Group $pools, Cache $cache, Database $dbForConsole, Registry $register): void + { + + Console::title('Delete orphaned projects V1'); + Console::success(APP_NAME . ' Delete orphaned projects started'); + + /** @var array $collections */ + $collectionsConfig = Config::getParam('collections', [])['projects'] ?? []; + + /* Initialise new Utopia app */ + $app = new App('UTC'); + $console = $app->getResource('console'); + $projects = [$console]; + + /** Database connections */ + $totalProjects = $dbForConsole->count('projects'); + Console::success("Found a total of: {$totalProjects} projects"); + + $orphans = 0; + $count = 0; + $limit = 30; + $sum = 30; + $offset = 0; + while (!empty($projects)) { + foreach ($projects as $project) { + + /** + * Skip user projects with id 'console' + */ + if ($project->getId() === 'console') { + continue; + } + + try { + $db = $project->getAttribute('database'); + $adapter = $pools + ->get($db) + ->pop() + ->getResource(); + + $dbForProject = new Database($adapter, $cache); + $dbForProject->setDefaultDatabase('appwrite'); + $dbForProject->setNamespace('_' . $project->getInternalId()); + $collectionsCreated = $dbForProject->count(Database::METADATA); + $message = ' (' . $collectionsCreated . ') collections where found on project (' . $project->getId() . '))'; + if ($collectionsCreated < (count($collectionsConfig) + 2)) { + Console::error($message); + $orphans++; + } else { + Console::log($message); + } + } catch (\Throwable $th) { + //$dbForConsole->deleteDocument('projects', $project->getId()); + //Console::success('Deleting project (' . $project->getId() . ')'); + Console::error(' (0) collections where found for project (' . $project->getId() . ')'); + $orphans++; + } finally { + $pools + ->get($db) + ->reclaim(); + } + } + + $sum = \count($projects); + + $projects = $dbForConsole->find('projects', [ + Query::limit($limit), + Query::offset($offset), + ]); + + $offset = $offset + $limit; + $count = $count + $sum; + } + + Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects found ' . $orphans . ' orphans'); + } +} diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index 89297a78ed..eb419ade11 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -6,9 +6,6 @@ use Appwrite\Auth\Auth; use Appwrite\Docker\Compose; use Appwrite\Docker\Env; use Appwrite\Utopia\View; -use Utopia\Analytics\Adapter; -use Utopia\Analytics\Adapter\GoogleAnalytics; -use Utopia\Analytics\Event; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Validator\Text; @@ -43,12 +40,6 @@ class Install extends Action /** @var array> $vars array whre key is variable name and value is variable */ $vars = []; - /** - * We are using a random value every execution for identification. - * This allows us to collect information without invading the privacy of our users. - */ - $analytics = new GoogleAnalytics('UA-26264668-9', uniqid('server.', true)); - foreach ($config as $category) { foreach ($category['variables'] ?? [] as $var) { $vars[$var['name']] = $var; @@ -82,7 +73,7 @@ class Install extends Action file_put_contents($this->path . '/docker-compose.yml.' . $time . '.backup', $data); $compose = new Compose($data); $appwrite = $compose->getService('appwrite'); - $oldVersion = ($appwrite) ? $appwrite->getImageVersion() : null; + $oldVersion = $appwrite?->getImageVersion(); try { $ports = $compose->getService('traefik')->getPorts(); } catch (\Throwable $th) { @@ -209,14 +200,12 @@ class Install extends Action if (!file_put_contents($this->path . '/docker-compose.yml', $templateForCompose->render(false))) { $message = 'Failed to save Docker Compose file'; - $this->sendEvent($analytics, $message); Console::error($message); Console::exit(1); } if (!file_put_contents($this->path . '/.env', $templateForEnv->render(false))) { $message = 'Failed to save environment variables file'; - $this->sendEvent($analytics, $message); Console::error($message); Console::exit(1); } @@ -237,29 +226,12 @@ class Install extends Action if ($exit !== 0) { $message = 'Failed to install Appwrite dockers'; - $this->sendEvent($analytics, $message); Console::error($message); Console::error($stderr); Console::exit($exit); } else { $message = 'Appwrite installed successfully'; - $this->sendEvent($analytics, $message); Console::success($message); } } - - private function sendEvent(Adapter $analytics, string $message): void - { - $event = new Event(); - $event->setName(APP_VERSION_STABLE); - $event->setValue($message); - $event->setUrl('http://localhost/'); - $event->setProps([ - 'category' => 'install/server', - 'action' => 'install', - ]); - $event->setType('install/server'); - - $analytics->createEvent($event); - } } diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 02c1835dd5..ade2125e4a 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -426,29 +426,31 @@ class Certificates extends Action $locale->setDefault('en'); } - $body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'); - $subject = \sprintf($locale->getText("emails.certificate.subject"), $domain); - $body - ->setParam('{{domain}}', $domain) - ->setParam('{{error}}', $errorMessage) - ->setParam('{{attempt}}', $attempt) - ->setParam('{{subject}}', $subject) - ->setParam('{{hello}}', $locale->getText("emails.certificate.hello")) + + $message = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-inner-base.tpl'); + $message ->setParam('{{body}}', $locale->getText("emails.certificate.body")) - ->setParam('{{redirect}}', 'https://' . $domain) + ->setParam('{{hello}}', $locale->getText("emails.certificate.hello")) ->setParam('{{footer}}', $locale->getText("emails.certificate.footer")) ->setParam('{{thanks}}', $locale->getText("emails.certificate.thanks")) - ->setParam('{{signature}}', $locale->getText("emails.certificate.signature")) - ->setParam('{{project}}', 'Console') - ->setParam('{{direction}}', $locale->getText('settings.direction')) - ->setParam('{{bg-body}}', '#f7f7f7') - ->setParam('{{bg-content}}', '#ffffff') - ->setParam('{{text-content}}', '#000000'); + ->setParam('{{signature}}', $locale->getText("emails.certificate.signature")); + $body = $message->render(); + + $emailVariables = [ + 'direction' => $locale->getText('settings.direction'), + 'domain' => $domain, + 'error' => '
' . $errorMessage . '
', + 'attempt' => $attempt, + 'project' => 'Console', + 'redirect' => 'https://' . $domain, + ]; $queueForMails ->setRecipient(App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')) - ->setBody($body->render()) + ->setSubject($subject) + ->setBody($body) + ->setVariables($emailVariables) ->setName('Appwrite Administrator') ->trigger(); } diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index 7a20212c9c..94a2a46087 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -58,13 +58,21 @@ class Mails extends Action $subject = $payload['subject']; $variables = $payload['variables']; $name = $payload['name']; - $body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'); + $body = $payload['body']; + $bodyTemplate = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base.tpl'); + $bodyTemplate->setParam('{{body}}', $body); foreach ($variables as $key => $value) { - $body->setParam('{{' . $key . '}}', $value); + $bodyTemplate->setParam('{{' . $key . '}}', $value); } + $body = $bodyTemplate->render(); - $body = $body->render(); + $subjectTemplate = Template::fromString($subject); + foreach ($variables as $key => $value) { + $subjectTemplate->setParam('{{' . $key . '}}', $value); + } + // render() will return the subject in

tags, so use strip_tags() to remove them + $subject = \strip_tags($subjectTemplate->render()); /** @var PHPMailer $mail */ $mail = empty($smtp) diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 229c9dd53d..a9eef62e06 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -95,4 +95,42 @@ class Request extends UtopiaRequest { return self::$route != null; } + + /** + * Get headers + * + * Method for getting all HTTP header parameters, including cookies. + * + * @return array + */ + public function getHeaders(): array + { + $headers = $this->generateHeaders(); + + $cookieHeaders = []; + foreach ($this->swoole->cookie as $key => $value) { + $cookieHeaders[] = "{$key}={$value}"; + } + + if (!empty($cookieHeaders)) { + $headers['cookie'] = \implode('; ', $cookieHeaders); + } + + return $headers; + } + + /** + * Get header + * + * Method for querying HTTP header parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param string $default + * @return string + */ + public function getHeader(string $key, string $default = ''): string + { + $headers = $this->getHeaders(); + return $headers[$key] ?? $default; + } } diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 408b8c8fe7..5f7bf85d0e 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -81,6 +81,8 @@ trait ProjectCustom 'locale.read', 'avatars.read', 'health.read', + 'rules.read', + 'rules.write' ], ]); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index cdc9ec846f..e39c6573e4 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1344,4 +1344,192 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); } + + public function testCookieExecution() + { + $timeout = 5; + $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-cookie/code.tar.gz"; + $this->packageCode('php-cookie'); + + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test PHP Cookie executions', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => $timeout, + ]); + + $functionId = $function['body']['$id'] ?? ''; + + $this->assertEquals(201, $function['headers']['status-code']); + + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'entrypoint' => 'index.php', + 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'activate' => true + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertEquals(202, $deployment['headers']['status-code']); + + // Poll until deployment is built + while (true) { + $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + if ( + $deployment['headers']['status-code'] >= 400 + || \in_array($deployment['body']['status'], ['ready', 'failed']) + ) { + break; + } + + \sleep(1); + } + + $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $deployment['headers']['status-code']); + + // Wait a little for activation to finish + sleep(5); + + $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5'; + + $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'async' => false, + 'headers' => [ + 'cookie' => $cookie + ] + ]); + + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertEquals($cookie, $execution['body']['responseBody']); + + // Cleanup : Delete function + $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], []); + + $this->assertEquals(204, $response['headers']['status-code']); + } + + public function testFunctionsDomain() + { + $timeout = 5; + $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-cookie/code.tar.gz"; + $this->packageCode('php-cookie'); + + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test PHP Cookie executions', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => $timeout, + 'execute' => ['any'] + ]); + + $functionId = $function['body']['$id'] ?? ''; + + $this->assertEquals(201, $function['headers']['status-code']); + + $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ 'equal("resourceId", "' . $functionId . '")', 'equal("resourceType", "function")' ] + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(1, $rules['body']['total']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + + $domain = $rules['body']['rules'][0]['domain']; + + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'entrypoint' => 'index.php', + 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'activate' => true + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertEquals(202, $deployment['headers']['status-code']); + + // Poll until deployment is built + while (true) { + $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + if ( + $deployment['headers']['status-code'] >= 400 + || \in_array($deployment['body']['status'], ['ready', 'failed']) + ) { + break; + } + + \sleep(1); + } + + $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $deployment['headers']['status-code']); + + // Wait a little for activation to finish + sleep(5); + + $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => $cookie + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + + // Cleanup : Delete function + $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], []); + + $this->assertEquals(204, $response['headers']['status-code']); + } } diff --git a/tests/e2e/Services/VCS/VCSBase.php b/tests/e2e/Services/VCS/VCSBase.php new file mode 100644 index 0000000000..7531ea3bc8 --- /dev/null +++ b/tests/e2e/Services/VCS/VCSBase.php @@ -0,0 +1,17 @@ +markTestSkipped('VCS is not enabled.'); + } + } +} diff --git a/tests/e2e/Services/VCS/VCSConsoleClientTest.php b/tests/e2e/Services/VCS/VCSConsoleClientTest.php new file mode 100644 index 0000000000..697fbdba91 --- /dev/null +++ b/tests/e2e/Services/VCS/VCSConsoleClientTest.php @@ -0,0 +1,319 @@ +client->call(Client::METHOD_GET, '/mock/github/callback', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'providerInstallationId' => $this->providerInstallationId, + 'projectId' => $this->getProject()['$id'], + ]); + + $this->assertNotEmpty($response['body']['installationId']); + $installationId = $response['body']['installationId']; + return $installationId; + } + + /** + * @depends testGitHubAuthorize + */ + public function testGetInstallation(string $installationId): void + { + /** + * Test for SUCCESS + */ + + $installation = $this->client->call(Client::METHOD_GET, '/vcs/installations/' . $installationId, array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $installation['headers']['status-code']); + $this->assertEquals('github', $installation['body']['provider']); + $this->assertEquals('appwrite-test', $installation['body']['organization']); + } + + /** + * @depends testGitHubAuthorize + */ + public function testDetectRuntime(string $installationId): void + { + /** + * Test for SUCCESS + */ + + $runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/detection', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $runtime['headers']['status-code']); + $this->assertEquals($runtime['body']['runtime'], 'ruby-3.1'); + + /** + * Test for FAILURE + */ + + $runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/randomRepositoryId/detection', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $runtime['headers']['status-code']); + } + + /** + * @depends testGitHubAuthorize + */ + public function testListRepositories(string $installationId): void + { + /** + * Test for SUCCESS + */ + + $repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $repositories['headers']['status-code']); + $this->assertEquals($repositories['body']['total'], 3); + $this->assertEquals($repositories['body']['providerRepositories'][0]['name'], 'function1.4'); + $this->assertEquals($repositories['body']['providerRepositories'][0]['organization'], 'appwrite-test'); + $this->assertEquals($repositories['body']['providerRepositories'][0]['provider'], 'github'); + $this->assertEquals($repositories['body']['providerRepositories'][1]['name'], 'appwrite'); + $this->assertEquals($repositories['body']['providerRepositories'][2]['name'], 'ruby-starter'); + + + $searchedRepositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'search' => 'func' + ]); + + $this->assertEquals(200, $searchedRepositories['headers']['status-code']); + $this->assertEquals($searchedRepositories['body']['total'], 1); + $this->assertEquals($searchedRepositories['body']['providerRepositories'][0]['name'], 'function1.4'); + + /** + * Test for FAILURE + */ + + $repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/randomInstallationId/providerRepositories', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $repositories['headers']['status-code']); + } + + /** + * @depends testGitHubAuthorize + */ + public function testGetRepository(string $installationId): void + { + /** + * Test for SUCCESS + */ + + $repository = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId, array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $repository['headers']['status-code']); + $this->assertEquals($repository['body']['name'], 'ruby-starter'); + $this->assertEquals($repository['body']['organization'], 'appwrite-test'); + $this->assertEquals($repository['body']['private'], false); + + $repository = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId2, array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $repository['headers']['status-code']); + $this->assertEquals($repository['body']['name'], 'function1.4'); + $this->assertEquals($repository['body']['organization'], 'appwrite-test'); + $this->assertEquals($repository['body']['private'], true); + + /** + * Test for FAILURE + */ + + $repository = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/randomRepositoryId', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $repository['headers']['status-code']); + } + + /** + * @depends testGitHubAuthorize + */ + public function testListRepositoryBranches(string $installationId): void + { + /** + * Test for SUCCESS + */ + + $repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $repositoryBranches['headers']['status-code']); + $this->assertEquals($repositoryBranches['body']['total'], 2); + $this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'main'); + $this->assertEquals($repositoryBranches['body']['branches'][1]['name'], 'test'); + + /** + * Test for FAILURE + */ + + $repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/randomRepositoryId/branches', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $repositoryBranches['headers']['status-code']); + } + + /** + * @depends testGitHubAuthorize + */ + public function testCreateFunctionUsingVCS(string $installationId): array + { + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'events' => [ + 'users.*.create', + 'users.*.delete', + ], + 'schedule' => '0 0 1 1 *', + 'timeout' => 10, + 'installationId' => $installationId, + 'providerRepositoryId' => $this->providerRepositoryId, + 'providerBranch' => 'main', + ]); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertEquals('Test', $function['body']['name']); + $this->assertEquals('php-8.0', $function['body']['runtime']); + $this->assertEquals('index.php', $function['body']['entrypoint']); + $this->assertEquals('705764267', $function['body']['providerRepositoryId']); + $this->assertEquals('main', $function['body']['providerBranch']); + + return [ + 'installationId' => $installationId, + 'functionId' => $function['body']['$id'] + ]; + } + + /** + * @depends testCreateFunctionUsingVCS + */ + public function testUpdateFunctionUsingVCS(array $data): string + { + $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test', + 'execute' => [Role::user($this->getUser()['$id'])->toString()], + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'events' => [ + 'users.*.create', + 'users.*.delete', + ], + 'schedule' => '0 0 1 1 *', + 'timeout' => 10, + 'installationId' => $data['installationId'], + 'providerRepositoryId' => $this->providerRepositoryId2, + 'providerBranch' => 'main', + ]); + + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals('Test', $function['body']['name']); + $this->assertEquals('php-8.0', $function['body']['runtime']); + $this->assertEquals('index.php', $function['body']['entrypoint']); + $this->assertEquals('708688544', $function['body']['providerRepositoryId']); + $this->assertEquals('main', $function['body']['providerBranch']); + + return $function['body']['$id']; + } + + /** + * @depends testGitHubAuthorize + */ + public function testCreateRepository(string $installationId): void + { + /** + * Test for SUCCESS + */ + + $github = new GitHub(new Cache(new None())); + $privateKey = App::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = App::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($this->providerInstallationId, $privateKey, $githubAppId); + + $repository = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'test-repo-1', + 'private' => true + ]); + + $this->assertEquals('test-repo-1', $repository['body']['name']); + $this->assertEquals('appwrite-test', $repository['body']['organization']); + $this->assertEquals('github', $repository['body']['provider']); + + /** + * Test for FAILURE + */ + + $repository = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'test-repo-1', + 'private' => true + ]); + + $this->assertEquals(400, $repository['headers']['status-code']); + $this->assertEquals('Provider Error: Repository creation failed. name already exists on this account', $repository['body']['message']); + + /** + * Test for SUCCESS + */ + + $result = $github->deleteRepository('appwrite-test', 'test-repo-1'); + $this->assertEquals($result, true); + } +} diff --git a/tests/e2e/Services/VCS/VCSCustomClientTest.php b/tests/e2e/Services/VCS/VCSCustomClientTest.php new file mode 100644 index 0000000000..084255cd69 --- /dev/null +++ b/tests/e2e/Services/VCS/VCSCustomClientTest.php @@ -0,0 +1,24 @@ +client->call(Client::METHOD_GET, '/vcs/installations/' . $installationId, array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(401, $installation['headers']['status-code']); + } +} diff --git a/tests/e2e/Services/VCS/VCSCustomServerTest.php b/tests/e2e/Services/VCS/VCSCustomServerTest.php new file mode 100644 index 0000000000..dee4cd7ced --- /dev/null +++ b/tests/e2e/Services/VCS/VCSCustomServerTest.php @@ -0,0 +1,25 @@ +client->call(Client::METHOD_GET, '/vcs/installations/' . $installationId, array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $this->getHeaders())); + + $this->assertEquals(401, $installation['headers']['status-code']); + } +} diff --git a/tests/resources/functions/php-cookie/index.php b/tests/resources/functions/php-cookie/index.php new file mode 100644 index 0000000000..8f38f752cb --- /dev/null +++ b/tests/resources/functions/php-cookie/index.php @@ -0,0 +1,5 @@ +res->send($context->req->headers['cookie'] ?? ''); +};