Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e623955
Fix TypeError committing bulkDelete with array queries
cursoragent Aug 28, 2026
c7ad4a7
fix(spec): emit FCM service account JSON as an object, not a string
ChiragAgg5k Aug 31, 2026
fc7e0e6
refactor(spec): honour the declared type in the OpenAPI3 parameter de…
ChiragAgg5k Aug 31, 2026
d68394c
test(spec): drop the OpenAPI3 object-type regression test
ChiragAgg5k Aug 31, 2026
0891542
refactor(spec): drop the object cases the parameter default now covers
ChiragAgg5k Aug 31, 2026
d67dd4e
Test transaction replay with array queries
ChiragAgg5k Aug 31, 2026
208c47c
Reject array queries in transaction operations
ChiragAgg5k Aug 31, 2026
8182b12
Enforce transaction query input at validation
ChiragAgg5k Aug 31, 2026
bc9afc0
feat: make the GitLab endpoint configurable
HarshMN2345 Sep 1, 2026
1bae872
fix(oauth2): stop GitLab rejecting a plain-string secret
HarshMN2345 Sep 1, 2026
47d917d
Column boundaries for numeric attributes
fogelito Sep 1, 2026
e157c3e
Merge branch 'main' of https://github.com/appwrite/appwrite into crea…
fogelito Sep 1, 2026
40c61dc
Fix comments
fogelito Sep 1, 2026
1abbbb3
Merge branch 'main' of https://github.com/appwrite/appwrite into crea…
fogelito Sep 2, 2026
92f3990
Merge branch 'main' into cursor/fix-transaction-bulk-query-parse-44d3
ChiragAgg5k Sep 2, 2026
a38e90d
chore: ship the GitLab endpoint variables empty
HarshMN2345 Sep 2, 2026
938bbae
Merge pull request #13437 from appwrite/feat/gitlab-endpoint-env
HarshMN2345 Sep 2, 2026
47772e2
Merge pull request #13441 from appwrite/create-collection-integer-for…
abnegate Sep 2, 2026
4c4b59e
Merge pull request #13407 from appwrite/cursor/fix-transaction-bulk-q…
ChiragAgg5k Sep 2, 2026
aeeca78
Merge pull request #13433 from ChiragAgg5k/fix/fcm-service-account-js…
ChiragAgg5k Sep 2, 2026
97da563
fix(databases): expose transactionId on DocumentsDB and VectorsDB cre…
atharvadeosthale Sep 2, 2026
bdd63a8
refactor(queues): remove intermediate publisher resources (#13449)
loks0n Sep 2, 2026
ab4ce26
test(databases): cover transactionId on DocumentsDB and VectorsDB create
cursoragent Sep 2, 2026
4423904
test(databases): bootstrap init.php in create transactionId unit test
cursoragent Sep 2, 2026
4d1487f
test(databases): drop HTTP create unit test
cursoragent Sep 2, 2026
b3d6846
Merge pull request #13451 from appwrite/fix-documentsdb-vectorsdb-cre…
ArnabChatterjee20k Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ _APP_CONSOLE_GITHUB_APP_ID=
_APP_CONSOLE_GITHUB_SECRET=
_APP_CONSOLE_GITLAB_APP_ID=
_APP_CONSOLE_GITLAB_SECRET=
_APP_CONSOLE_GITLAB_ENDPOINT=
_APP_CONSOLE_BITBUCKET_APP_ID=
_APP_CONSOLE_BITBUCKET_SECRET=
_APP_CONSOLE_GOOGLE_APP_ID=
Expand Down Expand Up @@ -181,6 +182,7 @@ _APP_VCS_GITEA_BROWSER_ENDPOINT=http://localhost:9515
_APP_VCS_GITEA_CLIENT_ID=
_APP_VCS_GITEA_CLIENT_SECRET=
_APP_VCS_GITEA_WEBHOOK_SECRET=gitea-webhook-secret
_APP_VCS_GITLAB_ENDPOINT=
_APP_VCS_GITLAB_CLIENT_ID=
_APP_VCS_GITLAB_CLIENT_SECRET=
_APP_VCS_GITLAB_WEBHOOK_SECRET=
Expand Down
8 changes: 7 additions & 1 deletion app/config/console.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@
'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''),
'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', ''),
'gitlabEnabled' => true,
'gitlabSecret' => System::getEnv('_APP_CONSOLE_GITLAB_SECRET', ''),
// Auth\OAuth2\Gitlab reads its endpoint out of a JSON-encoded secret, so
// _APP_CONSOLE_GITLAB_ENDPOINT travels with the credential. Stays empty when
// unconfigured so account.php keeps reporting the provider as disabled.
'gitlabSecret' => empty(System::getEnv('_APP_CONSOLE_GITLAB_SECRET', '')) ? '' : \json_encode([
'clientSecret' => System::getEnv('_APP_CONSOLE_GITLAB_SECRET', ''),
'endpoint' => System::getEnv('_APP_CONSOLE_GITLAB_ENDPOINT', 'https://gitlab.com'),
]),
'gitlabAppid' => System::getEnv('_APP_CONSOLE_GITLAB_APP_ID', ''),
'bitbucketEnabled' => true,
'bitbucketSecret' => System::getEnv('_APP_CONSOLE_BITBUCKET_SECRET', ''),
Expand Down
18 changes: 18 additions & 0 deletions app/config/variables.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,15 @@
'question' => '',
'filter' => ''
],
[
'name' => '_APP_CONSOLE_GITLAB_ENDPOINT',
'description' => 'URL of the GitLab instance used for signing in to the Appwrite console, for self-hosted GitLab. Defaults to https://gitlab.com when unset. This is separate from _APP_VCS_GITLAB_ENDPOINT, which powers repository integration rather than console sign-in.',
'introduction' => '2.0.0',
'default' => '',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_CONSOLE_BITBUCKET_APP_ID',
'description' => 'Bitbucket OAuth consumer key used for signing in to the Appwrite console. You can find it in your Bitbucket workspace settings under OAuth consumers. This is separate from _APP_VCS_BITBUCKET_CLIENT_ID, which powers repository integration rather than console sign-in.',
Expand Down Expand Up @@ -1572,6 +1581,15 @@
'question' => '',
'filter' => ''
],
[
'name' => '_APP_VCS_GITLAB_ENDPOINT',
'description' => 'URL of your self-hosted GitLab instance, reachable from the Appwrite server and the browser. Defaults to https://gitlab.com when unset.',
'introduction' => '2.0.0',
'default' => '',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_VCS_GITLAB_CLIENT_ID',
'description' => 'GitLab OAuth2 application client ID. You can generate one in your GitLab instance under Settings > Applications.',
Expand Down
5 changes: 3 additions & 2 deletions app/config/vcs.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Appwrite\Auth\OAuth2\Gitea as OAuth2Gitea;
use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Auth\OAuth2\Gitlab as OAuth2Gitlab;
use Utopia\System\System;
use Utopia\VCS\Adapter\Git\Bitbucket;
use Utopia\VCS\Adapter\Git\Gitea;
use Utopia\VCS\Adapter\Git\GitHub;
Expand Down Expand Up @@ -55,8 +56,8 @@
'clientSecret' => $clientSecret,
'endpoint' => $endpoint,
]), ''),
// Only official gitlab.com is supported -- fixed, not configurable.
'endpoint' => 'https://gitlab.com',
// Defaults to gitlab.com; self-hosted installs point _APP_VCS_GITLAB_ENDPOINT at their own instance.
'endpoint' => System::getEnv('_APP_VCS_GITLAB_ENDPOINT', 'https://gitlab.com'),
'variables' => [
'clientId' => ['required' => true, 'envVariable' => '_APP_VCS_GITLAB_CLIENT_ID'],
'clientSecret' => ['required' => true, 'envVariable' => '_APP_VCS_GITLAB_CLIENT_SECRET'],
Expand Down
20 changes: 3 additions & 17 deletions app/init/resources.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,6 @@

$container->set('publisher', fn (Group $pools) => new BrokerPool(publisher: $pools->get('publisher')), ['pools']);

$container->set('publisherDatabases', fn (Publisher $publisher) => $publisher, ['publisher']);

$container->set('publisherFunctions', fn (Publisher $publisher) => $publisher, ['publisher']);

$container->set('publisherMigrations', fn (Publisher $publisher) => $publisher, ['publisher']);

$container->set('publisherMails', fn (Publisher $publisher) => $publisher, ['publisher']);

$container->set('publisherDeletes', fn (Publisher $publisher) => $publisher, ['publisher']);

$container->set('publisherMessaging', fn (Publisher $publisher) => $publisher, ['publisher']);

$container->set('publisherWebhooks', fn (Publisher $publisher) => $publisher, ['publisher']);

$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
$publisher,
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
Expand Down Expand Up @@ -204,10 +190,10 @@
new Queue(System::getEnv('_APP_JOBS_QUEUE_NAME', Event::JOBS_QUEUE_NAME))
), ['publisher']);

