From 69e6c0afc0954c2a2ee531a3e2a1df5b6e5d6c30 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 28 Jan 2026 14:53:24 +0200 Subject: [PATCH 01/19] getCursorQueries --- app/controllers/api/account.php | 12 +--- app/controllers/api/messaging.php | 55 +++++-------------- app/controllers/api/migrations.php | 12 +--- app/controllers/api/teams.php | 25 ++------- app/controllers/api/users.php | 39 +++++-------- app/controllers/api/vcs.php | 13 ++--- .../Databases/Collections/Documents/XList.php | 10 +--- .../Databases/Collections/Indexes/XList.php | 11 +--- .../Http/Databases/Collections/XList.php | 11 +--- .../Databases/Http/Databases/XList.php | 13 ++--- .../Functions/Http/Deployments/XList.php | 12 +--- .../Functions/Http/Executions/XList.php | 12 +--- .../Functions/Http/Functions/XList.php | 12 +--- .../Modules/Projects/Http/Projects/XList.php | 12 +--- .../Modules/Proxy/Http/Rules/XList.php | 12 +--- .../Modules/Sites/Http/Deployments/XList.php | 12 +--- .../Modules/Sites/Http/Logs/XList.php | 12 +--- .../Modules/Sites/Http/Sites/XList.php | 12 +--- .../Storage/Http/Buckets/Files/XList.php | 12 +--- .../Modules/Storage/Http/Buckets/XList.php | 12 +--- .../Http/Tokens/Buckets/Files/XList.php | 12 ++-- 21 files changed, 91 insertions(+), 242 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index ce655bfe18..2f30633859 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -4574,16 +4574,10 @@ App::get('/v1/account/identities') $queries[] = Query::equal('userInternalId', [$user->getSequence()]); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 6ac36fe3c0..aa9fe50391 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -1087,15 +1087,10 @@ App::get('/v1/messaging/providers') $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); - if ($cursor) { + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); @@ -2496,15 +2491,10 @@ App::get('/v1/messaging/topics') $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); - if ($cursor) { + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); @@ -2904,15 +2894,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers') $queries[] = Query::equal('topicInternalId', [$topic->getSequence()]); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); - if ($cursor) { + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); @@ -3719,15 +3704,10 @@ App::get('/v1/messaging/messages') $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); - if ($cursor) { + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); @@ -3899,15 +3879,10 @@ App::get('/v1/messaging/messages/:messageId/targets') $queries[] = Query::equal('$id', $targetIDs); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); - if ($cursor) { + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 1a17853577..379a90a92b 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -629,16 +629,10 @@ App::get('/v1/migrations') $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 2cee394a9c..2bff998bda 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -191,16 +191,10 @@ App::get('/v1/teams') $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); @@ -882,22 +876,15 @@ App::get('/v1/teams/:teamId/memberships') // Set internal queries $queries[] = Query::equal('teamInternalId', [$team->getSequence()]); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); } - $membershipId = $cursor->getValue(); $cursorDocument = $dbForProject->getDocument('memberships', $membershipId); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index a963284538..be0a73d1e1 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -675,16 +675,10 @@ App::get('/v1/users') $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); @@ -1042,14 +1036,11 @@ App::get('/v1/users/:userId/targets') throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } $queries[] = Query::equal('userId', [$userId]); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); @@ -1106,15 +1097,11 @@ App::get('/v1/users/identities') if (!empty($search)) { $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 2bb9c17fd3..65753fd660 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -1649,15 +1649,10 @@ App::get('/v1/vcs/installations') $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index ff94e67b02..295b78a114 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -99,16 +99,10 @@ class XList extends Action throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - + $cursor = Query::getCursorQueries($queries, false); $cursor = \reset($cursor); - if ($cursor) { + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php index 90826ffbe3..b9705515eb 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/XList.php @@ -98,15 +98,10 @@ class XList extends Action Query::equal('collectionId', [$collectionId]), ); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); - if ($cursor) { + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php index c23286f3cd..87e0720089 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php @@ -89,15 +89,10 @@ class XList extends Action $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); - if ($cursor) { + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php index e3dd46839a..ff2f6a574b 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php @@ -73,15 +73,10 @@ class XList extends Action $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php index 55711495e9..ea31af8580 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php @@ -95,16 +95,10 @@ class XList extends Base $queries[] = Query::equal('resourceInternalId', [$function->getSequence()]); $queries[] = Query::equal('resourceType', ['functions']); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php index ff381e1f3d..f82207eaee 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php @@ -91,16 +91,10 @@ class XList extends Base $queries[] = Query::equal('resourceInternalId', [$function->getSequence()]); $queries[] = Query::equal('resourceType', ['functions']); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/XList.php index aaf288bee7..8e71258501 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/XList.php @@ -78,16 +78,10 @@ class XList extends Base $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index 7269582a15..fe29fdc4da 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -86,16 +86,10 @@ class XList extends Action $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php index e160b71060..19daf8c8d2 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php @@ -81,16 +81,10 @@ class XList extends Action $queries[] = Query::equal('projectInternalId', [$project->getSequence()]); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php index 73e5ea4d77..a01f54a6ff 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php @@ -95,16 +95,10 @@ class XList extends Base $queries[] = Query::equal('resourceInternalId', [$site->getSequence()]); $queries[] = Query::equal('resourceType', ['sites']); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Logs/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Logs/XList.php index e933a8d12b..38c6d4b29a 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Logs/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Logs/XList.php @@ -80,16 +80,10 @@ class XList extends Base $queries[] = Query::equal('resourceInternalId', [$site->getSequence()]); $queries[] = Query::equal('resourceType', ['sites']); - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/XList.php index 8daf3eb063..2571043363 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/XList.php @@ -73,16 +73,10 @@ class XList extends Base $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php index 3663b56fab..6de360ae0e 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php @@ -101,16 +101,10 @@ class XList extends Action $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/XList.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/XList.php index 601d9b5321..8f2cd9bbac 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/XList.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/XList.php @@ -82,16 +82,10 @@ class XList extends Action $queries[] = Query::search('search', $search); } - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + if ($cursor !== false) { $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php index 13da92cbc6..947801b465 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php @@ -69,13 +69,11 @@ class XList extends Action $queries = Query::parseQueries($queries); $queries[] = Query::equal('resourceType', [TOKENS_RESOURCE_TYPE_FILES]); $queries[] = Query::equal('resourceInternalId', [$bucket->getSequence() . ':' . $file->getSequence()]); - // Get cursor document if there was a cursor query - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { $tokenId = $cursor->getValue(); $cursorDocument = $dbForProject->getDocument('resourceTokens', $tokenId); From f35aad0816cfa092bc8c4743196ef07ea97a479d Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 28 Jan 2026 14:55:09 +0200 Subject: [PATCH 02/19] Remove extra line --- app/controllers/api/vcs.php | 1 - src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php | 1 - 2 files changed, 2 deletions(-) diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 65753fd660..9feaa4d38d 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -1653,7 +1653,6 @@ App::get('/v1/vcs/installations') $cursor = \reset($cursor); if ($cursor !== false) { - $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php index ff2f6a574b..f9589c4469 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/XList.php @@ -77,7 +77,6 @@ class XList extends Action $cursor = \reset($cursor); if ($cursor !== false) { - $validator = new Cursor(); if (!$validator->isValid($cursor)) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); From 95ce53b66eab24f1a0a7eebeeb00800270c40f4e Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 28 Jan 2026 15:15:33 +0200 Subject: [PATCH 03/19] Exception --- .../Tokens/Http/Tokens/Buckets/Files/XList.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php index 947801b465..579c6e7100 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/XList.php @@ -2,18 +2,18 @@ namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files; -use Appwrite\Extend\Exception as ExtendException; +use Appwrite\Extend\Exception; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\FileTokens; use Appwrite\Utopia\Response; -use Exception; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; @@ -74,11 +74,16 @@ class XList extends Action $cursor = \reset($cursor); if ($cursor !== false) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + $tokenId = $cursor->getValue(); $cursorDocument = $dbForProject->getDocument('resourceTokens', $tokenId); if ($cursorDocument->isEmpty()) { - throw new Exception(ExtendException::GENERAL_CURSOR_NOT_FOUND, "File token '{$tokenId}' for the 'cursor' value not found."); + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "File token '{$tokenId}' for the 'cursor' value not found."); } $cursor->setValue($cursorDocument); From 0003443c776db6d7daf390bcfc91e06626eb648d Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Wed, 4 Feb 2026 19:23:27 +0530 Subject: [PATCH 04/19] Create rule in SSL task --- src/Appwrite/Platform/Tasks/SSL.php | 88 +++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SSL.php b/src/Appwrite/Platform/Tasks/SSL.php index 651cb4de11..9ba8d65dd9 100644 --- a/src/Appwrite/Platform/Tasks/SSL.php +++ b/src/Appwrite/Platform/Tasks/SSL.php @@ -4,7 +4,12 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Certificate; use Utopia\CLI\Console; +use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Helpers\ID; +use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; +use Utopia\Domains\Domain; use Utopia\Platform\Action; use Utopia\System\System; use Utopia\Validator\Boolean; @@ -22,22 +27,85 @@ class SSL extends Action $this ->desc('Validate server certificates') ->param('domain', System::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true) - ->param('skip-check', true, new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true) + ->param('skip-check', 'true', new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true) + ->inject('console') + ->inject('dbForPlatform') ->inject('queueForCertificates') + ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, bool|string $skipCheck, Certificate $queueForCertificates): void + public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, Authorization $authorization): void { - $skipCheck = \strval($skipCheck) === 'true'; + $domain = new Domain(!empty($domain) ? $domain : ''); + if (!$domain->isKnown() || $domain->isTest()) { + Console::error('Domain is not known or is a test domain: ' . $domain->get()); + return; + } - Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain); + $authorization->skip(function () use ($skipCheck, $console, $dbForPlatform, $domain, $queueForCertificates) { + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; + $skipCheck = \strval($skipCheck) === 'true'; - $queueForCertificates - ->setDomain(new Document([ - 'domain' => $domain - ])) - ->setSkipRenewCheck($skipCheck) - ->trigger(); + $rule = $isMd5 + ? $dbForPlatform->getDocument('rules', md5($domain->get())) + : $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain->get()]), + ]); + + if (!$rule->isEmpty()) { + Console::warning('Rule ' . $rule->getId() . ' already exists for domain: ' . $domain->get()); + return; + } + + $owner = ''; + + // Mark owner as Appwrite if its appwrite-owned domain + $appwriteDomains = []; + $appwriteDomainEnvs = [ + System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), + System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + System::getEnv('_APP_DOMAIN_SITES', ''), + ]; + foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { + foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { + if (empty($appwriteDomain)) { + continue; + } + $appwriteDomains[] = $appwriteDomain; + } + } + + foreach ($appwriteDomains as $appwriteDomain) { + if (\str_ends_with($domain->get(), $appwriteDomain)) { + $owner = 'Appwrite'; + break; + } + } + + $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); + $rule = $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'domain' => $domain->get(), + 'type' => 'api', + 'status' => RULE_STATUS_CERTIFICATE_GENERATING, + 'projectId' => $console->getId(), + 'projectInternalId' => $console->getSequence(), + 'search' => implode(' ', [$ruleId, $domain->get()]), + 'owner' => $owner, + 'region' => $console->getAttribute('region') + ])); + + Console::success('Rule ' . $rule->getId() . ' created for domain: ' . $domain->get()); + + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $domain->get() + ])) + ->setSkipRenewCheck($skipCheck) + ->trigger(); + + Console::success('Scheduled a job to issue a TLS certificate for domain: ' . $domain->get()); + }); } } From a30f12cd66b42525339329361f637c2f498f7ed7 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Wed, 4 Feb 2026 19:31:00 +0530 Subject: [PATCH 05/19] tiny --- src/Appwrite/Platform/Tasks/SSL.php | 106 +++++++++++++--------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SSL.php b/src/Appwrite/Platform/Tasks/SSL.php index 9ba8d65dd9..514e2b670a 100644 --- a/src/Appwrite/Platform/Tasks/SSL.php +++ b/src/Appwrite/Platform/Tasks/SSL.php @@ -8,7 +8,6 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\Domains\Domain; use Utopia\Platform\Action; use Utopia\System\System; @@ -31,11 +30,10 @@ class SSL extends Action ->inject('console') ->inject('dbForPlatform') ->inject('queueForCertificates') - ->inject('authorization') ->callback($this->action(...)); } - public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, Authorization $authorization): void + public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $queueForCertificates): void { $domain = new Domain(!empty($domain) ? $domain : ''); if (!$domain->isKnown() || $domain->isTest()) { @@ -43,69 +41,67 @@ class SSL extends Action return; } - $authorization->skip(function () use ($skipCheck, $console, $dbForPlatform, $domain, $queueForCertificates) { - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $skipCheck = \strval($skipCheck) === 'true'; + $skipCheck = \strval($skipCheck) === 'true'; + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $rule = $isMd5 - ? $dbForPlatform->getDocument('rules', md5($domain->get())) - : $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain->get()]), - ]); + $rule = $isMd5 + ? $dbForPlatform->getDocument('rules', md5($domain->get())) + : $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain->get()]), + ]); - if (!$rule->isEmpty()) { - Console::warning('Rule ' . $rule->getId() . ' already exists for domain: ' . $domain->get()); - return; - } + if (!$rule->isEmpty()) { + Console::warning('Rule ' . $rule->getId() . ' already exists for domain: ' . $domain->get()); + return; + } - $owner = ''; + $owner = ''; - // Mark owner as Appwrite if its appwrite-owned domain - $appwriteDomains = []; - $appwriteDomainEnvs = [ - System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), - System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), - System::getEnv('_APP_DOMAIN_SITES', ''), - ]; - foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { - foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { - if (empty($appwriteDomain)) { - continue; - } - $appwriteDomains[] = $appwriteDomain; + // Mark owner as Appwrite if its appwrite-owned domain + $appwriteDomains = []; + $appwriteDomainEnvs = [ + System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), + System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + System::getEnv('_APP_DOMAIN_SITES', ''), + ]; + foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { + foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { + if (empty($appwriteDomain)) { + continue; } + $appwriteDomains[] = $appwriteDomain; } + } - foreach ($appwriteDomains as $appwriteDomain) { - if (\str_ends_with($domain->get(), $appwriteDomain)) { - $owner = 'Appwrite'; - break; - } + foreach ($appwriteDomains as $appwriteDomain) { + if (\str_ends_with($domain->get(), $appwriteDomain)) { + $owner = 'Appwrite'; + break; } + } - $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); - $rule = $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'domain' => $domain->get(), - 'type' => 'api', - 'status' => RULE_STATUS_CERTIFICATE_GENERATING, - 'projectId' => $console->getId(), - 'projectInternalId' => $console->getSequence(), - 'search' => implode(' ', [$ruleId, $domain->get()]), - 'owner' => $owner, - 'region' => $console->getAttribute('region') - ])); + $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); + $rule = $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'domain' => $domain->get(), + 'type' => 'api', + 'status' => RULE_STATUS_CERTIFICATE_GENERATING, + 'projectId' => $console->getId(), + 'projectInternalId' => $console->getSequence(), + 'search' => implode(' ', [$ruleId, $domain->get()]), + 'owner' => $owner, + 'region' => $console->getAttribute('region') + ])); - Console::success('Rule ' . $rule->getId() . ' created for domain: ' . $domain->get()); + Console::info('Rule ' . $rule->getId() . ' created for domain: ' . $domain->get()); - $queueForCertificates - ->setDomain(new Document([ - 'domain' => $domain->get() - ])) - ->setSkipRenewCheck($skipCheck) - ->trigger(); + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $domain->get() + ])) + ->setSkipRenewCheck($skipCheck) + ->trigger(); - Console::success('Scheduled a job to issue a TLS certificate for domain: ' . $domain->get()); - }); + Console::success('Scheduled a job to issue a TLS certificate for domain: ' . $domain->get()); } } From 45e74d7b3db615b8050e27f4b62a673b1aefda4a Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Fri, 6 Feb 2026 00:14:15 +0530 Subject: [PATCH 06/19] rule --- src/Appwrite/Platform/Tasks/SSL.php | 79 +++++++++++++++-------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/SSL.php b/src/Appwrite/Platform/Tasks/SSL.php index 514e2b670a..699a09972e 100644 --- a/src/Appwrite/Platform/Tasks/SSL.php +++ b/src/Appwrite/Platform/Tasks/SSL.php @@ -50,51 +50,54 @@ class SSL extends Action Query::equal('domain', [$domain->get()]), ]); - if (!$rule->isEmpty()) { - Console::warning('Rule ' . $rule->getId() . ' already exists for domain: ' . $domain->get()); - return; - } + if ($rule->isEmpty()) { + $owner = ''; - $owner = ''; - - // Mark owner as Appwrite if its appwrite-owned domain - $appwriteDomains = []; - $appwriteDomainEnvs = [ - System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), - System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), - System::getEnv('_APP_DOMAIN_SITES', ''), - ]; - foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { - foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { - if (empty($appwriteDomain)) { - continue; + // Mark owner as Appwrite if its appwrite-owned domain + $appwriteDomains = []; + $appwriteDomainEnvs = [ + System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''), + System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + System::getEnv('_APP_DOMAIN_SITES', ''), + ]; + foreach ($appwriteDomainEnvs as $appwriteDomainEnv) { + foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) { + if (empty($appwriteDomain)) { + continue; + } + $appwriteDomains[] = $appwriteDomain; } - $appwriteDomains[] = $appwriteDomain; } - } - foreach ($appwriteDomains as $appwriteDomain) { - if (\str_ends_with($domain->get(), $appwriteDomain)) { - $owner = 'Appwrite'; - break; + foreach ($appwriteDomains as $appwriteDomain) { + if (\str_ends_with($domain->get(), $appwriteDomain)) { + $owner = 'Appwrite'; + break; + } } + + $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); + $rule = $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'domain' => $domain->get(), + 'type' => 'api', + 'status' => RULE_STATUS_CERTIFICATE_GENERATING, + 'projectId' => $console->getId(), + 'projectInternalId' => $console->getSequence(), + 'search' => implode(' ', [$ruleId, $domain->get()]), + 'owner' => $owner, + 'region' => $console->getAttribute('region') + ])); + + Console::info('Rule ' . $rule->getId() . ' created for domain: ' . $domain->get()); + } else { + $rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + 'status' => RULE_STATUS_CERTIFICATE_GENERATING, + ])); + + Console::info('Updated existing rule ' . $rule->getId() . ' for domain: ' . $domain->get()); } - $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); - $rule = $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'domain' => $domain->get(), - 'type' => 'api', - 'status' => RULE_STATUS_CERTIFICATE_GENERATING, - 'projectId' => $console->getId(), - 'projectInternalId' => $console->getSequence(), - 'search' => implode(' ', [$ruleId, $domain->get()]), - 'owner' => $owner, - 'region' => $console->getAttribute('region') - ])); - - Console::info('Rule ' . $rule->getId() . ' created for domain: ' . $domain->get()); - $queueForCertificates ->setDomain(new Document([ 'domain' => $domain->get() From 455004a536d2be46d9986c36a2a7f462e4582226 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 6 Feb 2026 15:04:29 +0530 Subject: [PATCH 07/19] chore: upgrade spomky-labs/otphp to latest --- app/http.php | 2 +- composer.json | 2 +- composer.lock | 333 +++++++++++++------------------------------------- 3 files changed, 88 insertions(+), 249 deletions(-) diff --git a/app/http.php b/app/http.php index 333f6d4de2..ae3f6c5341 100644 --- a/app/http.php +++ b/app/http.php @@ -181,7 +181,7 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) { include __DIR__ . '/controllers/general.php'; -function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, callable $extraSetup = null): void +function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void { $max = 10; $sleep = 1; diff --git a/composer.json b/composer.json index d42dcc63c4..b68c632796 100644 --- a/composer.json +++ b/composer.json @@ -82,7 +82,7 @@ "phpmailer/phpmailer": "6.9.*", "chillerlan/php-qrcode": "4.4.*", "adhocore/jwt": "1.1.*", - "spomky-labs/otphp": "10.0.*", + "spomky-labs/otphp": "^11.4.2", "webonyx/graphql-php": "14.11.*", "league/csv": "9.24.*", "enshrined/svg-sanitize": "0.22.*" diff --git a/composer.lock b/composer.lock index 180f4095b2..477bae6a31 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": "78db79de4948322d4e6307f9eb9a74a6", + "content-hash": "647d248df5990c3470fe85b7cce60498", "packages": [ { "name": "adhocore/jwt", @@ -214,73 +214,6 @@ }, "time": "2025-11-11T13:44:44+00:00" }, - { - "name": "beberlei/assert", - "version": "v3.3.3", - "source": { - "type": "git", - "url": "https://github.com/beberlei/assert.git", - "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd", - "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-simplexml": "*", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "*", - "phpstan/phpstan": "*", - "phpunit/phpunit": ">=6.0.0", - "yoast/phpunit-polyfills": "^0.1.0" - }, - "suggest": { - "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" - }, - "type": "library", - "autoload": { - "files": [ - "lib/Assert/functions.php" - ], - "psr-4": { - "Assert\\": "lib/Assert" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-2-Clause" - ], - "authors": [ - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de", - "role": "Lead Developer" - }, - { - "name": "Richard Quadling", - "email": "rquadling@gmail.com", - "role": "Collaborator" - } - ], - "description": "Thin assertion library for input validation in business models.", - "keywords": [ - "assert", - "assertion", - "validation" - ], - "support": { - "issues": "https://github.com/beberlei/assert/issues", - "source": "https://github.com/beberlei/assert/tree/v3.3.3" - }, - "time": "2024-07-15T13:18:35+00:00" - }, { "name": "brick/math", "version": "0.14.6", @@ -1708,24 +1641,26 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v2.8.2", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226" + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/e30811f7bc69e4b5b6d5783e712c06c8eabf0226", - "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", "shasum": "" }, "require": { - "php": "^7|^8" + "php": "^8" }, "require-dev": { - "phpunit/phpunit": "^6|^7|^8|^9", - "vimeo/psalm": "^1|^2|^3|^4" + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" }, "type": "library", "autoload": { @@ -1771,7 +1706,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2025-09-24T15:12:37+00:00" + "time": "2025-09-24T15:06:41+00:00" }, { "name": "paragonie/random_compat", @@ -2174,6 +2109,54 @@ ], "time": "2026-01-27T09:17:28+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -2593,43 +2576,28 @@ }, { "name": "spomky-labs/otphp", - "version": "v10.0.3", + "version": "11.4.2", "source": { "type": "git", "url": "https://github.com/Spomky-Labs/otphp.git", - "reference": "9784d9f7c790eed26e102d6c78f12c754036c366" + "reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/9784d9f7c790eed26e102d6c78f12c754036c366", - "reference": "9784d9f7c790eed26e102d6c78f12c754036c366", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad", + "reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad", "shasum": "" }, "require": { - "beberlei/assert": "^3.0", - "ext-mbstring": "*", - "paragonie/constant_time_encoding": "^2.0", - "php": "^7.2|^8.0", - "thecodingmachine/safe": "^0.1.14|^1.0|^2.0" + "paragonie/constant_time_encoding": "^2.0 || ^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "symfony/deprecation-contracts": "^3.2" }, "require-dev": { - "php-coveralls/php-coveralls": "^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-beberlei-assert": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^8.0", - "thecodingmachine/phpstan-safe-rule": "^1.0 || ^2.0" + "symfony/error-handler": "^6.4|^7.0|^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "v8.3": "8.3.x-dev", - "v9.0": "9.0.x-dev", - "v10.0": "10.0.x-dev" - } - }, "autoload": { "psr-4": { "OTPHP\\": "src/" @@ -2662,9 +2630,19 @@ ], "support": { "issues": "https://github.com/Spomky-Labs/otphp/issues", - "source": "https://github.com/Spomky-Labs/otphp/tree/v10.0.3" + "source": "https://github.com/Spomky-Labs/otphp/tree/11.4.2" }, - "time": "2022-03-17T08:00:35+00:00" + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-01-23T10:53:01+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3376,145 +3354,6 @@ }, "time": "2025-06-29T15:42:06+00:00" }, - { - "name": "thecodingmachine/safe", - "version": "v2.5.0", - "source": { - "type": "git", - "url": "https://github.com/thecodingmachine/safe.git", - "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", - "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", - "shasum": "" - }, - "require": { - "php": "^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.5", - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev" - } - }, - "autoload": { - "files": [ - "deprecated/apc.php", - "deprecated/array.php", - "deprecated/datetime.php", - "deprecated/libevent.php", - "deprecated/misc.php", - "deprecated/password.php", - "deprecated/mssql.php", - "deprecated/stats.php", - "deprecated/strings.php", - "lib/special_cases.php", - "deprecated/mysqli.php", - "generated/apache.php", - "generated/apcu.php", - "generated/array.php", - "generated/bzip2.php", - "generated/calendar.php", - "generated/classobj.php", - "generated/com.php", - "generated/cubrid.php", - "generated/curl.php", - "generated/datetime.php", - "generated/dir.php", - "generated/eio.php", - "generated/errorfunc.php", - "generated/exec.php", - "generated/fileinfo.php", - "generated/filesystem.php", - "generated/filter.php", - "generated/fpm.php", - "generated/ftp.php", - "generated/funchand.php", - "generated/gettext.php", - "generated/gmp.php", - "generated/gnupg.php", - "generated/hash.php", - "generated/ibase.php", - "generated/ibmDb2.php", - "generated/iconv.php", - "generated/image.php", - "generated/imap.php", - "generated/info.php", - "generated/inotify.php", - "generated/json.php", - "generated/ldap.php", - "generated/libxml.php", - "generated/lzf.php", - "generated/mailparse.php", - "generated/mbstring.php", - "generated/misc.php", - "generated/mysql.php", - "generated/network.php", - "generated/oci8.php", - "generated/opcache.php", - "generated/openssl.php", - "generated/outcontrol.php", - "generated/pcntl.php", - "generated/pcre.php", - "generated/pgsql.php", - "generated/posix.php", - "generated/ps.php", - "generated/pspell.php", - "generated/readline.php", - "generated/rpminfo.php", - "generated/rrd.php", - "generated/sem.php", - "generated/session.php", - "generated/shmop.php", - "generated/sockets.php", - "generated/sodium.php", - "generated/solr.php", - "generated/spl.php", - "generated/sqlsrv.php", - "generated/ssdeep.php", - "generated/ssh2.php", - "generated/stream.php", - "generated/strings.php", - "generated/swoole.php", - "generated/uodbc.php", - "generated/uopz.php", - "generated/url.php", - "generated/var.php", - "generated/xdiff.php", - "generated/xml.php", - "generated/xmlrpc.php", - "generated/yaml.php", - "generated/yaz.php", - "generated/zip.php", - "generated/zlib.php" - ], - "classmap": [ - "lib/DateTime.php", - "lib/DateTimeImmutable.php", - "lib/Exceptions/", - "deprecated/Exceptions/", - "generated/Exceptions/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "support": { - "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v2.5.0" - }, - "time": "2023-04-05T11:54:14+00:00" - }, { "name": "utopia-php/abuse", "version": "1.2.2", @@ -4626,16 +4465,16 @@ }, { "name": "utopia-php/migration", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "0d6e77748ca7d9302a88953751bd89d577610afd" + "reference": "b5fe19804b41d5bdd85571e7cdb83a268b6859e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/0d6e77748ca7d9302a88953751bd89d577610afd", - "reference": "0d6e77748ca7d9302a88953751bd89d577610afd", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/b5fe19804b41d5bdd85571e7cdb83a268b6859e2", + "reference": "b5fe19804b41d5bdd85571e7cdb83a268b6859e2", "shasum": "" }, "require": { @@ -4676,9 +4515,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.5.0" + "source": "https://github.com/utopia-php/migration/tree/1.5.1" }, - "time": "2026-02-02T10:42:04+00:00" + "time": "2026-02-05T11:32:03+00:00" }, { "name": "utopia-php/mongo", From ec418e60b77d77a45fc0590e828c9a520f947bd4 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:32:01 +0000 Subject: [PATCH 08/19] chore: remove auth skip from workers --- src/Appwrite/Platform/Workers/Certificates.php | 12 ++++++------ src/Appwrite/Platform/Workers/Migrations.php | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 4f898c7e98..a726647ec4 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -153,11 +153,11 @@ class Certificates extends Action ): void { // Get rule $rule = System::getEnv('_APP_RULES_FORMAT') === 'md5' - ? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get()))) - : $authorization->skip(fn () => $dbForPlatform->findOne('rules', [ + ? $dbForPlatform->getDocument('rules', md5($domain->get())) + : $dbForPlatform->findOne('rules', [ Query::equal('domain', [$domain->get()]), Query::limit(1), - ])); + ]); // Skip if rule is not desired state (created but not verified yet). if ($rule->getAttribute('status', '') !== RULE_STATUS_CREATED) { @@ -269,11 +269,11 @@ class Certificates extends Action // Get rule document for domain // TODO: (@Meldiron) Remove after 1.7.x migration $rule = System::getEnv('_APP_RULES_FORMAT') === 'md5' - ? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get()))) - : $authorization->skip(fn () => $dbForPlatform->findOne('rules', [ + ? $dbForPlatform->getDocument('rules', md5($domain->get())) + : $dbForPlatform->findOne('rules', [ Query::equal('domain', [$domain->get()]), Query::limit(1), - ])); + ]); // Rule not found (or) not in the expected state if ($rule->isEmpty() || !\in_array($rule->getAttribute('status'), [RULE_STATUS_CERTIFICATE_GENERATING, RULE_STATUS_VERIFIED])) { diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 649f62609b..edda34cf5a 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -493,7 +493,7 @@ class Migrations extends Action throw new \Exception('User ' . $userInternalId . ' not found'); } - $bucket = $authorization->skip(fn () => $this->dbForPlatform->getDocument('buckets', $bucketId)); + $bucket = $this->dbForPlatform->getDocument('buckets', $bucketId); if ($bucket->isEmpty()) { throw new \Exception('Bucket not found'); } From da7f5b7336839af93063dfae7f547353dd263d78 Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:22:44 +0530 Subject: [PATCH 09/19] Move VCS repository APIs to Modules (#11259) * Move VCS repository APIs to Modules * add to http * lint * strategy fix * rename to detections --- app/controllers/api/vcs.php | 803 ------------------ .../Repositories/Branches/XList.php | 95 +++ .../Repositories/Contents/Get.php | 110 +++ .../Installations/Repositories/Create.php | 158 ++++ .../Repositories/Detections/Create.php | 315 +++++++ .../Http/Installations/Repositories/Get.php | 96 +++ .../Http/Installations/Repositories/XList.php | 319 +++++++ .../Platform/Modules/VCS/Services/Http.php | 14 + 8 files changed, 1107 insertions(+), 803 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php create mode 100644 src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Contents/Get.php create mode 100644 src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php create mode 100644 src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php create mode 100644 src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php create mode 100644 src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 373f413304..6571032a62 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -11,11 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Vcs\Comment; -use Swoole\Coroutine\WaitGroup; use Utopia\CLI\Console; -use Utopia\Config\Adapters\Dotenv as ConfigDotenv; -use Utopia\Config\Config; -use Utopia\Config\Exceptions\Parse; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -25,52 +21,12 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Queries; -use Utopia\Database\Validator\Query\Limit; -use Utopia\Database\Validator\Query\Offset; -use Utopia\Detector\Detection\Framework\Analog; -use Utopia\Detector\Detection\Framework\Angular; -use Utopia\Detector\Detection\Framework\Astro; -use Utopia\Detector\Detection\Framework\Flutter; -use Utopia\Detector\Detection\Framework\Lynx; -use Utopia\Detector\Detection\Framework\NextJs; -use Utopia\Detector\Detection\Framework\Nuxt; -use Utopia\Detector\Detection\Framework\React; -use Utopia\Detector\Detection\Framework\ReactNative; -use Utopia\Detector\Detection\Framework\Remix; -use Utopia\Detector\Detection\Framework\Svelte; -use Utopia\Detector\Detection\Framework\SvelteKit; -use Utopia\Detector\Detection\Framework\TanStackStart; -use Utopia\Detector\Detection\Framework\Vue; -use Utopia\Detector\Detection\Packager\NPM; -use Utopia\Detector\Detection\Packager\PNPM; -use Utopia\Detector\Detection\Packager\Yarn; -use Utopia\Detector\Detection\Runtime\Bun; -use Utopia\Detector\Detection\Runtime\CPP; -use Utopia\Detector\Detection\Runtime\Dart; -use Utopia\Detector\Detection\Runtime\Deno; -use Utopia\Detector\Detection\Runtime\Dotnet; -use Utopia\Detector\Detection\Runtime\Java; -use Utopia\Detector\Detection\Runtime\Node; -use Utopia\Detector\Detection\Runtime\PHP; -use Utopia\Detector\Detection\Runtime\Python; -use Utopia\Detector\Detection\Runtime\Ruby; -use Utopia\Detector\Detection\Runtime\Swift; -use Utopia\Detector\Detector\Framework; -use Utopia\Detector\Detector\Packager; -use Utopia\Detector\Detector\Runtime; -use Utopia\Detector\Detector\Strategy; use Utopia\Http; use Utopia\System\System; -use Utopia\Validator\Boolean; use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; -use Utopia\VCS\Exception\FileNotFound; use Utopia\VCS\Exception\RepositoryNotFound; -use function Swoole\Coroutine\batch; - $createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Authorization $authorization, Build $queueForBuilds, callable $getProjectDB, Request $request, array $platform) { $errors = []; foreach ($repositories as $repository) { @@ -705,765 +661,6 @@ Http::get('/v1/vcs/github/callback') ->redirect($redirectSuccess); }); -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') - ->desc('Get files and directories of a VCS repository') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.read') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'getRepositoryContents', - description: '/docs/references/vcs/get-repository-contents.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_VCS_CONTENT_LIST, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('providerRepositoryId', '', new Text(256), 'Repository Id') - ->param('providerRootDirectory', '', new Text(256, 0), 'Path to get contents of nested directory', true) - ->param('providerReference', '', new Text(256, 0), 'Git reference (branch, tag, commit) to get contents from', true) - ->inject('gitHub') - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, string $providerReference, GitHub $github, Response $response, Document $project, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $owner = $github->getOwnerName($providerInstallationId); - try { - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - } catch (RepositoryNotFound $e) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - - $contents = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory, $providerReference); - - $vcsContents = []; - foreach ($contents as $content) { - $isDirectory = false; - if ($content['type'] === GitHub::CONTENTS_DIRECTORY) { - $isDirectory = true; - } - - $vcsContents[] = new Document([ - 'isDirectory' => $isDirectory, - 'name' => $content['name'] ?? '', - 'size' => $content['size'] ?? 0 - ]); - } - - $response->dynamic(new Document([ - 'contents' => $vcsContents - ]), Response::MODEL_VCS_CONTENT_LIST); - }); - -Http::post('/v1/vcs/github/installations/:installationId/detections') - ->alias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') - ->desc('Create repository detection') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.write') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'createRepositoryDetection', - description: '/docs/references/vcs/create-repository-detection.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DETECTION_RUNTIME, - ), - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DETECTION_FRAMEWORK, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('providerRepositoryId', '', new Text(256), 'Repository Id') - ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') - ->param('providerRootDirectory', '', new Text(256, 0), 'Path to Root Directory', true) - ->inject('gitHub') - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $providerRepositoryId, string $type, string $providerRootDirectory, GitHub $github, Response $response, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $owner = $github->getOwnerName($providerInstallationId); - try { - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - } catch (RepositoryNotFound $e) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - - $files = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory); - $files = \array_column($files, 'name'); - $languages = $github->listRepositoryLanguages($owner, $repositoryName); - - $detector = new Packager(); - foreach ($files as $file) { - $detector->addInput($file); - } - $detector - ->addOption(new Yarn()) - ->addOption(new PNPM()) - ->addOption(new NPM()); - $detection = $detector->detect(); - - $packager = !\is_null($detection) ? $detection->getName() : 'npm'; - - if ($type === 'framework') { - $packages = ''; - try { - $contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/package.json'); - $packages = $contentResponse['content'] ?? ''; - } catch (FileNotFound $e) { - // Continue detection without package.json - } - - $output = new Document([ - 'framework' => '', - 'installCommand' => '', - 'buildCommand' => '', - 'outputDirectory' => '', - ]); - - $detector = new Framework($packager); - $detector->addInput($packages, Framework::INPUT_PACKAGES); - foreach ($files as $file) { - $detector->addInput($file, Framework::INPUT_FILE); - } - - $detector - ->addOption(new Analog()) - ->addOption(new Angular()) - ->addOption(new Astro()) - ->addOption(new Flutter()) - ->addOption(new Lynx()) - ->addOption(new NextJs()) - ->addOption(new Nuxt()) - ->addOption(new React()) - ->addOption(new ReactNative()) - ->addOption(new Remix()) - ->addOption(new Svelte()) - ->addOption(new SvelteKit()) - ->addOption(new TanStackStart()) - ->addOption(new Vue()); - - $framework = $detector->detect(); - - if (!\is_null($framework)) { - $output->setAttribute('installCommand', $framework->getInstallCommand()); - $output->setAttribute('buildCommand', $framework->getBuildCommand()); - $output->setAttribute('outputDirectory', $framework->getOutputDirectory()); - $framework = $framework->getName(); - } else { - $framework = 'other'; - $output->setAttribute('installCommand', ''); - $output->setAttribute('buildCommand', ''); - $output->setAttribute('outputDirectory', ''); - } - - $frameworks = Config::getParam('frameworks'); - if (!\in_array($framework, \array_keys($frameworks), true)) { - $framework = 'other'; - } - $output->setAttribute('framework', $framework); - } else { - $output = new Document([ - 'runtime' => '', - 'commands' => '', - 'entrypoint' => '', - ]); - - $strategies = [ - new Strategy(Strategy::FILEMATCH), - new Strategy(Strategy::LANGUAGES), - new Strategy(Strategy::EXTENSION), - ]; - - foreach ($strategies as $strategy) { - $detector = new Runtime($strategy, $packager); - - if ($strategy === Strategy::LANGUAGES) { - foreach ($languages as $language) { - $detector->addInput($language); - } - } else { - foreach ($files as $file) { - $detector->addInput($file); - } - } - - $detector - ->addOption(new Node()) - ->addOption(new Bun()) - ->addOption(new Deno()) - ->addOption(new PHP()) - ->addOption(new Python()) - ->addOption(new Dart()) - ->addOption(new Swift()) - ->addOption(new Ruby()) - ->addOption(new Java()) - ->addOption(new CPP()) - ->addOption(new Dotnet()); - - $runtime = $detector->detect(); - - if (!\is_null($runtime)) { - $output->setAttribute('commands', $runtime->getCommands()); - $output->setAttribute('entrypoint', $runtime->getEntrypoint()); - $runtime = $runtime->getName(); - break; - } - } - - if (!empty($runtime)) { - $runtimes = Config::getParam('runtimes'); - $runtimeWithVersion = ''; - foreach ($runtimes as $runtimeKey => $runtimeConfig) { - if ($runtimeConfig['key'] === $runtime) { - $runtimeWithVersion = $runtimeKey; - } - } - - if (empty($runtimeWithVersion)) { - throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED); - } - - $output->setAttribute('runtime', $runtimeWithVersion); - } else { - throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED); - } - } - - $wg = new WaitGroup(); - $envs = []; - foreach ($files as $file) { - if (!(\str_starts_with($file, '.env'))) { - continue; - } - - $wg->add(); - go(function () use ($github, $owner, $repositoryName, $providerRootDirectory, $file, $wg, &$envs) { - try { - $contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file); - $envFile = $contentResponse['content'] ?? ''; - - $configAdapter = new ConfigDotenv(); - try { - $envObject = $configAdapter->parse($envFile); - foreach ($envObject as $envName => $envValue) { - $envs[$envName] = $envValue; - } - } catch (Parse $err) { - // Silence error, so rest of endpoint can return - } - } finally { - $wg->done(); - } - }); - } - $wg->wait(); - - $variables = []; - foreach ($envs as $key => $value) { - $variables[] = [ - 'name' => $key, - 'value' => $value, - ]; - } - - $output->setAttribute('variables', $variables); - - $response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME); - }); - -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories') - ->desc('List repositories') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.read') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'listRepositories', - description: '/docs/references/vcs/list-repositories.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, - ), - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') - ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) - ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) - ->inject('gitHub') - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $type, string $search, array $queries, GitHub $github, Response $response, Database $dbForPlatform) { - if (empty($search)) { - $search = ""; - } - - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $queries = Query::parseQueries($queries); - $limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT)); - $offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET)); - - $limit = !empty($limitQuery) ? $limitQuery->getValue() : 4; - $offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0; - - if ($offset % $limit !== 0) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit'); - } - - $page = ($offset / $limit) + 1; - $owner = $github->getOwnerName($providerInstallationId); - ['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search); - - $repos = \array_map(function ($repo) use ($installation) { - $repo['id'] = \strval($repo['id'] ?? ''); - $repo['pushedAt'] = $repo['pushed_at'] ?? null; - $repo['provider'] = $installation->getAttribute('provider', '') ?? ''; - $repo['organization'] = $installation->getAttribute('organization', '') ?? ''; - return $repo; - }, $repos); - - $repos = batch(\array_map(function ($repo) use ($type, $github) { - return function () use ($repo, $type, $github) { - $files = $github->listRepositoryContents($repo['organization'], $repo['name'], ''); - $files = \array_column($files, 'name'); - - $detector = new Packager(); - foreach ($files as $file) { - $detector->addInput($file); - } - $detector - ->addOption(new Yarn()) - ->addOption(new PNPM()) - ->addOption(new NPM()); - $detection = $detector->detect(); - - $packager = !\is_null($detection) ? $detection->getName() : 'npm'; - - if ($type === 'framework') { - $packages = ''; - try { - $contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], 'package.json'); - $packages = $contentResponse['content'] ?? ''; - } catch (FileNotFound $e) { - // Continue detection without package.json - } - - $frameworkDetector = new Framework($packager); - $frameworkDetector->addInput($packages, Framework::INPUT_PACKAGES); - foreach ($files as $file) { - $frameworkDetector->addInput($file, Framework::INPUT_FILE); - } - - $frameworkDetector - ->addOption(new Analog()) - ->addOption(new Angular()) - ->addOption(new Astro()) - ->addOption(new Flutter()) - ->addOption(new Lynx()) - ->addOption(new NextJs()) - ->addOption(new Nuxt()) - ->addOption(new React()) - ->addOption(new ReactNative()) - ->addOption(new Remix()) - ->addOption(new Svelte()) - ->addOption(new SvelteKit()) - ->addOption(new TanStackStart()) - ->addOption(new Vue()); - - $detectedFramework = $frameworkDetector->detect(); - - if (!\is_null($detectedFramework)) { - $framework = $detectedFramework->getName(); - } else { - $framework = 'other'; - } - - $frameworks = Config::getParam('frameworks'); - if (!\in_array($framework, \array_keys($frameworks), true)) { - $framework = 'other'; - } - $repo['framework'] = $framework; - } else { - $languages = $github->listRepositoryLanguages($repo['organization'], $repo['name']); - - $strategies = [ - new Strategy(Strategy::FILEMATCH), - new Strategy(Strategy::LANGUAGES), - new Strategy(Strategy::EXTENSION), - ]; - - foreach ($strategies as $strategy) { - $detector = new Runtime($strategy, $packager); - if ($strategy === Strategy::LANGUAGES) { - foreach ($languages as $language) { - $detector->addInput($language); - } - } else { - foreach ($files as $file) { - $detector->addInput($file); - } - } - $detector - ->addOption(new Node()) - ->addOption(new Bun()) - ->addOption(new Deno()) - ->addOption(new PHP()) - ->addOption(new Python()) - ->addOption(new Dart()) - ->addOption(new Swift()) - ->addOption(new Ruby()) - ->addOption(new Java()) - ->addOption(new CPP()) - ->addOption(new Dotnet()); - - $runtime = $detector->detect(); - - if (!\is_null($runtime)) { - $runtime = $runtime->getName(); - break; - } - } - - if (!empty($runtime)) { - $runtimes = Config::getParam('runtimes'); - $runtimeWithVersion = ''; - foreach ($runtimes as $runtimeKey => $runtimeConfig) { - if ($runtimeConfig['key'] === $runtime) { - $runtimeWithVersion = $runtimeKey; - } - } - - $repo['runtime'] = $runtimeWithVersion ?? ''; - } - } - - $wg = new WaitGroup(); - $envs = []; - foreach ($files as $file) { - if (!(\str_starts_with($file, '.env'))) { - continue; - } - - $wg->add(); - go(function () use ($github, $repo, $file, $wg, &$envs) { - try { - $contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], $file); - $envFile = $contentResponse['content'] ?? ''; - - $configAdapter = new ConfigDotenv(); - try { - $envObject = $configAdapter->parse($envFile); - foreach ($envObject as $envName => $envValue) { - $envs[$envName] = $envValue; - } - } catch (Parse) { - // Silence error, so rest of endpoint can return - } - } finally { - $wg->done(); - } - }); - } - $wg->wait(); - - $repo['variables'] = []; - foreach ($envs as $key => $value) { - $repo['variables'][] = [ - 'name' => $key, - 'value' => $value, - ]; - } - - return $repo; - }; - }, $repos)); - - $repos = \array_map(function ($repo) { - return new Document($repo); - }, $repos); - - $response->dynamic(new Document([ - $type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos, - 'total' => $total, - ]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST); - }); - -Http::post('/v1/vcs/github/installations/:installationId/providerRepositories') - ->desc('Create repository') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.write') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'createRepository', - description: '/docs/references/vcs/create-repository.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('name', '', new Text(256), 'Repository name (slug)') - ->param('private', '', new Boolean(false), 'Mark repository public or private') - ->inject('gitHub') - ->inject('user') - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $name, bool $private, GitHub $github, Document $user, Response $response, Document $project, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - if ($installation->getAttribute('personal', false) === true) { - $oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), ""); - - $accessToken = $installation->getAttribute('personalAccessToken'); - $refreshToken = $installation->getAttribute('personalRefreshToken'); - $accessTokenExpiry = $installation->getAttribute('personalAccessTokenExpiry'); - - if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) { - $identity = $dbForPlatform->findOne('identities', [ - Query::equal('provider', ['github']), - Query::equal('userInternalId', [$user->getSequence()]), - ]); - if ($identity->isEmpty()) { - throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); - } - - $accessToken = $accessToken ?? $identity->getAttribute('providerAccessToken'); - $refreshToken = $refreshToken ?? $identity->getAttribute('providerRefreshToken'); - $accessTokenExpiry = $accessTokenExpiry ?? $identity->getAttribute('providerAccessTokenExpiry'); - } - - $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); - if ($isExpired) { - $oauth2->refreshTokens($refreshToken); - - $accessToken = $oauth2->getAccessToken(''); - $refreshToken = $oauth2->getRefreshToken(''); - - $verificationId = $oauth2->getUserID($accessToken); - - if (empty($verificationId)) { - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, "Another request is currently refreshing OAuth token. Please try again."); - } - - $installation = $installation - ->setAttribute('personalAccessToken', $accessToken) - ->setAttribute('personalRefreshToken', $refreshToken) - ->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); - - $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); - } - - try { - $repository = $oauth2->createRepository($accessToken, $name, $private); - } catch (Exception $exception) { - throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage()); - } - } else { - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - $owner = $github->getOwnerName($providerInstallationId); - - try { - $repository = $github->createRepository($owner, $name, $private); - } catch (Exception $exception) { - throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage()); - } - } - - if (isset($repository['errors'])) { - $message = $repository['message'] ?? 'Unknown error.'; - if (isset($repository['errors'][0])) { - $message .= ' ' . $repository['errors'][0]['message']; - } - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $message); - } - - if (isset($repository['message'])) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $repository['message']); - } - - $repository['id'] = \strval($repository['id']) ?? ''; - $repository['pushedAt'] = $repository['pushed_at'] ?? ''; - $repository['organization'] = $installation->getAttribute('organization', ''); - $repository['provider'] = $installation->getAttribute('provider', ''); - - $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); - }); - -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') - ->desc('Get repository') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.read') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'getRepository', - description: '/docs/references/vcs/get-repository.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('providerRepositoryId', '', new Text(256), 'Repository Id') - ->inject('gitHub') - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $owner = $github->getOwnerName($providerInstallationId) ?? ''; - try { - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - } catch (RepositoryNotFound $e) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - - $repository = $github->getRepository($owner, $repositoryName); - - $repository['id'] = \strval($repository['id']) ?? ''; - $repository['pushedAt'] = $repository['pushed_at'] ?? ''; - $repository['organization'] = $installation->getAttribute('organization', ''); - $repository['provider'] = $installation->getAttribute('provider', ''); - $repository['defaultBranch'] = $repository['default_branch'] ?? ''; - - $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); - }); - -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') - ->desc('List repository branches') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.read') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'listRepositoryBranches', - description: '/docs/references/vcs/list-repository-branches.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_BRANCH_LIST, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('providerRepositoryId', '', new Text(256), 'Repository Id') - ->inject('gitHub') - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $owner = $github->getOwnerName($providerInstallationId) ?? ''; - try { - $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; - if (empty($repositoryName)) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - } catch (RepositoryNotFound $e) { - throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); - } - - $branches = $github->listBranches($owner, $repositoryName) ?? []; - - $response->dynamic(new Document([ - 'branches' => \array_map(function ($branch) { - return new Document(['name' => $branch]); - }, $branches), - 'total' => \count($branches), - ]), Response::MODEL_BRANCH_LIST); - }); - Http::post('/v1/vcs/github/events') ->desc('Create event') ->groups(['api', 'vcs']) diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php new file mode 100644 index 0000000000..4ed4241d25 --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php @@ -0,0 +1,95 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') + ->desc('List repository branches') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.read') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'listRepositoryBranches', + description: '/docs/references/vcs/list-repository-branches.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_BRANCH_LIST, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('providerRepositoryId', '', new Text(256), 'Repository Id') + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $providerRepositoryId, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $owner = $github->getOwnerName($providerInstallationId) ?? ''; + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + + $branches = $github->listBranches($owner, $repositoryName) ?? []; + + $response->dynamic(new Document([ + 'branches' => \array_map(function ($branch) { + return new Document(['name' => $branch]); + }, $branches), + 'total' => \count($branches), + ]), Response::MODEL_BRANCH_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Contents/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Contents/Get.php new file mode 100644 index 0000000000..a0dcec8590 --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Contents/Get.php @@ -0,0 +1,110 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') + ->desc('Get files and directories of a VCS repository') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.read') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'getRepositoryContents', + description: '/docs/references/vcs/get-repository-contents.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_VCS_CONTENT_LIST, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('providerRepositoryId', '', new Text(256), 'Repository Id') + ->param('providerRootDirectory', '', new Text(256, 0), 'Path to get contents of nested directory', true) + ->param('providerReference', '', new Text(256, 0), 'Git reference (branch, tag, commit) to get contents from', true) + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $providerRepositoryId, + string $providerRootDirectory, + string $providerReference, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $owner = $github->getOwnerName($providerInstallationId); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + + $contents = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory, $providerReference); + + $vcsContents = []; + foreach ($contents as $content) { + $isDirectory = false; + if ($content['type'] === GitHub::CONTENTS_DIRECTORY) { + $isDirectory = true; + } + + $vcsContents[] = new Document([ + 'isDirectory' => $isDirectory, + 'name' => $content['name'] ?? '', + 'size' => $content['size'] ?? 0 + ]); + } + + $response->dynamic(new Document([ + 'contents' => $vcsContents + ]), Response::MODEL_VCS_CONTENT_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php new file mode 100644 index 0000000000..e357d1d5fa --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php @@ -0,0 +1,158 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories') + ->desc('Create repository') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.write') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'createRepository', + description: '/docs/references/vcs/create-repository.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROVIDER_REPOSITORY, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('name', '', new Text(256), 'Repository name (slug)') + ->param('private', '', new Boolean(false), 'Mark repository public or private') + ->inject('gitHub') + ->inject('user') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $name, + bool $private, + GitHub $github, + Document $user, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + if ($installation->getAttribute('personal', false) === true) { + $oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), ""); + + $accessToken = $installation->getAttribute('personalAccessToken'); + $refreshToken = $installation->getAttribute('personalRefreshToken'); + $accessTokenExpiry = $installation->getAttribute('personalAccessTokenExpiry'); + + if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) { + $identity = $dbForPlatform->findOne('identities', [ + Query::equal('provider', ['github']), + Query::equal('userInternalId', [$user->getSequence()]), + ]); + if ($identity->isEmpty()) { + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); + } + + $accessToken = $accessToken ?? $identity->getAttribute('providerAccessToken'); + $refreshToken = $refreshToken ?? $identity->getAttribute('providerRefreshToken'); + $accessTokenExpiry = $accessTokenExpiry ?? $identity->getAttribute('providerAccessTokenExpiry'); + } + + $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); + if ($isExpired) { + $oauth2->refreshTokens($refreshToken); + + $accessToken = $oauth2->getAccessToken(''); + $refreshToken = $oauth2->getRefreshToken(''); + + $verificationId = $oauth2->getUserID($accessToken); + + if (empty($verificationId)) { + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, "Another request is currently refreshing OAuth token. Please try again."); + } + + $installation = $installation + ->setAttribute('personalAccessToken', $accessToken) + ->setAttribute('personalRefreshToken', $refreshToken) + ->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); + + $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); + } + + try { + $repository = $oauth2->createRepository($accessToken, $name, $private); + } catch (Exception $exception) { + throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage()); + } + } else { + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + $owner = $github->getOwnerName($providerInstallationId); + + try { + $repository = $github->createRepository($owner, $name, $private); + } catch (Exception $exception) { + throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage()); + } + } + + if (isset($repository['errors'])) { + $message = $repository['message'] ?? 'Unknown error.'; + if (isset($repository['errors'][0])) { + $message .= ' ' . $repository['errors'][0]['message']; + } + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $message); + } + + if (isset($repository['message'])) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $repository['message']); + } + + $repository['id'] = \strval($repository['id']) ?? ''; + $repository['pushedAt'] = $repository['pushed_at'] ?? ''; + $repository['organization'] = $installation->getAttribute('organization', ''); + $repository['provider'] = $installation->getAttribute('provider', ''); + + $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php new file mode 100644 index 0000000000..bada6d98bb --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php @@ -0,0 +1,315 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/vcs/github/installations/:installationId/detections') + ->httpAlias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') + ->desc('Create repository detection') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.write') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'createRepositoryDetection', + description: '/docs/references/vcs/create-repository-detection.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_DETECTION_RUNTIME, + ), + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_DETECTION_FRAMEWORK, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('providerRepositoryId', '', new Text(256), 'Repository Id') + ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') + ->param('providerRootDirectory', '', new Text(256, 0), 'Path to Root Directory', true) + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $providerRepositoryId, + string $type, + string $providerRootDirectory, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $owner = $github->getOwnerName($providerInstallationId); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + + $files = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory); + $files = \array_column($files, 'name'); + $languages = $github->listRepositoryLanguages($owner, $repositoryName); + + $detector = new Packager(); + foreach ($files as $file) { + $detector->addInput($file); + } + $detector + ->addOption(new Yarn()) + ->addOption(new PNPM()) + ->addOption(new NPM()); + $detection = $detector->detect(); + + $packager = !\is_null($detection) ? $detection->getName() : 'npm'; + + if ($type === 'framework') { + $packages = ''; + try { + $contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/package.json'); + $packages = $contentResponse['content'] ?? ''; + } catch (FileNotFound $e) { + // Continue detection without package.json + } + + $output = new Document([ + 'framework' => '', + 'installCommand' => '', + 'buildCommand' => '', + 'outputDirectory' => '', + ]); + + $detector = new Framework($packager); + $detector->addInput($packages, Framework::INPUT_PACKAGES); + foreach ($files as $file) { + $detector->addInput($file, Framework::INPUT_FILE); + } + + $detector + ->addOption(new Analog()) + ->addOption(new Angular()) + ->addOption(new Astro()) + ->addOption(new Flutter()) + ->addOption(new Lynx()) + ->addOption(new NextJs()) + ->addOption(new Nuxt()) + ->addOption(new React()) + ->addOption(new ReactNative()) + ->addOption(new Remix()) + ->addOption(new Svelte()) + ->addOption(new SvelteKit()) + ->addOption(new TanStackStart()) + ->addOption(new Vue()); + + $framework = $detector->detect(); + + if (!\is_null($framework)) { + $output->setAttribute('installCommand', $framework->getInstallCommand()); + $output->setAttribute('buildCommand', $framework->getBuildCommand()); + $output->setAttribute('outputDirectory', $framework->getOutputDirectory()); + $framework = $framework->getName(); + } else { + $framework = 'other'; + $output->setAttribute('installCommand', ''); + $output->setAttribute('buildCommand', ''); + $output->setAttribute('outputDirectory', ''); + } + + $frameworks = Config::getParam('frameworks'); + if (!\in_array($framework, \array_keys($frameworks), true)) { + $framework = 'other'; + } + $output->setAttribute('framework', $framework); + } else { + $output = new Document([ + 'runtime' => '', + 'commands' => '', + 'entrypoint' => '', + ]); + + $strategies = [ + new Strategy(Strategy::FILEMATCH), + new Strategy(Strategy::LANGUAGES), + new Strategy(Strategy::EXTENSION), + ]; + + foreach ($strategies as $strategy) { + $detector = new Runtime($strategy, $packager); + + if ($strategy->getValue() === Strategy::LANGUAGES) { + foreach ($languages as $language) { + $detector->addInput($language); + } + } else { + foreach ($files as $file) { + $detector->addInput($file); + } + } + + $detector + ->addOption(new Node()) + ->addOption(new Bun()) + ->addOption(new Deno()) + ->addOption(new PHP()) + ->addOption(new Python()) + ->addOption(new Dart()) + ->addOption(new Swift()) + ->addOption(new Ruby()) + ->addOption(new Java()) + ->addOption(new CPP()) + ->addOption(new Dotnet()); + + $runtime = $detector->detect(); + + if (!\is_null($runtime)) { + $output->setAttribute('commands', $runtime->getCommands()); + $output->setAttribute('entrypoint', $runtime->getEntrypoint()); + $runtime = $runtime->getName(); + break; + } + } + + if (!empty($runtime)) { + $runtimes = Config::getParam('runtimes'); + $runtimeWithVersion = ''; + foreach ($runtimes as $runtimeKey => $runtimeConfig) { + if ($runtimeConfig['key'] === $runtime) { + $runtimeWithVersion = $runtimeKey; + } + } + + if (empty($runtimeWithVersion)) { + throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED); + } + + $output->setAttribute('runtime', $runtimeWithVersion); + } else { + throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED); + } + } + + $wg = new WaitGroup(); + $envs = []; + foreach ($files as $file) { + if (!(\str_starts_with($file, '.env'))) { + continue; + } + + $wg->add(); + go(function () use ($github, $owner, $repositoryName, $providerRootDirectory, $file, $wg, &$envs) { + try { + $contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file); + $envFile = $contentResponse['content'] ?? ''; + + $configAdapter = new ConfigDotenv(); + try { + $envObject = $configAdapter->parse($envFile); + foreach ($envObject as $envName => $envValue) { + $envs[$envName] = $envValue; + } + } catch (Parse $err) { + // Silence error, so rest of endpoint can return + } + } finally { + $wg->done(); + } + }); + } + $wg->wait(); + + $variables = []; + foreach ($envs as $key => $value) { + $variables[] = [ + 'name' => $key, + 'value' => $value, + ]; + } + + $output->setAttribute('variables', $variables); + + $response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php new file mode 100644 index 0000000000..4dee84752b --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php @@ -0,0 +1,96 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') + ->desc('Get repository') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.read') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'getRepository', + description: '/docs/references/vcs/get-repository.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROVIDER_REPOSITORY, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('providerRepositoryId', '', new Text(256), 'Repository Id') + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $providerRepositoryId, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $owner = $github->getOwnerName($providerInstallationId) ?? ''; + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + + $repository = $github->getRepository($owner, $repositoryName); + + $repository['id'] = \strval($repository['id']) ?? ''; + $repository['pushedAt'] = $repository['pushed_at'] ?? ''; + $repository['organization'] = $installation->getAttribute('organization', ''); + $repository['provider'] = $installation->getAttribute('provider', ''); + $repository['defaultBranch'] = $repository['default_branch'] ?? ''; + + $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php new file mode 100644 index 0000000000..9acc77e1f2 --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php @@ -0,0 +1,319 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories') + ->desc('List repositories') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.read') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'listRepositories', + description: '/docs/references/vcs/list-repositories.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, + ), + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $type, + string $search, + array $queries, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + if (empty($search)) { + $search = ""; + } + + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $queries = Query::parseQueries($queries); + $limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT)); + $offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET)); + + $limit = !empty($limitQuery) ? $limitQuery->getValue() : 4; + $offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0; + + if ($offset % $limit !== 0) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit'); + } + + $page = ($offset / $limit) + 1; + $owner = $github->getOwnerName($providerInstallationId); + ['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search); + + $repos = \array_map(function ($repo) use ($installation) { + $repo['id'] = \strval($repo['id'] ?? ''); + $repo['pushedAt'] = $repo['pushed_at'] ?? null; + $repo['provider'] = $installation->getAttribute('provider', '') ?? ''; + $repo['organization'] = $installation->getAttribute('organization', '') ?? ''; + return $repo; + }, $repos); + + $repos = batch(\array_map(function ($repo) use ($type, $github) { + return function () use ($repo, $type, $github) { + $files = $github->listRepositoryContents($repo['organization'], $repo['name'], ''); + $files = \array_column($files, 'name'); + + $detector = new Packager(); + foreach ($files as $file) { + $detector->addInput($file); + } + $detector + ->addOption(new Yarn()) + ->addOption(new PNPM()) + ->addOption(new NPM()); + $detection = $detector->detect(); + + $packager = !\is_null($detection) ? $detection->getName() : 'npm'; + + if ($type === 'framework') { + $packages = ''; + try { + $contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], 'package.json'); + $packages = $contentResponse['content'] ?? ''; + } catch (FileNotFound $e) { + // Continue detection without package.json + } + + $frameworkDetector = new Framework($packager); + $frameworkDetector->addInput($packages, Framework::INPUT_PACKAGES); + foreach ($files as $file) { + $frameworkDetector->addInput($file, Framework::INPUT_FILE); + } + + $frameworkDetector + ->addOption(new Analog()) + ->addOption(new Angular()) + ->addOption(new Astro()) + ->addOption(new Flutter()) + ->addOption(new Lynx()) + ->addOption(new NextJs()) + ->addOption(new Nuxt()) + ->addOption(new React()) + ->addOption(new ReactNative()) + ->addOption(new Remix()) + ->addOption(new Svelte()) + ->addOption(new SvelteKit()) + ->addOption(new TanStackStart()) + ->addOption(new Vue()); + + $detectedFramework = $frameworkDetector->detect(); + + if (!\is_null($detectedFramework)) { + $framework = $detectedFramework->getName(); + } else { + $framework = 'other'; + } + + $frameworks = Config::getParam('frameworks'); + if (!\in_array($framework, \array_keys($frameworks), true)) { + $framework = 'other'; + } + $repo['framework'] = $framework; + } else { + $languages = $github->listRepositoryLanguages($repo['organization'], $repo['name']); + + $strategies = [ + new Strategy(Strategy::FILEMATCH), + new Strategy(Strategy::LANGUAGES), + new Strategy(Strategy::EXTENSION), + ]; + + foreach ($strategies as $strategy) { + $detector = new Runtime($strategy, $packager); + if ($strategy->getValue() === Strategy::LANGUAGES) { + foreach ($languages as $language) { + $detector->addInput($language); + } + } else { + foreach ($files as $file) { + $detector->addInput($file); + } + } + $detector + ->addOption(new Node()) + ->addOption(new Bun()) + ->addOption(new Deno()) + ->addOption(new PHP()) + ->addOption(new Python()) + ->addOption(new Dart()) + ->addOption(new Swift()) + ->addOption(new Ruby()) + ->addOption(new Java()) + ->addOption(new CPP()) + ->addOption(new Dotnet()); + + $runtime = $detector->detect(); + + if (!\is_null($runtime)) { + $runtime = $runtime->getName(); + break; + } + } + + if (!empty($runtime)) { + $runtimes = Config::getParam('runtimes'); + $runtimeWithVersion = ''; + foreach ($runtimes as $runtimeKey => $runtimeConfig) { + if ($runtimeConfig['key'] === $runtime) { + $runtimeWithVersion = $runtimeKey; + } + } + + $repo['runtime'] = $runtimeWithVersion ?? ''; + } + } + + $wg = new WaitGroup(); + $envs = []; + foreach ($files as $file) { + if (!(\str_starts_with($file, '.env'))) { + continue; + } + + $wg->add(); + go(function () use ($github, $repo, $file, $wg, &$envs) { + try { + $contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], $file); + $envFile = $contentResponse['content'] ?? ''; + + $configAdapter = new ConfigDotenv(); + try { + $envObject = $configAdapter->parse($envFile); + foreach ($envObject as $envName => $envValue) { + $envs[$envName] = $envValue; + } + } catch (Parse) { + // Silence error, so rest of endpoint can return + } + } finally { + $wg->done(); + } + }); + } + $wg->wait(); + + $repo['variables'] = []; + foreach ($envs as $key => $value) { + $repo['variables'][] = [ + 'name' => $key, + 'value' => $value, + ]; + } + + return $repo; + }; + }, $repos)); + + $repos = \array_map(function ($repo) { + return new Document($repo); + }, $repos); + + $response->dynamic(new Document([ + $type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos, + 'total' => $total, + ]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Services/Http.php b/src/Appwrite/Platform/Modules/VCS/Services/Http.php index 3630a5b32f..11e26be0d5 100644 --- a/src/Appwrite/Platform/Modules/VCS/Services/Http.php +++ b/src/Appwrite/Platform/Modules/VCS/Services/Http.php @@ -4,6 +4,12 @@ namespace Appwrite\Platform\Modules\VCS\Services; use Appwrite\Platform\Modules\VCS\Http\Installations\Delete as DeleteInstallation; use Appwrite\Platform\Modules\VCS\Http\Installations\Get as GetInstallation; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Branches\XList as ListRepositoryBranches; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Contents\Get as GetRepositoryContents; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Create as CreateRepository; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Detections\Create as CreateRepositoryDetections; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Get as GetRepository; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\XList as ListRepositories; use Appwrite\Platform\Modules\VCS\Http\Installations\XList as ListInstallations; use Utopia\Platform\Service; @@ -17,5 +23,13 @@ class Http extends Service $this->addAction(GetInstallation::getName(), new GetInstallation()); $this->addAction(ListInstallations::getName(), new ListInstallations()); $this->addAction(DeleteInstallation::getName(), new DeleteInstallation()); + + // Repositories + $this->addAction(CreateRepository::getName(), new CreateRepository()); + $this->addAction(GetRepository::getName(), new GetRepository()); + $this->addAction(ListRepositories::getName(), new ListRepositories()); + $this->addAction(ListRepositoryBranches::getName(), new ListRepositoryBranches()); + $this->addAction(GetRepositoryContents::getName(), new GetRepositoryContents()); + $this->addAction(CreateRepositoryDetections::getName(), new CreateRepositoryDetections()); } } From 1219391c766f75d9b3098682a2565f26ae1a3517 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 6 Feb 2026 15:27:42 +0530 Subject: [PATCH 10/19] update composer --- composer.lock | 71 ++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/composer.lock b/composer.lock index 477bae6a31..e202fdf305 100644 --- a/composer.lock +++ b/composer.lock @@ -606,16 +606,16 @@ }, { "name": "giggsey/libphonenumber-for-php-lite", - "version": "8.13.36", + "version": "9.0.23", "source": { "type": "git", "url": "https://github.com/giggsey/libphonenumber-for-php-lite.git", - "reference": "144bbe70d67664b5245910a475c7190ff140ab4b" + "reference": "e8f6f896c1bef398136849934a934904bd9bf072" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/144bbe70d67664b5245910a475c7190ff140ab4b", - "reference": "144bbe70d67664b5245910a475c7190ff140ab4b", + "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/e8f6f896c1bef398136849934a934904bd9bf072", + "reference": "e8f6f896c1bef398136849934a934904bd9bf072", "shasum": "" }, "require": { @@ -627,18 +627,19 @@ }, "require-dev": { "ext-dom": "*", - "friendsofphp/php-cs-fixer": "^3.12", - "infection/infection": "^0.28", - "pear/pear-core-minimal": "^1.10.11", - "pear/pear_exception": "^1.0.2", - "pear/versioncontrol_git": "^0.7", - "phing/phing": "^2.17.4", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.2", - "phpunit/phpunit": "^10.5", - "symfony/console": "^6.0", - "symfony/var-exporter": "^6.0" + "friendsofphp/php-cs-fixer": "^3.71", + "infection/infection": "^0.29|^0.31.0", + "nette/php-generator": "^4.1", + "php-coveralls/php-coveralls": "^2.7", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.7", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2.0.3", + "phpunit/phpunit": "^10.5.45", + "symfony/console": "^6.4", + "symfony/filesystem": "^6.4", + "symfony/process": "^6.4" }, "suggest": { "giggsey/libphonenumber-for-php": "Use libphonenumber-for-php for geocoding, carriers, timezones and matching" @@ -646,19 +647,13 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "8.x-dev" + "dev-master": "9.x-dev" } }, "autoload": { "psr-4": { "libphonenumber\\": "src/" - }, - "exclude-from-classmap": [ - "/src/data/", - "/src/carrier/data/", - "/src/geocoding/data/", - "/src/timezone/data/" - ] + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -685,7 +680,7 @@ "issues": "https://github.com/giggsey/libphonenumber-for-php-lite/issues", "source": "https://github.com/giggsey/libphonenumber-for-php-lite" }, - "time": "2024-05-03T06:31:11+00:00" + "time": "2026-01-30T12:33:03+00:00" }, { "name": "google/protobuf", @@ -3610,16 +3605,16 @@ }, { "name": "utopia-php/cli", - "version": "0.15.2", + "version": "0.15.4", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "da00ff6b8b29a826a1794002ae43442cdf3a0f5f" + "reference": "cc873548dcab4329f2d56ca35f2edf3ae9d70f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/da00ff6b8b29a826a1794002ae43442cdf3a0f5f", - "reference": "da00ff6b8b29a826a1794002ae43442cdf3a0f5f", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/cc873548dcab4329f2d56ca35f2edf3ae9d70f51", + "reference": "cc873548dcab4329f2d56ca35f2edf3ae9d70f51", "shasum": "" }, "require": { @@ -3653,9 +3648,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.15.2" + "source": "https://github.com/utopia-php/cli/tree/0.15.4" }, - "time": "2025-04-15T10:08:48+00:00" + "time": "2026-02-06T09:49:09+00:00" }, { "name": "utopia-php/compression", @@ -4414,22 +4409,22 @@ }, { "name": "utopia-php/messaging", - "version": "0.20.0", + "version": "0.20.1", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "6c5be4588d97e3732a1907ecb13cd8a098eade96" + "reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/6c5be4588d97e3732a1907ecb13cd8a098eade96", - "reference": "6c5be4588d97e3732a1907ecb13cd8a098eade96", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0", + "reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0", "shasum": "" }, "require": { "ext-curl": "*", "ext-openssl": "*", - "giggsey/libphonenumber-for-php-lite": "8.13.36", + "giggsey/libphonenumber-for-php-lite": "9.0.23", "php": ">=8.0.0", "phpmailer/phpmailer": "6.9.1" }, @@ -4459,9 +4454,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.20.0" + "source": "https://github.com/utopia-php/messaging/tree/0.20.1" }, - "time": "2025-10-22T04:27:37+00:00" + "time": "2026-02-06T09:56:06+00:00" }, { "name": "utopia-php/migration", From ab3580637242e47155b0a0822652338c378d5bfb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 6 Feb 2026 15:43:59 +0530 Subject: [PATCH 11/19] update system lib --- composer.json | 2 +- composer.lock | 44 ++++++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/composer.json b/composer.json index b68c632796..356723ec8d 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,7 @@ "utopia-php/registry": "0.5.*", "utopia-php/storage": "0.18.*", "utopia-php/swoole": "1.*", - "utopia-php/system": "0.9.*", + "utopia-php/system": "0.10.*", "utopia-php/telemetry": "0.2.*", "utopia-php/vcs": "1.*", "utopia-php/websocket": "0.3.*", diff --git a/composer.lock b/composer.lock index e202fdf305..6041a4984a 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": "647d248df5990c3470fe85b7cce60498", + "content-hash": "917f9050c673379c91e46814c7c07f64", "packages": [ { "name": "adhocore/jwt", @@ -161,21 +161,21 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.19.2", + "version": "0.19.3", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "e5c142519df5aced37de9c302971c29c079ce3d9" + "reference": "bfe9f3c730c8c203c5e2d80bd5a81c9b1579714e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/runtimes/zipball/e5c142519df5aced37de9c302971c29c079ce3d9", - "reference": "e5c142519df5aced37de9c302971c29c079ce3d9", + "url": "https://api.github.com/repos/appwrite/runtimes/zipball/bfe9f3c730c8c203c5e2d80bd5a81c9b1579714e", + "reference": "bfe9f3c730c8c203c5e2d80bd5a81c9b1579714e", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/system": "0.9.*" + "utopia-php/system": "0.10.*" }, "require-dev": { "laravel/pint": "^1.15", @@ -210,9 +210,9 @@ ], "support": { "issues": "https://github.com/appwrite/runtimes/issues", - "source": "https://github.com/appwrite/runtimes/tree/0.19.2" + "source": "https://github.com/appwrite/runtimes/tree/0.19.3" }, - "time": "2025-11-11T13:44:44+00:00" + "time": "2026-02-06T10:08:18+00:00" }, { "name": "brick/math", @@ -5109,16 +5109,16 @@ }, { "name": "utopia-php/system", - "version": "0.9.0", + "version": "0.10.0", "source": { "type": "git", "url": "https://github.com/utopia-php/system.git", - "reference": "8e4a7edaf2dfeb4c9524e9f766d27754f2c4b64d" + "reference": "6441a9c180958a373e5ddb330264dd638539dfdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/system/zipball/8e4a7edaf2dfeb4c9524e9f766d27754f2c4b64d", - "reference": "8e4a7edaf2dfeb4c9524e9f766d27754f2c4b64d", + "url": "https://api.github.com/repos/utopia-php/system/zipball/6441a9c180958a373e5ddb330264dd638539dfdb", + "reference": "6441a9c180958a373e5ddb330264dd638539dfdb", "shasum": "" }, "require": { @@ -5126,7 +5126,7 @@ }, "require-dev": { "laravel/pint": "1.13.*", - "phpstan/phpstan": "1.10.*", + "phpstan/phpstan": "1.12.*", "phpunit/phpunit": "9.6.*" }, "type": "library", @@ -5159,9 +5159,9 @@ ], "support": { "issues": "https://github.com/utopia-php/system/issues", - "source": "https://github.com/utopia-php/system/tree/0.9.0" + "source": "https://github.com/utopia-php/system/tree/0.10.0" }, - "time": "2024-10-09T14:44:01+00:00" + "time": "2025-10-15T19:12:00+00:00" }, { "name": "utopia-php/telemetry", @@ -5265,16 +5265,16 @@ }, { "name": "utopia-php/vcs", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "3a427529dae09c0794541adac19062603d441e28" + "reference": "0f942f667319bb22cd420431e4e79107b41061f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/3a427529dae09c0794541adac19062603d441e28", - "reference": "3a427529dae09c0794541adac19062603d441e28", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/0f942f667319bb22cd420431e4e79107b41061f3", + "reference": "0f942f667319bb22cd420431e4e79107b41061f3", "shasum": "" }, "require": { @@ -5282,7 +5282,7 @@ "php": ">=8.0", "utopia-php/cache": "1.0.*", "utopia-php/framework": "0.*.*", - "utopia-php/system": "0.9.*" + "utopia-php/system": "0.10.*" }, "require-dev": { "laravel/pint": "1.*.*", @@ -5308,9 +5308,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/1.0.0" + "source": "https://github.com/utopia-php/vcs/tree/1.0.1" }, - "time": "2026-01-30T06:18:24+00:00" + "time": "2026-02-06T10:11:58+00:00" }, { "name": "utopia-php/websocket", From d864a61f9390819ebe1150a07a769e463324dbd2 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:16:37 +0000 Subject: [PATCH 12/19] chore: upgrade traefik --- app/views/install/compose.phtml | 2 +- docker-compose.yml | 2 +- tests/resources/docker/docker-compose.yml | 2 +- tests/unit/Docker/ComposeTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 16af33ca3a..0104a9d5a5 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -14,7 +14,7 @@ $image = $this->getParam('image', ''); $enableAssistant = $this->getParam('enableAssistant', false); ?>services: traefik: - image: traefik:2.11 + image: traefik:3.6 container_name: appwrite-traefik <<: *x-logging command: diff --git a/docker-compose.yml b/docker-compose.yml index 349a81e2de..37305055e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ x-logging: &x-logging services: traefik: - image: traefik:2.11 + image: traefik:3.6 <<: *x-logging container_name: appwrite-traefik command: diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml index 039ffb731a..c648b2f4c3 100644 --- a/tests/resources/docker/docker-compose.yml +++ b/tests/resources/docker/docker-compose.yml @@ -1,6 +1,6 @@ services: traefik: - image: traefik:2.2 + image: traefik:3.6 container_name: appwrite-traefik command: - --log.level=DEBUG diff --git a/tests/unit/Docker/ComposeTest.php b/tests/unit/Docker/ComposeTest.php index 93e0204264..74779230ac 100644 --- a/tests/unit/Docker/ComposeTest.php +++ b/tests/unit/Docker/ComposeTest.php @@ -26,7 +26,7 @@ class ComposeTest extends TestCase $this->assertCount(15, $this->object->getServices()); $this->assertEquals('appwrite', $this->object->getService('appwrite')->getContainerName()); $this->assertEquals('', $this->object->getService('appwrite')->getImageVersion()); - $this->assertEquals('2.2', $this->object->getService('traefik')->getImageVersion()); + $this->assertEquals('3.6', $this->object->getService('traefik')->getImageVersion()); $this->assertEquals(['2080' => '80', '2443' => '443', '8080' => '8080'], $this->object->getService('traefik')->getPorts()); } From a520a67da893b918ea1a38d00f34526af14f2cc1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 7 Feb 2026 21:19:00 +0530 Subject: [PATCH 13/19] fix: disable execution document writes for nyc region Temporarily skip all execution createDocument and updateDocument calls in the Functions worker when _APP_REGION is set to 'nyc' to reduce database load. Marked with TODO comments for removal. --- src/Appwrite/Platform/Workers/Functions.php | 42 +++++++++++++-------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index a0f4b65763..98e846aa4d 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -80,7 +80,9 @@ class Functions extends Action // Short-term solution to offhand write operation from API container if ($type === Func::TYPE_ASYNC_WRITE) { $execution = new Document($payload['execution'] ?? []); - $dbForProject->createDocument('executions', $execution); + if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check + $dbForProject->createDocument('executions', $execution); + } return; } @@ -309,10 +311,12 @@ class Functions extends Action 'duration' => 0.0, ]); - $execution = $dbForProject->createDocument('executions', $execution); + if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check + $execution = $dbForProject->createDocument('executions', $execution); - if ($execution->isEmpty()) { - throw new Exception('Failed to create execution'); + if ($execution->isEmpty()) { + throw new Exception('Failed to create execution'); + } } } @@ -451,22 +455,26 @@ class Functions extends Action 'duration' => 0.0, ]); - $execution = $dbForProject->createDocument('executions', $execution); + if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check + $execution = $dbForProject->createDocument('executions', $execution); - // TODO: @Meldiron Trigger executions.create event here + // TODO: @Meldiron Trigger executions.create event here - if ($execution->isEmpty()) { - throw new Exception('Failed to create or read execution'); + if ($execution->isEmpty()) { + throw new Exception('Failed to create or read execution'); + } } } if ($execution->getAttribute('status') !== 'processing') { $execution->setAttribute('status', 'processing'); - try { - $execution = $dbForProject->updateDocument('executions', $executionId, $execution); - } catch (\Throwable $e) { - $log->addExtra('updateError', $e->getMessage()); + if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check + try { + $execution = $dbForProject->updateDocument('executions', $executionId, $execution); + } catch (\Throwable $e) { + $log->addExtra('updateError', $e->getMessage()); + } } } @@ -611,10 +619,12 @@ class Functions extends Action $errorCode = $th->getCode(); } finally { /** Update execution status */ - try { - $execution = $dbForProject->updateDocument('executions', $executionId, $execution); - } catch (\Throwable $e) { - $log->addExtra('updateError', $e->getMessage()); + if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check + try { + $execution = $dbForProject->updateDocument('executions', $executionId, $execution); + } catch (\Throwable $e) { + $log->addExtra('updateError', $e->getMessage()); + } } /** Trigger usage queue */ From f1639e0b67e933f864b9ddf6be597586a61f9064 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 7 Feb 2026 21:42:10 +0530 Subject: [PATCH 14/19] fix: disable execution document writes for nyc region in API Temporarily skip execution createDocument calls in the Executions Create handler when _APP_REGION is set to 'nyc' to reduce database load. Marked with TODO comments for removal. --- .../Modules/Functions/Http/Executions/Create.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 6d2048b233..9265d72b48 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -301,7 +301,9 @@ class Create extends Base if ($async) { if (is_null($scheduledAt)) { - $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); + if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); + } $queueForFunctions ->setType('http') ->setExecution($execution) @@ -342,7 +344,9 @@ class Create extends Base ->setAttribute('scheduleInternalId', $schedule->getSequence()) ->setAttribute('scheduledAt', $scheduledAt); - $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); + if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); + } } $this->enqueueDeletes( @@ -501,7 +505,9 @@ class Create extends Base ->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) ; - $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); + if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check + $execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution)); + } } $executionResponse['headers']['x-appwrite-execution-id'] = $execution->getId(); From bed46a6bef1709002eb25c08e8673de31a21b7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 8 Feb 2026 14:12:23 +0100 Subject: [PATCH 15/19] Fix redirect url approval for oauth flow --- app/init/resources.php | 77 +++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index ccbb703f50..9227b0fe99 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -236,45 +236,68 @@ Http::setResource('allowedSchemes', function (Document $project) { * Rule associated with a request origin. */ Http::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { - $domain = \parse_url($request->getOrigin(), PHP_URL_HOST); - if (empty($domain)) { + $domains = []; + + $originDomain = \parse_url($request->getOrigin(), PHP_URL_HOST); + if (!empty($originDomain)) { + $domains[] = $originDomain; + } + + $refererDomain = \parse_url($request->getReferer(), PHP_URL_HOST); + if (!empty($refererDomain)) { + $domains[] = $refererDomain; + } + + if (\count($domains) === 0) { return new Document(); } - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { - if ($isMd5) { - return $dbForPlatform->getDocument('rules', md5($domain)); - } + $permittedRule = null; - return $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain]), - ]) ?? new Document(); - }); - - $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); - - // Temporary implementation until custom wildcard domains are an official feature - // Allow trusted projects; Used for Console (website) previews - if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) { - $trustedProjects = []; - foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { - if (empty($trustedProject)) { - continue; + foreach ($domains as $domain) { + // TODO: (@Meldiron) Remove after 1.7.x migration + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; + $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { + if ($isMd5) { + return $dbForPlatform->getDocument('rules', md5($domain)); } - $trustedProjects[] = $trustedProject; + + return $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain]), + ]) ?? new Document(); + }); + + if ($rule->isEmpty()) { + continue; } - if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { - $permitsCurrentProject = true; + + $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); + + // Temporary implementation until custom wildcard domains are an official feature + // Allow trusted projects; Used for Console (website) previews + if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) { + $trustedProjects = []; + foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { + if (empty($trustedProject)) { + continue; + } + $trustedProjects[] = $trustedProject; + } + if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { + $permitsCurrentProject = true; + } + } + + if ($permitsCurrentProject) { + $permittedRule = $rule; } } - if (!$permitsCurrentProject) { + if (\is_null($permittedRule)) { return new Document(); } - return $rule; + return $permittedRule; }, ['request', 'dbForPlatform', 'project', 'authorization']); /** From 3a0dc60a4cc638012338946a76e3b48bc9ce16ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 8 Feb 2026 14:36:34 +0100 Subject: [PATCH 16/19] Add test; fix implementation; pr reviews --- app/init/resources.php | 90 +++++++++---------- .../Projects/ProjectsConsoleClientTest.php | 85 ++++++++++++++++++ 2 files changed, 126 insertions(+), 49 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index 9227b0fe99..9fdfdedeba 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -198,15 +198,26 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje } $originHostname = parse_url($request->getOrigin(), PHP_URL_HOST); + $refererHostname = parse_url($request->getReferer(), PHP_URL_HOST); + + $hostname = $originHostname; + if (empty($hostname)) { + $hostname = $refererHostname; + } /* Add request hostname for preflight requests */ if ($request->getMethod() === 'OPTIONS') { - $allowed[] = $originHostname; + $allowed[] = $hostname; } - /* Allow the request origin if a dev key or rule is found */ - if ((!$rule->isEmpty() || !$devKey->isEmpty()) && !empty($originHostname)) { - $allowed[] = $originHostname; + /* Allow the request origin of rule */ + if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) { + $allowed[] = $rule->getAttribute('domain', ''); + } + + /* Allow the request origin if a dev key is found */ + if (!$devKey->isEmpty() && !empty($hostname)) { + $allowed[] = $hostname; } return array_unique($allowed); @@ -236,68 +247,49 @@ Http::setResource('allowedSchemes', function (Document $project) { * Rule associated with a request origin. */ Http::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) { - $domains = []; + $domain = \parse_url($request->getOrigin(), PHP_URL_HOST); - $originDomain = \parse_url($request->getOrigin(), PHP_URL_HOST); - if (!empty($originDomain)) { - $domains[] = $originDomain; + if (empty($domain)) { + $domain = \parse_url($request->getReferer(), PHP_URL_HOST); } - $refererDomain = \parse_url($request->getReferer(), PHP_URL_HOST); - if (!empty($refererDomain)) { - $domains[] = $refererDomain; - } - - if (\count($domains) === 0) { + if (empty($domain)) { return new Document(); } - $permittedRule = null; - - foreach ($domains as $domain) { - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { - if ($isMd5) { - return $dbForPlatform->getDocument('rules', md5($domain)); - } - - return $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain]), - ]) ?? new Document(); - }); - - if ($rule->isEmpty()) { - continue; + $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; + $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { + if ($isMd5) { + return $dbForPlatform->getDocument('rules', md5($domain)); } - $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); + return $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$domain]), + ]) ?? new Document(); + }); - // Temporary implementation until custom wildcard domains are an official feature - // Allow trusted projects; Used for Console (website) previews - if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) { - $trustedProjects = []; - foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { - if (empty($trustedProject)) { - continue; - } - $trustedProjects[] = $trustedProject; - } - if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { - $permitsCurrentProject = true; + $permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence(); + + // Temporary implementation until custom wildcard domains are an official feature + // Allow trusted projects; Used for Console (website) previews + if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) { + $trustedProjects = []; + foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) { + if (empty($trustedProject)) { + continue; } + $trustedProjects[] = $trustedProject; } - - if ($permitsCurrentProject) { - $permittedRule = $rule; + if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects)) { + $permitsCurrentProject = true; } } - if (\is_null($permittedRule)) { + if (!$permitsCurrentProject) { return new Document(); } - return $permittedRule; + return $rule; }, ['request', 'dbForPlatform', 'project', 'authorization']); /** diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 559ffe9f1d..460c9e365a 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -5116,6 +5116,91 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); } + public function testRuleOAuthRedirect(): void + { + // Prepare project + $projectId = $this->setupProject([ + 'projectId' => ID::unique(), + 'name' => 'testRuleOAuthRedirect', + 'region' => System::getEnv('_APP_REGION', 'default') + ]); + + $provider = 'mock'; + $appId = '1'; + $secret = '123456'; + + // Prepare OAuth provider + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/oauth2', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'provider' => $provider, + 'appId' => $appId, + 'secret' => $secret, + 'enabled' => true, + ]); + $this->assertEquals(200, $response['headers']['status-code']); + + // Prepare rule. In reality this is site rule, but for testing, API rule is enough, and faster to prepare + $domain = \uniqid() . '-with-rule.custom.localhost'; + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + ], $this->getHeaders()), [ + 'domain' => $domain + ]); + + $this->assertEquals(201, $rule['headers']['status-code']); + + // Ensure unknown domain cannot be redirect URL + $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'referer' => 'https://' . $domain, + ], [ + 'success' => 'https://domain-without-rule.com', + 'failure' => 'https://domain-without-rule.com' + ], followRedirects: false); + $this->assertEquals(400, $response['headers']['status-code']); + + // Ensure rule's domain can be redirect URL + $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'referer' => 'https://' . $domain, + ], [ + 'success' => 'https://' . $domain, + 'failure' => 'https://' . $domain + ], followRedirects: false); + $this->assertEquals(301, $response['headers']['status-code']); + + // Ensure unknown domain cannot be redirect URL + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'referer' => 'https://' . $domain, + ], [ + 'userId' => ID::unique(), + 'email' => 'user@appwrite.io', + 'url' => 'https://domain-without-rule.com', + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Ensure rule's domain can be redirect URL + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'referer' => 'https://' . $domain, + ], [ + 'userId' => ID::unique(), + 'email' => 'user@appwrite.io', + 'url' => 'https://' . $domain, + ]); + $this->assertEquals(201, $response['headers']['status-code']); + } + /** * @group abuseEnabled */ From fd323feae8d78596c03fbebd9ee3d83acc524df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 8 Feb 2026 14:37:35 +0100 Subject: [PATCH 17/19] Simplify diff --- app/init/resources.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/init/resources.php b/app/init/resources.php index 9fdfdedeba..8f78df1573 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -257,6 +257,7 @@ Http::setResource('rule', function (Request $request, Database $dbForPlatform, D return new Document(); } + // TODO: (@Meldiron) Remove after 1.7.x migration $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; $rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) { if ($isMd5) { From 94581adfcb2085a3abf1d3345ed9735034210fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 8 Feb 2026 14:44:21 +0100 Subject: [PATCH 18/19] Improve dev key tests --- .../Projects/ProjectsConsoleClientTest.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 460c9e365a..9c9b93d43b 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -5091,6 +5091,31 @@ class ProjectsConsoleClientTest extends Scope 'failure' => 'https://example.com' ]); $this->assertEquals(200, $response['headers']['status-code']); + + /** Ensure any hostname is allowed */ + $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-dev-key' => $devKey['secret'], + 'origin' => '', + 'referer' => 'https://domain-without-rule.com' + ], [ + 'success' => 'https://domain-without-rule.com', + 'failure' => 'https://domain-without-rule.com' + ], followRedirects: false); + $this->assertEquals(301, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-dev-key' => $devKey['secret'], + 'referer' => '', + 'origin' => 'https://domain-without-rule.com' + ], [ + 'success' => 'https://domain-without-rule.com', + 'failure' => 'https://domain-without-rule.com' + ], followRedirects: false); + $this->assertEquals(301, $response['headers']['status-code']); /** Test hostname in Magic URL */ $response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [ @@ -5159,6 +5184,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'referer' => 'https://' . $domain, + 'origin' => '', ], [ 'success' => 'https://domain-without-rule.com', 'failure' => 'https://domain-without-rule.com' @@ -5170,6 +5196,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'referer' => 'https://' . $domain, + 'origin' => '', ], [ 'success' => 'https://' . $domain, 'failure' => 'https://' . $domain @@ -5181,6 +5208,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'referer' => 'https://' . $domain, + 'origin' => '', ], [ 'userId' => ID::unique(), 'email' => 'user@appwrite.io', @@ -5193,6 +5221,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $projectId, 'referer' => 'https://' . $domain, + 'origin' => '', ], [ 'userId' => ID::unique(), 'email' => 'user@appwrite.io', From 801707c4072bc90c2a20b4a5b9924d9379252054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 8 Feb 2026 14:47:47 +0100 Subject: [PATCH 19/19] Linter fix --- tests/e2e/Services/Projects/ProjectsConsoleClientTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 9c9b93d43b..e2e5621662 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -5091,7 +5091,7 @@ class ProjectsConsoleClientTest extends Scope 'failure' => 'https://example.com' ]); $this->assertEquals(200, $response['headers']['status-code']); - + /** Ensure any hostname is allowed */ $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [ 'content-type' => 'application/json', @@ -5104,7 +5104,7 @@ class ProjectsConsoleClientTest extends Scope 'failure' => 'https://domain-without-rule.com' ], followRedirects: false); $this->assertEquals(301, $response['headers']['status-code']); - + $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [ 'content-type' => 'application/json', 'x-appwrite-project' => $projectId,