1
0
Fork 0
mirror of synced 2024-07-02 05:00:33 +12:00

Merge branch 'master' of https://github.com/appwrite/appwrite into feat-4803-flutter-web-platform-type

This commit is contained in:
Torsten Dittmann 2023-01-27 13:24:41 +01:00
commit 24a7caf936
32 changed files with 190 additions and 716 deletions

View file

@ -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?

Binary file not shown.

View file

@ -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' => '',
],

View file

@ -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;

View file

@ -1,7 +1,7 @@
<?php
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\URL;
use Utopia\Validator\URL;
use Appwrite\URL\URL as URLParse;
use Appwrite\Utopia\Response;
use chillerlan\QRCode\QRCode;

View file

@ -33,8 +33,8 @@ use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Locale\Locale;
use Appwrite\Auth\Auth;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
use Utopia\Validator\IP;
use Utopia\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;

View file

@ -6,9 +6,9 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Validator\Event;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\Domain as DomainValidator;
use Utopia\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\Origin;
use Appwrite\Network\Validator\URL;
use Utopia\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Adapters\TimeLimit;

View file

@ -65,7 +65,7 @@ App::post('/v1/storage/buckets')
->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;

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -509,6 +509,7 @@ services:
entrypoint: worker-messaging
<<: *x-logging
container_name: appwrite-worker-messaging
restart: unless-stopped
networks:
- appwrite
depends_on:

View file

@ -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,

View file

@ -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
{

View file

@ -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.*",

44
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "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"
}

View file

@ -554,6 +554,7 @@ services:
entrypoint: worker-messaging
<<: *x-logging
container_name: appwrite-worker-messaging
restart: unless-stopped
image: appwrite-dev
networks:
- appwrite

View file

@ -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();

View file

@ -1,78 +0,0 @@
<?php
namespace Appwrite\Network\Validator;
use Utopia\Validator;
/**
* Domain
*
* Validate that an variable is a valid domain address
*
* @package Utopia\Validator
*/
class Domain extends Validator
{
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Value must be a valid domain';
}
/**
* Is valid
*
* Validation will pass when $value is valid domain.
*
* Validates domain names against RFC 1034, RFC 1035, RFC 952, RFC 1123, RFC 2732, RFC 2181, and RFC 1123.
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
if (empty($value)) {
return false;
}
if (!is_string($value)) {
return false;
}
if (\filter_var($value, FILTER_VALIDATE_DOMAIN) === false) {
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;
}
}

View file

@ -1,84 +0,0 @@
<?php
namespace Appwrite\Network\Validator;
use Utopia\Validator\Hostname;
use Utopia\Validator;
/**
* Host
*
* Validate that a host is allowed from given whitelisted hosts list
*
* @package Utopia\Validator
*/
class Host extends Validator
{
protected $whitelist = [];
/**
* @param array $whitelist
*/
public function __construct(array $whitelist)
{
$this->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;
}
}

View file

@ -1,114 +0,0 @@
<?php
namespace Appwrite\Network\Validator;
use Exception;
use Utopia\Validator;
/**
* IP
*
* Validate that an variable is a valid IP address
*
* @package Utopia\Validator
*/
class IP extends Validator
{
public const ALL = 'all';
public const V4 = 'ipv4';
public const V6 = 'ipv6';
/**
* @var string
*/
protected $type = self::ALL;
/**
* Constructor
*
* Set a the type of IP check.
*
* @param string $type
*/
public function __construct(string $type = self::ALL)
{
if (!in_array($type, [self::ALL, self::V4, self::V6])) {
throw new Exception('Unsupported IP type');
}
$this->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;
}
}

View file

@ -1,92 +0,0 @@
<?php
namespace Appwrite\Network\Validator;
use Utopia\Validator;
/**
* URL
*
* Validate that an variable is a valid URL
*
* @package Appwrite\Network\Validator
*/
class URL extends Validator
{
protected array $allowedSchemes;
/**
* @param array $allowedSchemes
*/
public function __construct(array $allowedSchemes = [])
{
$this->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;
}
}

View file

@ -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);

View file

@ -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';

View file

@ -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';

View file

@ -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,
])

View file

@ -1,43 +0,0 @@
<?php
namespace Tests\Unit\Network\Validators;
use Appwrite\Network\Validator\Domain;
use PHPUnit\Framework\TestCase;
class DomainTest extends TestCase
{
protected ?Domain $domain = null;
public function setUp(): void
{
$this->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));
}
}

View file

@ -1,44 +0,0 @@
<?php
/**
* Utopia PHP Framework
*
* @package Framework
* @subpackage Tests
*
* @link https://github.com/utopia-php/framework
* @author Appwrite Team <team@appwrite.io>
* @version 1.0 RC4
* @license The MIT License (MIT) <http://www.opensource.org/licenses/mit-license.php>
*/
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');
}
}

View file

@ -1,87 +0,0 @@
<?php
/**
* Utopia PHP Framework
*
* @package Framework
* @subpackage Tests
*
* @link https://github.com/utopia-php/framework
* @author Appwrite Team <team@appwrite.io>
* @version 1.0 RC4
* @license The MIT License (MIT) <http://www.opensource.org/licenses/mit-license.php>
*/
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);
}
}

View file

@ -1,57 +0,0 @@
<?php
/**
* Utopia PHP Framework
*
* @package Framework
* @subpackage Tests
*
* @link https://github.com/utopia-php/framework
* @author Appwrite Team <team@appwrite.io>
* @version 1.0 RC4
* @license The MIT License (MIT) <http://www.opensource.org/licenses/mit-license.php>
*/
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'));
}
}