$container->set('publisherForDatabase', fn (Publisher $publisherDatabases) => new DatabasePublisher(
$publisherDatabases,
$container->set('publisherForDatabase', fn (Publisher $publisher) => new DatabasePublisher(
$publisher,
new Queue(System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME))
), ['publisherDatabases']);
), ['publisher']);

$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
Expand Down
8 changes: 4 additions & 4 deletions app/init/resources/request.php
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@
return;
}, ['user', 'store', 'proofForToken']);

$context->set('dbForProject', function (DatabaseFactory $databaseFactory, Database $dbForPlatform, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Request $request) {
$context->set('dbForProject', function (DatabaseFactory $databaseFactory, Database $dbForPlatform, Document $project, Response $response, Publisher $publisher, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Request $request) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
Expand Down Expand Up @@ -796,7 +796,7 @@
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForWebhooksClone = clone $queueForWebhooks;
$queueForRealtime = new Realtime();

$database
Expand All @@ -811,15 +811,15 @@
$response,
$queueForEventsClone->from($queueForEvents),
$publisherForFunctions,
$queueForWebhooks->from($queueForEvents),
$queueForWebhooksClone->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));

return $database;
}, ['databaseFactory', 'dbForPlatform', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'publisherForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'request']);
}, ['databaseFactory', 'dbForPlatform', 'project', 'response', 'publisher', 'queueForEvents', 'publisherForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'request']);

$context->set('schema', function ($utopia, $dbForProject, $authorization) {

Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ services:
- _APP_CONSOLE_GITHUB_SECRET
- _APP_CONSOLE_GITLAB_APP_ID
- _APP_CONSOLE_GITLAB_SECRET
- _APP_CONSOLE_GITLAB_ENDPOINT
- _APP_CONSOLE_BITBUCKET_APP_ID
- _APP_CONSOLE_BITBUCKET_SECRET
- _APP_CONSOLE_GOOGLE_APP_ID
Expand Down Expand Up @@ -229,6 +230,7 @@ services:
- _APP_VCS_GITEA_CLIENT_ID
- _APP_VCS_GITEA_CLIENT_SECRET
- _APP_VCS_GITEA_WEBHOOK_SECRET
- _APP_VCS_GITLAB_ENDPOINT
- _APP_VCS_GITLAB_CLIENT_ID
- _APP_VCS_GITLAB_CLIENT_SECRET
- _APP_VCS_GITLAB_WEBHOOK_SECRET
Expand Down Expand Up @@ -482,6 +484,7 @@ services:
- _APP_VCS_GITEA_CLIENT_ID
- _APP_VCS_GITEA_CLIENT_SECRET
- _APP_VCS_GITEA_WEBHOOK_SECRET
- _APP_VCS_GITLAB_ENDPOINT
- _APP_VCS_GITLAB_CLIENT_ID
- _APP_VCS_GITLAB_CLIENT_SECRET
- _APP_VCS_GITLAB_WEBHOOK_SECRET
Expand Down Expand Up @@ -824,6 +827,7 @@ services:
- _APP_VCS_GITEA_CLIENT_ID
- _APP_VCS_GITEA_CLIENT_SECRET
- _APP_VCS_GITEA_WEBHOOK_SECRET
- _APP_VCS_GITLAB_ENDPOINT
- _APP_VCS_GITLAB_CLIENT_ID
- _APP_VCS_GITLAB_CLIENT_SECRET
- _APP_VCS_GITLAB_WEBHOOK_SECRET
Expand Down
17 changes: 12 additions & 5 deletions src/Appwrite/Auth/OAuth2/Gitlab.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
* Shared with the "Sign in with GitLab" account-login OAuth2 provider, which
* stores its secret as JSON ({"clientSecret": "...", "endpoint": "..."}) to
* support self-hosted GitLab per-project. The VCS flow (see app/config/vcs.php)
* only supports official gitlab.com, but still encodes to that same JSON
* shape so getAppSecret()/getEndpoint() stay correct for both consumers.
* and the console project (see app/config/console.php) encode to that same JSON
* shape, taking their endpoint from _APP_VCS_GITLAB_ENDPOINT and
* _APP_CONSOLE_GITLAB_ENDPOINT respectively.
*/
class Gitlab extends OAuth2
{
Expand Down Expand Up @@ -230,7 +231,8 @@ protected function getUser(string $accessToken): array
}

/**
* Decode the JSON stored in appSecret
* Decode the JSON stored in appSecret.
* Falls back to treating the raw string as the client secret for backwards compatibility.
*
* @return array
*/
Expand All @@ -239,8 +241,13 @@ protected function getAppSecret(): array
try {
$secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
} catch (\Throwable $th) {
throw new \Exception('Invalid secret');
return ['clientSecret' => $this->appSecret];
}

if (!\is_array($secret)) {
return ['clientSecret' => $this->appSecret];
}

return $secret;
}

Expand All @@ -255,6 +262,6 @@ protected function getEndpoint(): string
$defaultEndpoint = 'https://gitlab.com';
$secret = $this->getAppSecret();
$endpoint = $secret['endpoint'] ?? $defaultEndpoint;
return empty($endpoint) ? $defaultEndpoint : $endpoint;
return empty($endpoint) ? $defaultEndpoint : \rtrim($endpoint, '/');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ public function action(string $databaseId, string $collectionId, string $key, ?b
throw new Exception($this->getInvalidValueException(), $validator->getDescription());
}

$size = $max > 2147483647 ? 8 : 4;
// The 4 byte column only holds a range that fits INT32. min counts: a
// column bounded below -2147483648 has to be able to store that value,
// and with min left out the bound is PHP_INT_MIN.
$size = $min >= -2147483648 && $max <= 2147483647 ? 4 : 8;

$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,16 +297,24 @@ protected function buildAttributeDocument(
$formatOptions = ['elements' => $attribute['elements']];
}

if (isset($attribute['min']) || isset($attribute['max'])) {
// The dedicated endpoints store a range on every numeric attribute, falling
// back to the full width of the type, so omitting min/max here has to produce
// the same document rather than one with no range at all.
if (\in_array($type, [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) {
$isFloat = $type === Database::VAR_FLOAT;

$format = match($type) {
Database::VAR_INTEGER => APP_DATABASE_ATTRIBUTE_INT_RANGE,
Database::VAR_BIGINT => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE,
default => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
};

$min = $attribute['min'] ?? ($isFloat ? -\PHP_FLOAT_MAX : \PHP_INT_MIN);
$max = $attribute['max'] ?? ($isFloat ? \PHP_FLOAT_MAX : \PHP_INT_MAX);

$formatOptions = [
'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
'min' => $isFloat ? \floatval($min) : $min,
'max' => $isFloat ? \floatval($max) : $max,
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public function __construct()
new Parameter('documentId', optional: false),
new Parameter('data', optional: false),
new Parameter('permissions', optional: true),
new Parameter('transactionId', optional: true),
]
),
new Method(
Expand All @@ -96,6 +97,7 @@ public function __construct()
new Parameter('databaseId', optional: false),
new Parameter('collectionId', optional: false),
new Parameter('documents', optional: false),
new Parameter('transactionId', optional: true),
]
)
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public function __construct()
new Parameter('documentId', optional: false),
new Parameter('data', optional: false),
new Parameter('permissions', optional: true),
new Parameter('transactionId', optional: true),
]
),
new Method(
Expand All @@ -90,6 +91,7 @@ public function __construct()
new Parameter('databaseId', optional: false),
new Parameter('collectionId', optional: false),
new Parameter('documents', optional: false),
new Parameter('transactionId', optional: true),
]
)
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protected function createOAuth2(string $callback, array $state): OAuth2
System::getEnv('_APP_VCS_GITLAB_CLIENT_ID', ''),
\json_encode([
'clientSecret' => System::getEnv('_APP_VCS_GITLAB_CLIENT_SECRET', ''),
'endpoint' => 'https://gitlab.com',
'endpoint' => System::getEnv('_APP_VCS_GITLAB_ENDPOINT', 'https://gitlab.com'),
]),
$callback,
$state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ protected function createOAuth2(string $callback): OAuth2
System::getEnv('_APP_VCS_GITLAB_CLIENT_ID', ''),
\json_encode([
'clientSecret' => System::getEnv('_APP_VCS_GITLAB_CLIENT_SECRET', ''),
'endpoint' => 'https://gitlab.com',
'endpoint' => System::getEnv('_APP_VCS_GITLAB_ENDPOINT', 'https://gitlab.com'),
]),
$callback
);
Expand Down
11 changes: 9 additions & 2 deletions src/Appwrite/SDK/Specification/Format/OpenAPI3.php
Original file line number Diff line number Diff line change
Expand Up @@ -731,9 +731,9 @@ public function parse(): array
$node['schema']['format'] = 'url';
$node['schema']['example'] = ($param['example'] ?? '') !== '' ? $param['example'] : 'https://example.com';
break;
case \Utopia\Validator\JSON::class:
case \Utopia\Validator\JSON\ObjectValidator::class:
case \Utopia\Validator\Assoc::class:
// Assoc reports TYPE_ARRAY, so only an explicit case publishes
// it as an object. TYPE_OBJECT is handled by the default.
$node['schema']['type'] = 'object';
$node['schema']['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['example'] = ($param['example'] ?? '') !== '' ? $param['example'] : '{}';
Expand Down Expand Up @@ -943,6 +943,13 @@ public function parse(): array
}
break;
default:
if ($validator->getType() === Validator::TYPE_OBJECT) {
$node['schema']['type'] = 'object';
$node['schema']['default'] = empty($param['default']) ? new \stdClass() : $param['default'];
$node['schema']['example'] = ($param['example'] ?? '') !== '' ? $param['example'] : '{}';
break;
}

