diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8522022bdb..ab59c97ff7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,21 +11,17 @@ Happy contributing! ## What does this PR do? -(Provide a description of what this PR does.) +(Provide a description of what this PR does and why it's needed.) ## Test Plan -(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.) +(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.) ## Related PRs and Issues -(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.) +- (Related PR or issue) -### Have you added your change to the [Changelog](https://github.com/appwrite/appwrite/blob/master/CHANGES.md)? +## Checklist -(The CHANGES.md file tracks all the changes that make it to the `main` branch. Add your change to this file in the following format) -- One line description of your PR [#pr_number](Link to your PR) - -### Have you read the [Contributing Guidelines on issues](https://github.com/appwrite/appwrite/blob/master/CONTRIBUTING.md)? - -(Write your answer here.) +- [ ] Have you read the [Contributing Guidelines on issues](https://github.com/appwrite/appwrite/blob/master/CONTRIBUTING.md)? +- [ ] If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs? diff --git a/app/assets/dbip/dbip-country-lite-2022-06.mmdb b/app/assets/dbip/dbip-country-lite-2022-06.mmdb deleted file mode 100644 index 0e8c8dfecf..0000000000 Binary files a/app/assets/dbip/dbip-country-lite-2022-06.mmdb and /dev/null differ diff --git a/app/assets/dbip/dbip-country-lite-2023-01.mmdb b/app/assets/dbip/dbip-country-lite-2023-01.mmdb new file mode 100644 index 0000000000..dd98483fe5 Binary files /dev/null and b/app/assets/dbip/dbip-country-lite-2023-01.mmdb differ diff --git a/app/config/variables.php b/app/config/variables.php index 19139c0d75..193e167b8f 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -482,9 +482,9 @@ return [ ], [ 'name' => '_APP_STORAGE_DEVICE', - 'description' => 'Select default storage device. The default value is \'Local\'. List of supported adapters are \'Local\', \'S3\', \'DOSpaces\', \'Backblaze\', \'Linode\' and \'Wasabi\'.', + 'description' => 'Select default storage device. The default value is \'local\'. List of supported adapters are \'local\', \'s3\', \'dospaces\', \'backblaze\', \'linode\' and \'wasabi\'.', 'introduction' => '0.13.0', - 'default' => 'Local', + 'default' => 'local', 'required' => false, 'question' => '', ], diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index f9425f690b..e7df74b686 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -10,8 +10,8 @@ use Appwrite\Event\Mail; use Appwrite\Event\Phone as EventPhone; use Appwrite\Extend\Exception; use Appwrite\Network\Validator\Email; -use Appwrite\Network\Validator\Host; -use Appwrite\Network\Validator\URL; +use Utopia\Validator\Host; +use Utopia\Validator\URL; use Appwrite\OpenSSL\OpenSSL; use Appwrite\Template\Template; use Appwrite\URL\URL as URLParser; diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 75d9ad434b..19daaea20d 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -1,7 +1,7 @@ param('enabled', true, new Boolean(true), 'Is bucket enabled?', true) ->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self-hosted setups you can change the max limit by changing the `_APP_STORAGE_LIMIT` environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) - ->param('compression', 'none', new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) + ->param('compression', COMPRESSION_TYPE_NONE, new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) ->inject('response') @@ -237,7 +237,7 @@ App::put('/v1/storage/buckets/:bucketId') ->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true) ->param('maximumFileSize', null, new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self hosted version you can change the limit by changing _APP_STORAGE_LIMIT environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) - ->param('compression', 'none', new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) + ->param('compression', COMPRESSION_TYPE_NONE, new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) ->inject('response') @@ -501,7 +501,7 @@ App::post('/v1/storage/buckets/:bucketId/files') } if ($chunksUploaded === $chunks) { - if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) { + if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) === Storage::DEVICE_LOCAL) { $antivirus = new Network( App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'), (int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310) @@ -516,14 +516,14 @@ App::post('/v1/storage/buckets/:bucketId/files') $mimeType = $deviceFiles->getFileMimeType($path); // Get mime-type before compression and encryption $data = ''; // Compression - $algorithm = $bucket->getAttribute('compression', 'none'); - if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != 'none') { + $algorithm = $bucket->getAttribute('compression', COMPRESSION_TYPE_NONE); + if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != COMPRESSION_TYPE_NONE) { $data = $deviceFiles->read($path); switch ($algorithm) { - case 'zstd': + case COMPRESSION_TYPE_ZSTD: $compressor = new Zstd(); break; - case 'gzip': + case COMPRESSION_TYPE_GZIP: default: $compressor = new GZIP(); break; diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 9eba2d01d7..72336633ad 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -7,7 +7,7 @@ use Appwrite\Event\Event; use Appwrite\Event\Mail; use Appwrite\Extend\Exception; use Appwrite\Network\Validator\Email; -use Appwrite\Network\Validator\Host; +use Utopia\Validator\Host; use Appwrite\Template\Template; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries; diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 034f18165b..b8691dfa1a 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -4,7 +4,7 @@ global $utopia, $request, $response; use Appwrite\Extend\Exception; use Utopia\Database\Document; -use Appwrite\Network\Validator\Host; +use Utopia\Validator\Host; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; diff --git a/app/executor.php b/app/executor.php index fba8c4c416..316794b929 100644 --- a/app/executor.php +++ b/app/executor.php @@ -119,7 +119,7 @@ function logError(Throwable $error, string $action, Utopia\Route $route = null) function getStorageDevice($root): Device { - switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) { + switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) { case Storage::DEVICE_LOCAL: default: return new Local($root); diff --git a/app/init.php b/app/init.php index 14072b0dfe..bbe145cb3b 100644 --- a/app/init.php +++ b/app/init.php @@ -33,9 +33,7 @@ use Appwrite\Extend\PDO; use Appwrite\GraphQL\Promises\Adapter\Swoole; use Appwrite\GraphQL\Schema; use Appwrite\Network\Validator\Email; -use Appwrite\Network\Validator\IP; use Appwrite\Network\Validator\Origin; -use Appwrite\Network\Validator\URL; use Appwrite\OpenSSL\OpenSSL; use Appwrite\Usage\Stats; use MaxMind\Db\Reader; @@ -74,6 +72,8 @@ use Utopia\Storage\Device\S3; use Utopia\Storage\Device\Wasabi; use Utopia\Storage\Storage; use Utopia\Validator\Range; +use Utopia\Validator\IP; +use Utopia\Validator\URL; use Utopia\Validator\WhiteList; const APP_NAME = 'Appwrite'; @@ -605,7 +605,7 @@ $register->set('smtp', function () { return $mail; }); $register->set('geodb', function () { - return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2022-06.mmdb'); + return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2023-01.mmdb'); }); $register->set('db', function () { // This is usually for our workers or CLI commands scope @@ -969,7 +969,7 @@ App::setResource('deviceBuilds', function ($project) { function getDevice($root): Device { - switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) { + switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) { case Storage::DEVICE_LOCAL: default: return new Local($root); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 2070043399..015a1f4a9d 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -509,6 +509,7 @@ services: entrypoint: worker-messaging <<: *x-logging container_name: appwrite-worker-messaging + restart: unless-stopped networks: - appwrite depends_on: diff --git a/app/workers/builds.php b/app/workers/builds.php index 5f33ed4010..fdd633f84a 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -91,7 +91,7 @@ class BuildsV1 extends Worker 'outputPath' => '', 'runtime' => $function->getAttribute('runtime'), 'source' => $deployment->getAttribute('path'), - 'sourceType' => App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL), + 'sourceType' => strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)), 'stdout' => '', 'stderr' => '', 'endTime' => null, diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 5dc7e8d737..111889318a 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -44,9 +44,6 @@ class DeletesV1 extends Worker case DELETE_TYPE_DATABASES: $this->deleteDatabase($document, $project->getId()); break; - case DELETE_TYPE_COLLECTIONS: - $this->deleteCollection($document, $project->getId()); - break; case DELETE_TYPE_PROJECTS: $this->deleteProject($document); break; @@ -66,6 +63,10 @@ class DeletesV1 extends Worker $this->deleteBucket($document, $project->getId()); break; default: + if (\str_starts_with($document->getCollection(), 'database_')) { + $this->deleteCollection($document, $project->getId()); + break; + } Console::error('No lazy delete operation available for document of type: ' . $document->getCollection()); break; } @@ -172,6 +173,7 @@ class DeletesV1 extends Worker /** * @param Document $document database document * @param string $projectId + * @throws Exception */ protected function deleteDatabase(Document $document, string $projectId): void { @@ -191,6 +193,7 @@ class DeletesV1 extends Worker /** * @param Document $document teams document * @param string $projectId + * @throws Exception */ protected function deleteCollection(Document $document, string $projectId): void { @@ -217,6 +220,7 @@ class DeletesV1 extends Worker /** * @param string $hourlyUsageRetentionDatetime + * @throws Exception */ protected function deleteUsageStats(string $hourlyUsageRetentionDatetime) { @@ -232,6 +236,7 @@ class DeletesV1 extends Worker /** * @param Document $document teams document * @param string $projectId + * @throws Exception */ protected function deleteMemberships(Document $document, string $projectId): void { @@ -245,25 +250,62 @@ class DeletesV1 extends Worker /** * @param Document $document project document + * @throws Exception */ protected function deleteProject(Document $document): void { $projectId = $document->getId(); - // Delete all DBs - $this->getProjectDB($projectId)->delete($projectId); + // Delete project domains and certificates + $dbForConsole = $this->getConsoleDB(); + + $domains = $dbForConsole->find('domains', [ + Query::equal('projectInternalId', [$document->getInternalId()]) + ]); + + foreach ($domains as $domain) { + $this->deleteCertificates($domain); + } + + // Delete project tables + $dbForProject = $this->getProjectDB($projectId, $document); + + while (true) { + $collections = $dbForProject->listCollections(); + + if (empty($collections)) { + break; + } + + foreach ($collections as $collection) { + $dbForProject->deleteCollection($collection->getId()); + } + } + + // Delete metadata tables + try { + $dbForProject->deleteCollection('_metadata'); + } catch (Exception) { + // Ignore: deleteCollection tries to delete a metadata entry after the collection is deleted, + // which will throw an exception here because the metadata collection is already deleted. + } // Delete all storage directories - $uploads = $this->getFilesDevice($document->getId()); - $cache = new Local(APP_STORAGE_CACHE . '/app-' . $document->getId()); + $uploads = $this->getFilesDevice($projectId); + $functions = $this->getFunctionsDevice($projectId); + $builds = $this->getBuildsDevice($projectId); + $cache = $this->getCacheDevice($projectId); $uploads->delete($uploads->getRoot(), true); + $functions->delete($functions->getRoot(), true); + $builds->delete($builds->getRoot(), true); $cache->delete($cache->getRoot(), true); } /** * @param Document $document user document * @param string $projectId + * @throws Exception */ protected function deleteUser(Document $document, string $projectId): void { @@ -305,6 +347,7 @@ class DeletesV1 extends Worker /** * @param string $datetime + * @throws Exception */ protected function deleteExecutionLogs(string $datetime): void { @@ -337,6 +380,7 @@ class DeletesV1 extends Worker /** * @param string $datetime + * @throws Exception */ protected function deleteRealtimeUsage(string $datetime): void { @@ -393,6 +437,7 @@ class DeletesV1 extends Worker /** * @param string $resource * @param string $projectId + * @throws Exception */ protected function deleteAuditLogsByResource(string $resource, string $projectId): void { @@ -406,6 +451,7 @@ class DeletesV1 extends Worker /** * @param Document $document function document * @param string $projectId + * @throws Exception */ protected function deleteFunction(Document $document, string $projectId): void { @@ -479,6 +525,7 @@ class DeletesV1 extends Worker /** * @param Document $document deployment document * @param string $projectId + * @throws Exception */ protected function deleteDeployment(Document $document, string $projectId): void { @@ -528,9 +575,10 @@ class DeletesV1 extends Worker /** * @param Document $document to be deleted * @param Database $database to delete it from - * @param callable $callback to perform after document is deleted + * @param callable|null $callback to perform after document is deleted * * @return bool + * @throws \Utopia\Database\Exception\Authorization */ protected function deleteById(Document $document, Database $database, callable $callback = null): bool { @@ -550,6 +598,7 @@ class DeletesV1 extends Worker /** * @param callable $callback + * @throws Exception */ protected function deleteForProjectIds(callable $callback): void { @@ -584,9 +633,10 @@ class DeletesV1 extends Worker /** * @param string $collection collectionID - * @param Query[] $queries + * @param array $queries * @param Database $database - * @param callable $callback + * @param callable|null $callback + * @throws Exception */ protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void { @@ -620,6 +670,7 @@ class DeletesV1 extends Worker /** * @param Document $document certificates document + * @throws \Utopia\Database\Exception\Authorization */ protected function deleteCertificates(Document $document): void { diff --git a/composer.json b/composer.json index b2008b5cc0..37b3e1bb17 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "utopia-php/config": "0.2.*", "utopia-php/database": "0.28.*", "utopia-php/domains": "1.1.*", - "utopia-php/framework": "0.25.*", + "utopia-php/framework": "0.26.*", "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.3.*", diff --git a/composer.lock b/composer.lock index d09230b398..0b9fd80951 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": "dbc502462e4afa550b1a1bc3898020b3", + "content-hash": "8782e69514f4564a3dcb44455161eedc", "packages": [ { "name": "adhocore/jwt", @@ -1946,16 +1946,16 @@ }, { "name": "utopia-php/framework", - "version": "0.25.1", + "version": "0.26.0", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "2391b397135586b2100d39e338827bef8d2f4ad0" + "reference": "e8da5576370366d3bf9c574ec855f8c96fe4f34e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/2391b397135586b2100d39e338827bef8d2f4ad0", - "reference": "2391b397135586b2100d39e338827bef8d2f4ad0", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/e8da5576370366d3bf9c574ec855f8c96fe4f34e", + "reference": "e8da5576370366d3bf9c574ec855f8c96fe4f34e", "shasum": "" }, "require": { @@ -1984,9 +1984,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.25.1" + "source": "https://github.com/utopia-php/framework/tree/0.26.0" }, - "time": "2022-11-23T18:22:23+00:00" + "time": "2023-01-13T08:14:43+00:00" }, { "name": "utopia-php/image", @@ -2652,16 +2652,16 @@ }, { "name": "webonyx/graphql-php", - "version": "v14.11.8", + "version": "v14.11.9", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "04a48693acd785330eefd3b0e4fa67df8dfee7c3" + "reference": "ff91c9f3cf241db702e30b2c42bcc0920e70ac70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/04a48693acd785330eefd3b0e4fa67df8dfee7c3", - "reference": "04a48693acd785330eefd3b0e4fa67df8dfee7c3", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/ff91c9f3cf241db702e30b2c42bcc0920e70ac70", + "reference": "ff91c9f3cf241db702e30b2c42bcc0920e70ac70", "shasum": "" }, "require": { @@ -2706,7 +2706,7 @@ ], "support": { "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v14.11.8" + "source": "https://github.com/webonyx/graphql-php/tree/v14.11.9" }, "funding": [ { @@ -2714,7 +2714,7 @@ "type": "open_collective" } ], - "time": "2022-09-21T15:35:03+00:00" + "time": "2023-01-06T12:12:50+00:00" } ], "packages-dev": [ @@ -2771,30 +2771,30 @@ }, { "name": "doctrine/instantiator", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^0.16 || ^1", "phpstan/phpstan": "^1.4", "phpstan/phpstan-phpunit": "^1", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -2821,7 +2821,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -2837,7 +2837,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "matthiasmullie/minify", @@ -5271,5 +5271,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.1.0" } diff --git a/docker-compose.yml b/docker-compose.yml index df1d5a78f3..8ba1ff0566 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -554,6 +554,7 @@ services: entrypoint: worker-messaging <<: *x-logging container_name: appwrite-worker-messaging + restart: unless-stopped image: appwrite-dev networks: - appwrite diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index a4b228d306..42bdc4b149 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -228,18 +228,18 @@ class Mapper case 'Appwrite\Network\Validator\CNAME': case 'Appwrite\Task\Validator\Cron': case 'Appwrite\Utopia\Database\Validator\CustomId': - case 'Appwrite\Network\Validator\Domain': + case 'Utopia\Validator\Domain': case 'Appwrite\Network\Validator\Email': case 'Appwrite\Event\Validator\Event': case 'Utopia\Validator\HexColor': - case 'Appwrite\Network\Validator\Host': - case 'Appwrite\Network\Validator\IP': + case 'Utopia\Validator\Host': + case 'Utopia\Validator\IP': case 'Utopia\Database\Validator\Key': - case 'Appwrite\Network\Validator\Origin': + case 'Utopia\Validator\Origin': case 'Appwrite\Auth\Validator\Password': case 'Utopia\Validator\Text': case 'Utopia\Database\Validator\UID': - case 'Appwrite\Network\Validator\URL': + case 'Utopia\Validator\URL': case 'Utopia\Validator\WhiteList': default: $type = Type::string(); diff --git a/src/Appwrite/Network/Validator/Domain.php b/src/Appwrite/Network/Validator/Domain.php deleted file mode 100644 index 9db6641316..0000000000 --- a/src/Appwrite/Network/Validator/Domain.php +++ /dev/null @@ -1,78 +0,0 @@ -whitelist = $whitelist; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'URL host must be one of: ' . \implode(', ', $this->whitelist); - } - - /** - * Is valid - * - * Validation will pass when $value starts with one of the given hosts - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - // Check if value is valid URL - $urlValidator = new URL(); - - if (!$urlValidator->isValid($value)) { - return false; - } - - $hostname = \parse_url($value, PHP_URL_HOST); - $hostnameValidator = new Hostname($this->whitelist); - return $hostnameValidator->isValid($hostname); - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/src/Appwrite/Network/Validator/IP.php b/src/Appwrite/Network/Validator/IP.php deleted file mode 100644 index 0245d59d3e..0000000000 --- a/src/Appwrite/Network/Validator/IP.php +++ /dev/null @@ -1,114 +0,0 @@ -type = $type; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid IP address'; - } - - /** - * Is valid - * - * Validation will pass when $value is valid IP address. - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - switch ($this->type) { - case self::ALL: - if (\filter_var($value, FILTER_VALIDATE_IP)) { - return true; - } - break; - - case self::V4: - if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - return true; - } - break; - - case self::V6: - if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - return true; - } - break; - - default: - return false; - break; - } - - return false; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/src/Appwrite/Network/Validator/URL.php b/src/Appwrite/Network/Validator/URL.php deleted file mode 100644 index 40a12420f5..0000000000 --- a/src/Appwrite/Network/Validator/URL.php +++ /dev/null @@ -1,92 +0,0 @@ -allowedSchemes = $allowedSchemes; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - if (!empty($this->allowedSchemes)) { - return 'Value must be a valid URL with following schemes (' . \implode(', ', $this->allowedSchemes) . ')'; - } - - return 'Value must be a valid URL'; - } - - /** - * Is valid - * - * Validation will pass when $value is valid URL. - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - $sanitizedURL = ''; - - foreach (str_split($value) as $character) { - $sanitizedURL .= (ord($character) > 127) ? rawurlencode($character) : $character; - } - - if (\filter_var($sanitizedURL, FILTER_VALIDATE_URL) === false) { - return false; - } - - if (!empty($this->allowedSchemes) && !\in_array(\parse_url($sanitizedURL, PHP_URL_SCHEME), $this->allowedSchemes)) { - return false; - } - - return true; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/src/Appwrite/Resque/Worker.php b/src/Appwrite/Resque/Worker.php index dd7cebd084..d7052379ff 100644 --- a/src/Appwrite/Resque/Worker.php +++ b/src/Appwrite/Resque/Worker.php @@ -2,22 +2,23 @@ namespace Appwrite\Resque; +use Exception; use Utopia\App; -use Utopia\Cache\Cache; use Utopia\Cache\Adapter\Redis as RedisCache; +use Utopia\Cache\Cache; use Utopia\CLI\Console; -use Utopia\Database\Database; use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Database\Validator\Authorization; use Utopia\Storage\Device; -use Utopia\Storage\Storage; -use Utopia\Storage\Device\Local; +use Utopia\Storage\Device\Backblaze; use Utopia\Storage\Device\DOSpaces; use Utopia\Storage\Device\Linode; -use Utopia\Storage\Device\Wasabi; -use Utopia\Storage\Device\Backblaze; +use Utopia\Storage\Device\Local; use Utopia\Storage\Device\S3; -use Exception; -use Utopia\Database\Validator\Authorization; +use Utopia\Storage\Device\Wasabi; +use Utopia\Storage\Storage; abstract class Worker { @@ -53,7 +54,7 @@ abstract class Worker * @return void * @throws \Exception|\Throwable */ - public function init() + public function init(): void { throw new Exception("Please implement init method in worker"); } @@ -65,7 +66,7 @@ abstract class Worker * @return void * @throws \Exception|\Throwable */ - public function run() + public function run(): void { throw new Exception("Please implement run method in worker"); } @@ -77,7 +78,7 @@ abstract class Worker * @return void * @throws \Exception|\Throwable */ - public function shutdown() + public function shutdown(): void { throw new Exception("Please implement shutdown method in worker"); } @@ -151,35 +152,39 @@ abstract class Worker /** * Register callback. Will be executed when error occurs. * @param callable $callback - * @param Throwable $error - * @return self + * @return void */ public static function error(callable $callback): void { - \array_push(self::$errorCallbacks, $callback); + self::$errorCallbacks[] = $callback; } + /** * Get internal project database * @param string $projectId * @return Database + * @throws Exception */ - protected function getProjectDB(string $projectId): Database + protected function getProjectDB(string $projectId, ?Document $project = null): Database { - $consoleDB = $this->getConsoleDB(); + if ($project === null) { + $consoleDB = $this->getConsoleDB(); - if ($projectId === 'console') { - return $consoleDB; + if ($projectId === 'console') { + return $consoleDB; + } + + /** @var Document $project */ + $project = Authorization::skip(fn() => $consoleDB->getDocument('projects', $projectId)); } - /** @var Document $project */ - $project = Authorization::skip(fn() => $consoleDB->getDocument('projects', $projectId)); - - return $this->getDB(self::DATABASE_PROJECT, $projectId, $project->getInternalId()); + return $this->getDB(self::DATABASE_PROJECT, $projectId, $project->getInternalId(), $project); } /** * Get console database * @return Database + * @throws Exception */ protected function getConsoleDB(): Database { @@ -187,24 +192,35 @@ abstract class Worker } /** - * Get console database - * @param string $type One of (internal, external, console) - * @param string $projectId of internal or external DB + * Get database + * @param string $type One of (project, console) + * @param string $projectId of project or console DB + * @param string $projectInternalId + * @param Document|null $project * @return Database + * @throws Exception */ - private function getDB(string $type, string $projectId = '', string $projectInternalId = ''): Database - { + private function getDB( + string $type, + string $projectId = '', + string $projectInternalId = '', + ?Document $project = null + ): Database { global $register; - $namespace = ''; $sleep = DATABASE_RECONNECT_SLEEP; // overwritten when necessary + if ($project !== null) { + $projectId = $project->getId(); + $projectInternalId = $project->getInternalId(); + } + switch ($type) { case self::DATABASE_PROJECT: if (!$projectId) { throw new \Exception('ProjectID not provided - cannot get database'); } - $namespace = "_{$projectInternalId}"; + $namespace = "_$projectInternalId"; break; case self::DATABASE_CONSOLE: $namespace = "_console"; @@ -212,12 +228,11 @@ abstract class Worker break; default: throw new \Exception('Unknown database type: ' . $type); - break; } $attempts = 0; - do { + while (true) { try { $attempts++; $cache = new Cache(new RedisCache($register->get('cache'))); @@ -225,8 +240,12 @@ abstract class Worker $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); $database->setNamespace($namespace); // Main DB - if (!empty($projectId) && !$database->getDocument('projects', $projectId)->isEmpty()) { - throw new \Exception("Project does not exist: {$projectId}"); + if ( + $project === null + && !empty($projectId) + && !$database->getDocument('projects', $projectId)->isEmpty() + ) { + throw new \Exception("Project does not exist: $projectId"); } if ($type === self::DATABASE_CONSOLE && !$database->exists($database->getDefaultDatabase(), Database::METADATA)) { @@ -235,13 +254,13 @@ abstract class Worker break; // leave loop if successful } catch (\Exception $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); + Console::warning("Database not ready. Retrying connection ($attempts)..."); if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) { throw new \Exception('Failed to connect to database: ' . $e->getMessage()); } sleep($sleep); } - } while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS); + } return $database; } @@ -251,7 +270,7 @@ abstract class Worker * @param string $projectId of the project * @return Device */ - protected function getFunctionsDevice($projectId): Device + protected function getFunctionsDevice(string $projectId): Device { return $this->getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $projectId); } @@ -261,7 +280,7 @@ abstract class Worker * @param string $projectId of the project * @return Device */ - protected function getFilesDevice($projectId): Device + protected function getFilesDevice(string $projectId): Device { return $this->getDevice(APP_STORAGE_UPLOADS . '/app-' . $projectId); } @@ -272,19 +291,24 @@ abstract class Worker * @param string $projectId of the project * @return Device */ - protected function getBuildsDevice($projectId): Device + protected function getBuildsDevice(string $projectId): Device { return $this->getDevice(APP_STORAGE_BUILDS . '/app-' . $projectId); } + protected function getCacheDevice(string $projectId): Device + { + return $this->getDevice(APP_STORAGE_CACHE . '/app-' . $projectId); + } + /** * Get Device based on selected storage environment * @param string $root path of the device * @return Device */ - public function getDevice($root): Device + public function getDevice(string $root): Device { - switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) { + switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) { case Storage::DEVICE_LOCAL: default: return new Local($root); diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index fb7208c569..0e54977d60 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -311,7 +311,7 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'email'; $node['schema']['x-example'] = 'email@example.com'; break; - case 'Appwrite\Network\Validator\URL': + case 'Utopia\Validator\URL': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'url'; $node['schema']['x-example'] = 'https://example.com'; @@ -391,7 +391,7 @@ class OpenAPI3 extends Format case 'Utopia\Validator\Length': $node['schema']['type'] = $validator->getType(); break; - case 'Appwrite\Network\Validator\Host': + case 'Utopia\Validator\Host': $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'url'; $node['schema']['x-example'] = 'https://example.com'; diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index e4d673ed14..ea48d671f2 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -312,7 +312,7 @@ class Swagger2 extends Format $node['format'] = 'email'; $node['x-example'] = 'email@example.com'; break; - case 'Appwrite\Network\Validator\URL': + case 'Utopia\Validator\URL': $node['type'] = $validator->getType(); $node['format'] = 'url'; $node['x-example'] = 'https://example.com'; @@ -393,7 +393,7 @@ class Swagger2 extends Format case 'Utopia\Validator\Length': $node['type'] = $validator->getType(); break; - case 'Appwrite\Network\Validator\Host': + case 'Utopia\Validator\Host': $node['type'] = $validator->getType(); $node['format'] = 'url'; $node['x-example'] = 'https://example.com'; diff --git a/src/Appwrite/Utopia/Response/Model/Locale.php b/src/Appwrite/Utopia/Response/Model/Locale.php index e883a76fd4..fdfa363acd 100644 --- a/src/Appwrite/Utopia/Response/Model/Locale.php +++ b/src/Appwrite/Utopia/Response/Model/Locale.php @@ -42,7 +42,7 @@ class Locale extends Model ]) ->addRule('eu', [ 'type' => self::TYPE_BOOLEAN, - 'description' => 'True if country is part of the Europian Union.', + 'description' => 'True if country is part of the European Union.', 'default' => false, 'example' => false, ]) diff --git a/tests/unit/Network/Validators/DomainTest.php b/tests/unit/Network/Validators/DomainTest.php deleted file mode 100644 index 631ea10753..0000000000 --- a/tests/unit/Network/Validators/DomainTest.php +++ /dev/null @@ -1,43 +0,0 @@ -domain = new Domain(); - } - - public function tearDown(): void - { - $this->domain = null; - } - - public function testIsValid(): void - { - // Assertions - $this->assertEquals(true, $this->domain->isValid('example.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain.example.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain.example-app.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain.example_app.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain-new.example.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain_new.example.com')); - $this->assertEquals(true, $this->domain->isValid('localhost')); - $this->assertEquals(true, $this->domain->isValid('appwrite.io')); - $this->assertEquals(true, $this->domain->isValid('appwrite.org')); - $this->assertEquals(true, $this->domain->isValid('appwrite.org')); - $this->assertEquals(false, $this->domain->isValid(false)); - $this->assertEquals(false, $this->domain->isValid('.')); - $this->assertEquals(false, $this->domain->isValid('..')); - $this->assertEquals(false, $this->domain->isValid('')); - $this->assertEquals(false, $this->domain->isValid(['string', 'string'])); - $this->assertEquals(false, $this->domain->isValid(1)); - $this->assertEquals(false, $this->domain->isValid(1.2)); - } -} diff --git a/tests/unit/Network/Validators/HostTest.php b/tests/unit/Network/Validators/HostTest.php deleted file mode 100755 index 7974bf86a1..0000000000 --- a/tests/unit/Network/Validators/HostTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @version 1.0 RC4 - * @license The MIT License (MIT) - */ - -namespace Tests\Unit\Network\Validators; - -use Appwrite\Network\Validator\Host; -use PHPUnit\Framework\TestCase; - -class HostTest extends TestCase -{ - protected ?Host $host = null; - - public function setUp(): void - { - $this->host = new Host(['appwrite.io', 'subdomain.appwrite.test', 'localhost']); - } - - public function tearDown(): void - { - $this->host = null; - } - - public function testIsValid(): void - { - // Assertions - $this->assertEquals($this->host->isValid('https://appwrite.io/link'), true); - $this->assertEquals($this->host->isValid('https://localhost'), true); - $this->assertEquals($this->host->isValid('localhost'), false); - $this->assertEquals($this->host->isValid('http://subdomain.appwrite.test/path'), true); - $this->assertEquals($this->host->isValid('http://test.subdomain.appwrite.test/path'), false); - $this->assertEquals($this->host->getType(), 'string'); - } -} diff --git a/tests/unit/Network/Validators/IPTest.php b/tests/unit/Network/Validators/IPTest.php deleted file mode 100755 index 57e395111c..0000000000 --- a/tests/unit/Network/Validators/IPTest.php +++ /dev/null @@ -1,87 +0,0 @@ - - * @version 1.0 RC4 - * @license The MIT License (MIT) - */ - -namespace Tests\Unit\Network\Validators; - -use Appwrite\Network\Validator\IP; -use PHPUnit\Framework\TestCase; - -class IPTest extends TestCase -{ - protected ?IP $validator; - - public function setUp(): void - { - $this->validator = new IP(); - } - - public function tearDown(): void - { - $this->validator = null; - } - - public function testIsValidIP(): void - { - $this->assertEquals($this->validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); - $this->assertEquals($this->validator->isValid('109.67.204.101'), true); - $this->assertEquals($this->validator->isValid(23.5), false); - $this->assertEquals($this->validator->isValid('23.5'), false); - $this->assertEquals($this->validator->isValid(null), false); - $this->assertEquals($this->validator->isValid(true), false); - $this->assertEquals($this->validator->isValid(false), false); - $this->assertEquals($this->validator->getType(), 'string'); - } - - public function testIsValidIPALL(): void - { - $this->validator = new IP(IP::ALL); - - // Assertions - $this->assertEquals($this->validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); - $this->assertEquals($this->validator->isValid('109.67.204.101'), true); - $this->assertEquals($this->validator->isValid(23.5), false); - $this->assertEquals($this->validator->isValid('23.5'), false); - $this->assertEquals($this->validator->isValid(null), false); - $this->assertEquals($this->validator->isValid(true), false); - $this->assertEquals($this->validator->isValid(false), false); - } - - public function testIsValidIPV4(): void - { - $this->validator = new IP(IP::V4); - - // Assertions - $this->assertEquals($this->validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), false); - $this->assertEquals($this->validator->isValid('109.67.204.101'), true); - $this->assertEquals($this->validator->isValid(23.5), false); - $this->assertEquals($this->validator->isValid('23.5'), false); - $this->assertEquals($this->validator->isValid(null), false); - $this->assertEquals($this->validator->isValid(true), false); - $this->assertEquals($this->validator->isValid(false), false); - } - - public function testIsValidIPV6(): void - { - $this->validator = new IP(IP::V6); - - // Assertions - $this->assertEquals($this->validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); - $this->assertEquals($this->validator->isValid('109.67.204.101'), false); - $this->assertEquals($this->validator->isValid(23.5), false); - $this->assertEquals($this->validator->isValid('23.5'), false); - $this->assertEquals($this->validator->isValid(null), false); - $this->assertEquals($this->validator->isValid(true), false); - $this->assertEquals($this->validator->isValid(false), false); - } -} diff --git a/tests/unit/Network/Validators/URLTest.php b/tests/unit/Network/Validators/URLTest.php deleted file mode 100755 index bc43f25623..0000000000 --- a/tests/unit/Network/Validators/URLTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @version 1.0 RC4 - * @license The MIT License (MIT) - */ - -namespace Tests\Unit\Network\Validators; - -use Appwrite\Network\Validator\URL; -use PHPUnit\Framework\TestCase; - -class URLTest extends TestCase -{ - protected ?URL $url; - - public function setUp(): void - { - $this->url = new URL(); - } - - public function tearDown(): void - { - $this->url = null; - } - - public function testIsValid(): void - { - $this->assertEquals('Value must be a valid URL', $this->url->getDescription()); - $this->assertEquals(true, $this->url->isValid('http://example.com')); - $this->assertEquals(true, $this->url->isValid('https://example.com')); - $this->assertEquals(true, $this->url->isValid('htts://example.com')); // does not validate protocol - $this->assertEquals(false, $this->url->isValid('example.com')); // though, requires some kind of protocol - $this->assertEquals(false, $this->url->isValid('http:/example.com')); - $this->assertEquals(true, $this->url->isValid('http://exa-mple.com')); - $this->assertEquals(false, $this->url->isValid('htt@s://example.com')); - $this->assertEquals(true, $this->url->isValid('http://www.example.com/foo%2\u00c2\u00a9zbar')); - $this->assertEquals(true, $this->url->isValid('http://www.example.com/?q=%3Casdf%3E')); - $this->assertEquals(true, $this->url->isValid('https://example.com/foo%2\u00c2\u00ä9zbär')); - } - - public function testIsValidAllowedSchemes(): void - { - $this->url = new URL(['http', 'https']); - $this->assertEquals('Value must be a valid URL with following schemes (http, https)', $this->url->getDescription()); - $this->assertEquals(true, $this->url->isValid('http://example.com')); - $this->assertEquals(true, $this->url->isValid('https://example.com')); - $this->assertEquals(false, $this->url->isValid('gopher://www.example.com')); - } -}