$node['schema']['type'] = 'string';
if (($param['example'] ?? '') !== '') {
$node['schema']['example'] = $param['example'];
Expand Down
16 changes: 16 additions & 0 deletions src/Appwrite/Utopia/Database/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class Attribute
Database::VAR_TEXT => 65535,
Database::VAR_MEDIUMTEXT => 16777215,
Database::VAR_LONGTEXT => 2147483647,
// Bytes, the widths createBigIntColumn and createFloatColumn hardcode.
// Every adapter maps these two types without consulting the size.
Database::VAR_BIGINT => 8,
Database::VAR_FLOAT => 0,
];

/**
Expand Down Expand Up @@ -91,6 +95,18 @@ public static function resolve(array $attribute): array
$size = self::FORMAT_SIZES[$format] ?? $size;
}

if ($type === Database::VAR_INTEGER) {
// Same width createIntegerColumn picks. That endpoint takes no size at
// all, so a size sent inline is ignored rather than left to promise a
// range the column cannot hold: the 4 byte column only holds a range
// that fits INT32, and a bound left out means the int64 edge, which
// does not.
$min = $attribute['min'] ?? \PHP_INT_MIN;
$max = $attribute['max'] ?? \PHP_INT_MAX;
$fitsInt32 = \is_int($min) && \is_int($max) && $min >= -2147483648 && $max <= 2147483647;
$size = $fitsInt32 ? 4 : 8;
}

return [
'type' => $type,
'format' => $format,
Expand Down
Loading
Loading