Compare commits

...
Author SHA1 Message Date
loks0nandClaude Opus 4.7 e97dfb041a Bump utopia-php/http to 2.0-rc1 and utopia-php/platform to 1.0-rc1
These RC releases ship the new resources/context API that this PR
adopts. Drop the dev-branch alias on utopia-php/http now that 2.0.0-rc1
is tagged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:55:41 +01:00
loks0nandClaude Opus 4.7 1cef07a2d4 Fix GraphQL coroutine context propagation
Promises\Swoole was already propagating the request container into child
coroutines, but using its own pre-existing key (__utopia_http_request_container)
rather than the new utopia-php/http key (__utopia__). With the keys
mismatched, the propagation was dead code and resolvers spawned from
webonyx coroutines saw an empty context, failing every GraphQL query
with 'Dependency utopia:graphql not found'.

Align the key so child coroutines actually inherit the request's
container (wrapped as a child Container, so request-scoped overrides
in the resolver can't bleed back into the outer request).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:47:52 +01:00
loks0nandClaude Opus 4.7 93e96a044d Fix bus resolver to look up context container per dispatch
The resources/context migration replaced the resolver closure with a
first-class callable from `$swoole->context()->get(...)`. That
captures the result of `$swoole->context()` once -- on the first
dispatch -- and binds the bus to the first request's per-request
container forever after.

Wrap the lookup in a fresh closure so each dispatch re-resolves
`context()` against the current Swoole coroutine. Restores the
behavior of the pre-migration `getContainer()` form.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 11:47:52 +01:00
loks0nandClaude Opus 4.7 040ce6e335 Simplify Swoole worker bootstrap
EVENT_START and EVENT_TASK were each constructing `new Http($swoole, 'UTC')`
purely as a vehicle to reach DI -- never configuring routing, never calling
->run(). Now that the Swoole adapter exposes resources() directly, the
Http instance is no longer needed: the resources container we passed into
the Server constructor is already in scope as $container.

Drop the throwaway Http construction, pass $container straight through, and
update createDatabase() to take Container instead of Http to match. Also
rename $swooleAdapter to $swoole for consistency with the rest of the file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:47:51 +01:00
loks0nandClaude Opus 4.7 4bdcdb6f96 Migrate to utopia-php/http resources/context API
Adopts the new split DI containers in utopia-php/http: `resources()` for
boot-time wiring (shared across requests) and `context()` for per-request
state. Replaces the removed `getResource()`/`setResource()`/`getContainer()`
helpers throughout the HTTP entry point, controllers, GraphQL layer, and
installer.

Bumps the dependency chain accordingly: utopia-php/http to the dev branch
(aliased to 0.34.25 to satisfy platform's exact pin), servers 0.4.*,
queue 0.18.*, and pulled-along cli/platform/database upgrades.

Also tightens app/init/resources/request.php by collapsing single-return
factories to arrow functions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:41:42 +01:00
Matej BačoandGitHub bbd24e5029 Merge pull request #12285 from appwrite/fix-schedule-doc-missing
Fix: scheduler doc missing for functions
2026-05-12 11:26:13 +02:00
Matej Bačo 8bef181ad8 Fix scheduler doc missing for functions 2026-05-12 10:44:16 +02:00
Matej Bačo ff92fd229b Revert "Fix function scheduler document missing"
This reverts commit 5ef5ead98f.
2026-05-12 10:43:11 +02:00
Matej Bačo 5ef5ead98f Fix function scheduler document missing 2026-05-12 10:42:56 +02:00
Atharva DeosthaleandGitHub 668b61e620 Merge pull request #12279 from appwrite/fix-codex-plugin
fix codex plugin
2026-05-12 12:29:06 +05:30
Jake BarnbyandGitHub ea28f12fb4 Merge pull request #12280 from appwrite/fix-migration-stuck-on-early-failure
fix(migrations): write _APP_MIGRATION_HOST in the installer-generated .env
2026-05-12 17:08:45 +12:00
Damodar LohaniandGitHub 45298f8e23 Merge pull request #12282 from appwrite/CLO-4320-fix-project-delete-event
Fix: remove invalid event label from project delete action
2026-05-12 10:47:33 +05:45
Damodar Lohani 0e87d0b483 fix: remove invalid event label from project delete action
Event::generateEvents() rejects the pattern 'project.delete' because
it parses 'delete' as a resource that must be present in route params.
The pre-migration route did not declare an event label, so functions
and webhooks were never triggered on project deletion. Restore that
behavior by removing the label.
2026-05-12 04:50:58 +00:00
Chirag AggarwalandGitHub fde30794ec Merge pull request #12246 from appwrite/refactor/mails-messaging-publishers
Migrate mails and messaging queues to publishers
2026-05-12 09:58:56 +05:30
premtsd-codeandGitHub 1f5ee53f3a Merge branch '1.9.x' into fix-migration-stuck-on-early-failure 2026-05-11 21:29:40 +01:00
Prem Palanisamy 3721e6b950 fix(migrations): write _APP_MIGRATION_HOST in generated .env (install & upgrade)
The migrations worker (Appwrite→Appwrite migrations + CSV/JSON imports
& exports) reads `_APP_MIGRATION_HOST` to call back into this instance's
API. `_APP_MIGRATION_HOST` was introduced in #11229 (1.8.x / 1.9.x) but
was never added to `app/config/variables.php`, so `appwrite install` /
`appwrite upgrade` never write it into the generated `.env`. With the
var unset, the migrations worker fails — on a fresh self-hosted install
the first export hangs with no error (#11853). (Contributors and CI
don't hit it because the repo's hand-maintained `.env` already has
`_APP_MIGRATION_HOST=appwrite`; only the *installer-generated* `.env` is
missing it.)

Add `_APP_MIGRATION_HOST` to `app/config/variables.php` with default
`appwrite` — the API service name in the standard Docker Compose setup,
which is what the repo's own `.env` and the cloud Helm charts already
use, and what the migration endpoint was hardcoded to before #11229.
`appwrite install` and `appwrite upgrade` now write it into the
generated `.env`, so fresh installs and upgrades have it set and the
migration/import/export flows work.

Scope: this PR fixes the install & upgrade paths only — it deliberately
doesn't change the worker code.

Fixes #11853
2026-05-11 21:26:26 +01:00
Levi van NoortandGitHub 103e219675 Merge pull request #12274 from appwrite/migrate-away-from-blacksmith-based-runners
refactor: enhance execution log checks in SitesCustomServerTest
2026-05-11 21:35:42 +02:00
Levi van Noort 994a4e0fc7 chore: remove 'family=m7' from e2e_service runner configuration 2026-05-11 20:11:08 +02:00
Levi van Noort 5fba2889cb chore: update e2e_service runner configuration to use m7 family 2026-05-11 16:58:36 +02:00
Levi van NoortandGitHub 9b4f8d7ac0 Merge branch '1.9.x' into migrate-away-from-blacksmith-based-runners 2026-05-11 16:16:55 +02:00
Atharva Deosthale 9056de0103 fix codex plugin 2026-05-11 19:38:06 +05:30
Chirag Aggarwal cf1bb1a1cc build 2026-05-11 17:59:43 +05:30
Chirag AggarwalandGitHub 330b11a64b Merge branch '1.9.x' into refactor/mails-messaging-publishers 2026-05-11 17:43:48 +05:30
Jake BarnbyandGitHub 11acbc6db4 Merge pull request #12245 from appwrite/feat-bump-sdk-23
feat: bump utopia-php/abuse and utopia-php/migration to feat/sdk-23 branches
2026-05-11 23:30:10 +12:00
Levi van Noort d8dbe15cb3 chore: update runner configuration for e2e_service jobs in ci workflow 2026-05-11 13:12:25 +02:00
Chirag AggarwalandGitHub 6ae113a969 Merge branch '1.9.x' into refactor/mails-messaging-publishers 2026-05-11 16:24:50 +05:30
premtsd-codeandGitHub 0b2a9240b3 Merge branch '1.9.x' into feat-bump-sdk-23 2026-05-11 10:28:38 +01:00
Levi van Noort d949482094 chore: set bigger size on database related e2e 2026-05-11 11:10:41 +02:00
Levi van NoortandGitHub c95901b879 Merge branch '1.9.x' into migrate-away-from-blacksmith-based-runners 2026-05-11 10:50:29 +02:00
Levi van Noort 0fb2e208ab chore: add spot=false label to the runs-on based runners 2026-05-11 10:47:22 +02:00
Matej BačoandGitHub 76aed33a5c Merge pull request #12263 from appwrite/feat-google-oauth-prompt-param
Feat: Google OAuth2 "prompt" param
2026-05-11 10:47:13 +02:00
Prem Palanisamy c26ff1849a chore: bump utopia-php/abuse to 1.3.0 and migration to 1.11.0 (released) 2026-05-11 09:37:07 +01:00
Levi van Noort 702a8a83a0 test: add assertion for action execution logs content in SitesCustomServerTest 2026-05-11 10:30:08 +02:00
Levi van NoortandGitHub 5be05da5ca Merge branch '1.9.x' into migrate-away-from-blacksmith-based-runners 2026-05-11 10:27:46 +02:00
Levi van Noort fe5e5b8891 refactor: enhance execution log checks in SitesCustomServerTest 2026-05-11 10:17:55 +02:00
Prem Palanisamy a902c25363 refactor(migrations): extract destination DSN resolver to named method
Replaces the inline match closure with resolveDestinationDatabaseDsn(),
mirroring cloud's worker. Adds a docblock explaining why documentsdb /
vectorsdb keep the source DSN.
2026-05-11 06:52:23 +01:00
premtsd-codeandGitHub 04a6eaf5d4 Merge branch '1.9.x' into feat-bump-sdk-23 2026-05-11 05:54:00 +01:00
Prem Palanisamy f0fb7bf877 test: remove orphaned VectorsDB testGetCollectionLogs
Endpoint deleted in 96fe989f6d ("update composer dependencies and remove
obsolete log classes") but the two test methods calling it were left
behind. They have been failing with 404 on every PR since.
2026-05-11 05:53:03 +01:00
Prem Palanisamy 74fbbea2b3 chore: bump utopia-php/abuse cfd290a + migration 447a987 (PHP >=8.2) 2026-05-11 05:41:37 +01:00
Prem Palanisamy 4957f568cd chore: bump utopia-php/migration to 4dc7270 (empty teams guard) 2026-05-11 02:38:01 +01:00
Prem Palanisamy 64c9d8d85f chore: bump utopia-php/migration to a9bdfba (revert deno fallback) 2026-05-11 02:28:07 +01:00
Prem Palanisamy 7b5cb379c4 chore: bump utopia-php/migration to e5dc657 (deno runtime fallback) 2026-05-11 02:21:18 +01:00
Prem Palanisamy fd625fca7b fix(migrations): preserve source DSN for documentsdb/vectorsdb resolver
Migration lib 1.10.2's getDatabaseDSN resolver returns the value
written into destination's _databases.database. The previous resolver
always returned the project's main DSN (mongodb in default CI) for
every database type, including documentsdb / vectorsdb — which are
routed to their own adapters (mongodb / postgresql) per
_APP_DB_ADAPTER_DOCUMENTSDB / _APP_DB_ADAPTER_VECTORSDB.

The wrong DSN routed vectorsdb attribute creates back to mongodb,
producing 'Vector types are not supported by the current database'
on the MixedDatabases / VectorsDB migration tests.

Mirror cloud's resolver: keep the source DSN for documentsdb /
vectorsdb (they target dedicated hosts), use destination project's
main DSN otherwise.
2026-05-10 22:09:39 +01:00
Matej Bačo e45e5a09f4 Reorder tests to make them pass 2026-05-10 13:19:18 +02:00
Matej Bačo e3dc30ad93 PR review fixes 2026-05-10 12:03:05 +02:00
Matej Bačo 0406d9e04d improve copy 2026-05-10 11:13:23 +02:00
Matej Bačo fbfde6cc77 Implement google oauth prompt param. 2026-05-10 11:11:47 +02:00
Matej BačoandGitHub c6f91e18c4 Merge pull request #12254 from appwrite/fix-sateless-git-hints
Fix: Stateless git hints
2026-05-10 10:14:51 +02:00
Matej BačoandGitHub 6db1d2e5c0 Merge pull request #12256 from appwrite/chore-google-oauth-dual-read
Chore: Dual read for google oauth secret
2026-05-10 10:14:30 +02:00
Prem Palanisamy 817172c460 Merge branch '1.9.x' into feat-bump-sdk-23
# Conflicts:
#	composer.lock
2026-05-10 05:54:06 +01:00
Prem Palanisamy cd445ceccf chore: bump utopia-php/migration to 6deabc6 (Sites::create named args) 2026-05-10 05:49:23 +01:00
Chirag AggarwalandGitHub 87a32f65ee Merge pull request #12182 from appwrite/add-codex-plugin 2026-05-10 09:26:49 +05:30
Prem Palanisamy 698fde247f chore: bump utopia-php/migration to 0e88268 (createVariable variableId fix) 2026-05-10 04:46:40 +01:00
Matej Bačo 0e939ea9d7 PR review fixes 2026-05-09 12:58:47 +02:00
Matej Bačo a5ddc465e6 PR review fixes 2026-05-09 12:53:11 +02:00
Matej Bačo 76a41d70b0 Dual read for google oauth secret
Will allow future support for more params
2026-05-09 10:51:46 +02:00
Matej Bačo 43777ee6d9 Add unit tests for github hints 2026-05-09 10:16:19 +02:00
Matej Bačo 6ee2196fae Fix git hint regnerating nonstop 2026-05-09 09:54:54 +02:00
Prem Palanisamy 5313460c7c fix(migrations): pass destination project DSN resolver to DestinationAppwrite
Migration lib's `_databases.database` resolver now defaults to empty
when no callable is supplied (utopia-php/migration ff3b444). The
runtime falls back via `$project->getAttribute('database')`, but tests
hit DSN("mysql://") and a 500 because that fallback is also empty for
fresh test projects.

Pass an explicit resolver returning the destination project's `database`
attribute so migrated databases store a usable DSN.
2026-05-08 17:53:09 +01:00
Prem Palanisamy c94ae409e8 chore: refresh composer.lock for utopia-php/fetch ^1.1 and logger 0.8.* 2026-05-08 16:50:04 +01:00
Prem Palanisamy 4844a4bf99 Merge branch '1.9.x' into feat-bump-sdk-23
# Conflicts:
#	composer.lock
2026-05-08 16:34:19 +01:00
Prem Palanisamy 687698001f chore: bump utopia-php/migration to 80e9a04 (SDK 23 nested typed object fixes) 2026-05-08 16:30:02 +01:00
Harsh MahajanandGitHub acc0b2c184 Merge pull request #12252 from appwrite/fix/repository-branch-pagination-validator
fix: Support branch query validators in SDK generation
2026-05-08 19:18:04 +05:30
harsh mahajan 10e4341db2 Support branch query validators in SDK generation 2026-05-08 19:15:17 +05:30
Chirag AggarwalandGitHub 8a35e87cfd Merge pull request #12251 from appwrite/fix/graphql-preview-test-assertions 2026-05-08 18:13:14 +05:30
Chirag Aggarwal 9295869279 Fix GraphQL preview test assertions 2026-05-08 17:51:42 +05:30
Chirag AggarwalandGitHub 5472bff265 Merge pull request #12219 from abhay-dev2901/fix/scheduler-platform-env 2026-05-08 17:17:43 +05:30
AbhayandGitHub 27de465032 Merge branch '1.9.x' into fix/scheduler-platform-env 2026-05-08 17:14:42 +05:30
Chirag AggarwalandGitHub f2a59de804 Merge branch '1.9.x' into refactor/mails-messaging-publishers 2026-05-08 17:13:49 +05:30
Prem Palanisamy 4563706b76 chore: bump utopia-php/migration to a5195ba (SDK 23 typed models refactor) 2026-05-08 12:38:20 +01:00
Chirag AggarwalandGitHub be9bb5e34d Merge pull request #12249 from appwrite/fix/preview-conditional-transforms 2026-05-08 16:40:56 +05:30
Chirag AggarwalandGitHub 0a582daff0 Merge pull request #12248 from appwrite/codex/bump-logger-0.8.0 2026-05-08 16:38:05 +05:30
Torsten DittmannandGitHub 003647abd7 Merge branch '1.9.x' into fix/preview-conditional-transforms 2026-05-08 15:06:08 +04:00
Torsten Dittmann 9100e06bbe perf(storage): skip no-op imagick transforms and add transform value spans
- Only call crop() when dimensions > 0 or gravity differs from center
- Only call setOpacity() when opacity !== 1.0 (was always called due to !empty(1.0))
- Only call setBorder() when borderWidth > 0
- Only call setBorderRadius() when borderRadius > 0
- Only call setRotation() when rotation !== 0
- Add Span::add() with actual values inside each transform condition
- Leave output() unconditional (format conversion always applies)
2026-05-08 15:04:54 +04:00
Chirag Aggarwal fc83b4d986 Bump logger dependency 2026-05-08 16:25:46 +05:30
Harsh MahajanandGitHub 4954655e8b Merge pull request #12243 from appwrite/feat/repository-branch-search-pagination
feat: Add search and pagination for repository branches
2026-05-08 16:14:06 +05:30
ArnabChatterjee20kandGitHub 319483d41d Merge pull request #12244 from appwrite/fix-method-ids
Enhance URL parameter handling in OpenAPI3 and Swagger2 formats to su…
2026-05-08 15:35:05 +05:30
ArnabChatterjee20k 6da8c1cb12 updated 2026-05-08 15:21:52 +05:30
Aditya Oberai 5f6b1dda1a update composer lock 2026-05-08 09:35:38 +00:00
Aditya Oberai 8df7a628c8 Merge branch '1.9.x' into add-codex-plugin 2026-05-08 09:34:43 +00:00
Chirag Aggarwal 18ec96f124 Address Greptile feedback for queue publishers 2026-05-08 14:41:58 +05:30
ArnabChatterjee20k d303d6f807 Refactor path parameter detection in OpenAPI3 and Swagger2 by utilizing array flipping for improved performance and clarity in matching aliases. 2026-05-08 14:39:26 +05:30
Chirag Aggarwal 34075322d7 Migrate mails and messaging queues to publishers 2026-05-08 14:32:11 +05:30
Prem Palanisamy 74b84ba38c feat: bump utopia-php/abuse and utopia-php/migration to feat/sdk-23 branches 2026-05-08 09:52:47 +01:00
ArnabChatterjee20k 07973dee2d Refactor URL parameter replacement logic in OpenAPI3 and Swagger2 to ensure accurate matching of path parameters by checking for trailing characters. 2026-05-08 14:19:51 +05:30
ArnabChatterjee20k 5cfaa0807d Refactor URL parameter matching in OpenAPI3 and Swagger2 to improve path parameter detection by checking for trailing characters. 2026-05-08 14:08:27 +05:30
ArnabChatterjee20k e181954dd1 removed regex 2026-05-08 14:01:04 +05:30
ArnabChatterjee20k 4b05a6cf8f Refactor URL parameter matching in OpenAPI3 and Swagger2 to use preg_match for improved accuracy with path aliases. 2026-05-08 13:52:25 +05:30
ArnabChatterjee20k 29bbc7299a Enhance URL parameter handling in OpenAPI3 and Swagger2 formats to support aliases for path parameters. 2026-05-08 13:45:46 +05:30
abhay-dev2901 f36105a7af fix: pass platform env vars to function schedulers 2026-05-05 17:23:50 +05:30
adityaoberai 7648978cfa Add codex plugin to SDKs 2026-04-29 19:22:46 +05:30
56 changed files with 1544 additions and 1617 deletions
+2 -11
View File
@@ -406,7 +406,7 @@ jobs:
e2e_service:
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
runs-on: ${{ matrix.runner || format('runs-on={0}/runner=4cpu-linux-x64/volume=120g/spot=false', github.run_id) }}
needs: [build, matrix]
permissions:
contents: read
@@ -445,19 +445,10 @@ jobs:
]
include:
- service: Databases
runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g
runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/volume=120g/spot=false
paratest_processes: 3
timeout_minutes: 30
- service: Sites
runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g
- service: Functions
runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g
- service: Avatars
runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g
- service: Realtime
runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g
- service: TablesDB
runner: runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/family=c7/volume=120g
paratest_processes: 3
timeout_minutes: 30
- service: Migrations
+20
View File
@@ -320,6 +320,26 @@ return [
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
],
[
'key' => 'codex-plugin',
'name' => 'CodexPlugin',
'version' => '0.1.1',
'url' => 'https://github.com/appwrite/codex-plugin.git',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'spec' => 'static',
'family' => APP_SDK_PLATFORM_STATIC,
'prism' => 'codex-plugin',
'source' => \realpath(__DIR__ . '/../sdks/static-codex-plugin'),
'gitUrl' => 'git@github.com:appwrite/codex-plugin.git',
'gitRepoName' => 'codex-plugin',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/codex-plugin/CHANGELOG.md'),
],
],
],
+9
View File
@@ -1336,6 +1336,15 @@ return [
'category' => 'Migrations',
'description' => '',
'variables' => [
[
'name' => '_APP_MIGRATION_HOST',
'description' => 'Internal hostname the migrations worker uses to reach this instance\'s API (for migrations and CSV/JSON imports & exports). Defaults to \'appwrite\', the API service name in the standard Docker Compose setup. Only change this for non-standard deployments.',
'introduction' => '1.9.0',
'default' => 'appwrite',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_MIGRATIONS_FIREBASE_CLIENT_ID',
'description' => 'Google OAuth client ID. You can find it in your GCP application settings.',
+126 -125
View File
@@ -13,8 +13,10 @@ use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Redirect;
@@ -2113,12 +2115,12 @@ Http::post('/v1/account/tokens/magic-url')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->inject('publisherForMails')
->inject('plan')
->inject('proofForPassword')
->inject('platform')
->inject('authorization')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2304,6 +2306,7 @@ Http::post('/v1/account/tokens/magic-url')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2321,13 +2324,6 @@ Http::post('/v1/account/tokens/magic-url')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -2348,11 +2344,17 @@ Http::post('/v1/account/tokens/magic-url')
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$projectName = $project->getAttribute('name');
@@ -2374,18 +2376,17 @@ Http::post('/v1/account/tokens/magic-url')
'team' => '',
];
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->appendVariables($emailVariables)
->setRecipient($email);
if ($project->getId() === 'console') {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $email,
subject: $subject,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$token->setAttribute('secret', $tokenSecret);
@@ -2436,12 +2437,12 @@ Http::post('/v1/account/tokens/email')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->inject('publisherForMails')
->inject('plan')
->inject('proofForPassword')
->inject('proofForCode')
->inject('authorization')
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2633,6 +2634,7 @@ Http::post('/v1/account/tokens/email')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2650,13 +2652,6 @@ Http::post('/v1/account/tokens/email')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -2677,11 +2672,17 @@ Http::post('/v1/account/tokens/email')
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$projectName = $project->getAttribute('name');
@@ -2717,20 +2718,18 @@ Http::post('/v1/account/tokens/email')
]);
}
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate($bodyTemplate)
->appendVariables($emailVariables)
->setRecipient($email);
// since this is console project, set email sender name!
if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $email,
subject: $subject,
bodyTemplate: $bodyTemplate,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$token->setAttribute('secret', $tokenSecret);
@@ -2880,7 +2879,7 @@ Http::post('/v1/account/tokens/phone')
->inject('platform')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('locale')
->inject('timelimit')
->inject('usage')
@@ -2888,7 +2887,7 @@ Http::post('/v1/account/tokens/phone')
->inject('store')
->inject('proofForCode')
->inject('authorization')
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, MessagingPublisher $publisherForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -3021,11 +3020,13 @@ Http::post('/v1/account/tokens/phone')
],
]);
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS);
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_INTERNAL,
project: $project,
message: $messageDoc,
recipients: [$phone],
providerType: MESSAGE_TYPE_SMS,
));
$helper = PhoneNumberUtil::getInstance();
try {
@@ -3681,11 +3682,11 @@ Http::post('/v1/account/recovery')
->inject('project')
->inject('platform')
->inject('locale')
->inject('queueForMails')
->inject('publisherForMails')
->inject('queueForEvents')
->inject('proofForToken')
->inject('authorization')
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, MailPublisher $publisherForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -3768,6 +3769,7 @@ Http::post('/v1/account/recovery')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -3785,13 +3787,6 @@ Http::post('/v1/account/recovery')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -3812,11 +3807,17 @@ Http::post('/v1/account/recovery')
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$emailVariables = [
@@ -3829,19 +3830,18 @@ Http::post('/v1/account/recovery')
'team' => ''
];
$queueForMails
->setRecipient($profile->getAttribute('email', ''))
->setName($profile->getAttribute('name', ''))
->setBody($body)
->appendVariables($emailVariables)
->setSubject($subject)
->setPreview($preview);
if ($project->getId() === 'console') {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $profile->getAttribute('email', ''),
name: $profile->getAttribute('name', ''),
subject: $subject,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$recovery->setAttribute('secret', $secret);
@@ -4009,10 +4009,10 @@ Http::post('/v1/account/verifications/email')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->inject('publisherForMails')
->inject('proofForToken')
->inject('authorization')
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -4099,6 +4099,7 @@ Http::post('/v1/account/verifications/email')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -4116,13 +4117,6 @@ Http::post('/v1/account/verifications/email')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -4143,11 +4137,17 @@ Http::post('/v1/account/verifications/email')
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$emailVariables = [
@@ -4174,20 +4174,19 @@ Http::post('/v1/account/verifications/email')
]);
}
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate($bodyTemplate)
->appendVariables($emailVariables)
->setRecipient($user->getAttribute('email'))
->setName($user->getAttribute('name') ?? '');
if ($project->getId() === 'console') {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $user->getAttribute('email'),
name: $user->getAttribute('name') ?? '',
subject: $subject,
bodyTemplate: $bodyTemplate,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$verification->setAttribute('secret', $verificationSecret);
@@ -4321,7 +4320,7 @@ Http::post('/v1/account/verifications/phone')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('project')
->inject('locale')
->inject('timelimit')
@@ -4329,7 +4328,7 @@ Http::post('/v1/account/verifications/phone')
->inject('plan')
->inject('proofForCode')
->inject('authorization')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, MessagingPublisher $publisherForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -4398,11 +4397,13 @@ Http::post('/v1/account/verifications/phone')
],
]);
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage($messageDoc)
->setRecipients([$user->getAttribute('phone')])
->setProviderType(MESSAGE_TYPE_SMS);
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_INTERNAL,
project: $project,
message: $messageDoc,
recipients: [$user->getAttribute('phone')],
providerType: MESSAGE_TYPE_SMS,
));
$helper = PhoneNumberUtil::getInstance();
try {
+44 -31
View File
@@ -5,7 +5,8 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Messaging;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
use Appwrite\Permission;
@@ -3187,9 +3188,9 @@ Http::post('/v1/messaging/messages/email')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3274,9 +3275,11 @@ Http::post('/v1/messaging/messages/email')
switch ($status) {
case MessageStatus::PROCESSING:
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3362,9 +3365,9 @@ Http::post('/v1/messaging/messages/sms')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3418,9 +3421,11 @@ Http::post('/v1/messaging/messages/sms')
switch ($status) {
case MessageStatus::PROCESSING:
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3498,10 +3503,10 @@ Http::post('/v1/messaging/messages/push')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->inject('platform')
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3638,9 +3643,11 @@ Http::post('/v1/messaging/messages/push')
switch ($status) {
case MessageStatus::PROCESSING:
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3983,9 +3990,9 @@ Http::patch('/v1/messaging/messages/email/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4141,9 +4148,11 @@ Http::patch('/v1/messaging/messages/email/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
}
$queueForEvents
@@ -4205,9 +4214,9 @@ Http::patch('/v1/messaging/messages/sms/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4323,9 +4332,11 @@ Http::patch('/v1/messaging/messages/sms/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
}
$queueForEvents
@@ -4379,10 +4390,10 @@ Http::patch('/v1/messaging/messages/push/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->inject('platform')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4584,9 +4595,11 @@ Http::patch('/v1/messaging/messages/push/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
}
$queueForEvents
+3 -3
View File
@@ -1274,7 +1274,7 @@ Http::error()
if (!$publish && $project->getId() !== 'console') {
$errorUser = new DBUser();
try {
$resolvedUser = $utopia->getResource('user');
$resolvedUser = $utopia->context()->get('user');
if ($resolvedUser instanceof DBUser) {
$errorUser = $resolvedUser;
}
@@ -1293,7 +1293,7 @@ Http::error()
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
$user = $utopia->context()->get('user');
} catch (\Throwable) {
// All good, user is optional information for logger
}
@@ -1494,7 +1494,7 @@ Http::error()
// the cors resource (which depends on rule -> DB) would cascade.
// Uses override:true to avoid duplicate headers if init() already set them.
try {
$cors = $utopia->getResource('cors');
$cors = $utopia->context()->get('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response
->removeHeader($name)
+2 -14
View File
@@ -8,10 +8,8 @@ use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
@@ -486,13 +484,11 @@ Http::init()
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('auditContext')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('usage')
->inject('queueForFunctions')
->inject('queueForMails')
->inject('dbForProject')
->inject('timelimit')
->inject('resourceToken')
@@ -504,7 +500,7 @@ Http::init()
->inject('platform')
->inject('authorization')
->inject('cacheControlForStorage')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
$response->setUser($user);
$request->setUser($user);
@@ -617,13 +613,10 @@ Http::init()
/* Auto-set projects */
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
$queueForMessaging->setProject($project);
$queueForFunctions->setProject($project);
$queueForMails->setProject($project);
/* Auto-set platforms */
$queueForFunctions->setPlatform($platform);
$queueForMails->setPlatform($platform);
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
@@ -815,7 +808,6 @@ Http::shutdown()
->inject('publisherForUsage')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForMessaging')
->inject('queueForFunctions')
->inject('queueForWebhooks')
->inject('queueForRealtime')
@@ -826,7 +818,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -975,10 +967,6 @@ Http::shutdown()
$queueForDatabase->trigger();
}
if (! empty($queueForMessaging->getType())) {
$queueForMessaging->trigger();
}
// Cache label
$useCache = $route->getLabel('cache', false);
if ($useCache) {
+31 -39
View File
@@ -3,7 +3,7 @@
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
$setRequestContext = require __DIR__ . '/init/resources/request.php';
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@@ -26,6 +26,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\DI\Container;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
@@ -57,7 +58,7 @@ $container->set('pools', function ($register) {
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$swooleAdapter = new Server(
$swoole = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
@@ -69,10 +70,10 @@ $swooleAdapter = new Server(
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
container: $container,
resources: $container,
);
$http = $swooleAdapter->getServer();
$http = $swoole->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
@@ -190,13 +191,11 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
$container->set('bus', function ($register) use ($swooleAdapter) {
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
}, ['register']);
$container->set('bus', fn ($register) => $register->get('bus')->setResolver(fn (string $name) => $swoole->context()->get($name)), ['register']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
function createDatabase(Container $resources, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
@@ -205,7 +204,7 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
while (true) {
try {
$attempts++;
$resource = $app->getResource($resourceKey);
$resource = $resources->get($resourceKey);
/* @var $database Database */
$database = is_callable($resource) ? $resource() : $resource;
break; // exit loop on success
@@ -288,23 +287,21 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
Span::current()?->finish();
}
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
$app = new Http($swooleAdapter, 'UTC');
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $container) {
/** @var \Utopia\Pools\Group $pools */
$pools = $app->getResource('pools');
$pools = $container->get('pools');
go(function () use ($app, $pools) {
go(function () use ($container, $pools) {
/** @var array $collections */
$collections = Config::getParam('collections', []);
// create logs database first, `getLogsDB` is a callable.
createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
createDatabase($container, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call.
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) {
$authorization = $app->getResource('authorization');
createDatabase($container, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $container) {
$authorization = $container->get('authorization');
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform);
@@ -416,7 +413,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
$cache = $app->getResource('cache');
$cache = $container->get('cache');
// All shared tables pools that need project metadata collections
$allSharedTables = \array_values(\array_unique(\array_filter([
@@ -502,7 +499,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
});
});
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
$swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoole, $setRequestContext) {
Span::init('http.request');
$request = new Request($utopiaRequest->getSwooleRequest());
@@ -522,21 +519,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
return;
}
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
$app = new Http($swoole, 'UTC');
$app->context()->set('request', fn () => $request);
$app->context()->set('response', fn () => $response);
$app->context()->set('utopia', fn () => $app);
$app = new Http($swooleAdapter, 'UTC');
$requestContainer->set('utopia', fn () => $app);
$registerRequestResources($requestContainer);
$setRequestContext($app->context());
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
try {
$authorization = $app->getResource('authorization');
$authorization = $app->context()->get('authorization');
$request->setAuthorization($authorization);
$response->setAuthorization($authorization);
@@ -552,18 +546,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$logger = $app->getResource("logger");
$logger = $app->context()->get("logger");
if ($logger) {
try {
/** @var Utopia\Database\Document $user */
$user = $app->getResource('user');
$user = $app->context()->get('user');
} catch (\Throwable $_th) {
// All good, user is optional information for logger
}
$route = $app->getRoute();
$log = $app->getResource("log");
$log = $app->context()->get("log");
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
@@ -642,18 +636,16 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
$http->on(Constant::EVENT_TASK, function () use ($container) {
$lastSyncUpdate = null;
$app = new Http($swooleAdapter, 'UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
$dbForPlatform = $container->get('dbForPlatform');
/** @var \Swoole\Table $riskyDomains */
$riskyDomains = $app->getResource('riskyDomains');
$riskyDomains = $container->get('riskyDomains');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $container) {
try {
$time = DateTime::now();
$limit = 1000;
@@ -670,7 +662,7 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
}
$results = [];
try {
$authorization = $app->getResource('authorization');
$authorization = $container->get('authorization');
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error('rules ' . $th->getMessage());
@@ -720,4 +712,4 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
});
});
$swooleAdapter->start();
$swoole->start();
+10
View File
@@ -5,6 +5,8 @@ use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
@@ -118,6 +120,14 @@ $container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPubl
$publisher,
new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMails', fn (Publisher $publisher) => new MailPublisher(
$publisher,
new Queue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMessaging', fn (Publisher $publisher) => new MessagingPublisher(
$publisher,
new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
), ['publisher']);
/**
* Platform configuration
+74 -118
View File
@@ -9,8 +9,6 @@ use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
@@ -61,26 +59,18 @@ use Utopia\Validator\WhiteList;
* These resources depend (directly or transitively) on request/response
* and must be fresh for each HTTP request.
*/
return function (Container $container): void {
$container->set('utopia:graphql', function ($utopia) {
return $utopia;
}, ['utopia']);
return function (Container $context): void {
$context->set('utopia:graphql', fn ($utopia) => $utopia, ['utopia']);
$container->set('log', fn () => new Log(), []);
$context->set('log', fn () => new Log(), []);
$container->set('logger', function ($register) {
return $register->get('logger');
}, ['register']);
$context->set('logger', fn ($register) => $register->get('logger'), ['register']);
$container->set('authorization', function () {
return new Authorization();
}, []);
$context->set('authorization', fn () => new Authorization(), []);
$container->set('store', function (): Store {
return new Store();
}, []);
$context->set('store', fn (): Store => new Store(), []);
$container->set('proofForPassword', function (): Password {
$context->set('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(7168)
@@ -94,21 +84,21 @@ return function (Container $container): void {
return $password;
});
$container->set('proofForToken', function (): Token {
$context->set('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
$container->set('proofForCode', function (): Code {
$context->set('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
$container->set('locale', function () {
$context->set('locale', function () {
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
@@ -116,38 +106,16 @@ return function (Container $container): void {
});
// Per-request queue resources (stateful, accumulate event data during request)
$container->set('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
$container->set('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
$container->set('usage', function () {
return new UsageContext();
}, []);
$container->set('auditContext', fn () => new AuditContext(), []);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('eventProcessor', function () {
return new EventProcessor();
}, []);
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$context->set('queueForDatabase', fn (Publisher $publisher) => new EventDatabase($publisher), ['publisher']);
$context->set('queueForDeletes', fn (Publisher $publisher) => new Delete($publisher), ['publisher']);
$context->set('queueForEvents', fn (Publisher $publisher) => new Event($publisher), ['publisher']);
$context->set('queueForWebhooks', fn (Publisher $publisher) => new Webhook($publisher), ['publisher']);
$context->set('queueForRealtime', fn () => new Realtime(), []);
$context->set('usage', fn () => new UsageContext(), []);
$context->set('auditContext', fn () => new AuditContext(), []);
$context->set('queueForFunctions', fn (Publisher $publisher) => new Func($publisher), ['publisher']);
$context->set('eventProcessor', fn () => new EventProcessor(), []);
$context->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
@@ -165,7 +133,7 @@ return function (Container $container): void {
return $database;
}, ['pools', 'cache', 'authorization']);
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$context->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$adapters = [];
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$adapters) {
@@ -222,7 +190,7 @@ return function (Container $container): void {
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$context->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) {
@@ -254,7 +222,7 @@ return function (Container $container): void {
/**
* List of allowed request hostnames for the request.
*/
$container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$context->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...($platform['hostnames'] ?? [])];
/* Add platform configured hostnames */
@@ -298,7 +266,7 @@ return function (Container $container): void {
/**
* List of allowed request schemes for the request.
*/
$container->set('allowedSchemes', function (array $platform, Document $project) {
$context->set('allowedSchemes', function (array $platform, Document $project) {
$allowed = [...($platform['schemas'] ?? [])];
if (! $project->isEmpty() && $project->getId() !== 'console') {
@@ -318,7 +286,7 @@ return function (Container $container): void {
/**
* Whether the request origin is verified against the request hostname.
*/
$container->set('domainVerification', function (Request $request) {
$context->set('domainVerification', function (Request $request) {
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string) $origin);
@@ -330,7 +298,7 @@ return function (Container $container): void {
/**
* Cookie domain for the current request.
*/
$container->set('cookieDomain', function (Request $request, Document $project) {
$context->set('cookieDomain', function (Request $request, Document $project) {
$localHosts = ['localhost', 'localhost:' . $request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
@@ -364,7 +332,7 @@ return function (Container $container): void {
/**
* Rule associated with a request origin.
*/
$container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$context->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
@@ -414,7 +382,7 @@ return function (Container $container): void {
/**
* CORS service
*/
$container->set('cors', function (array $allowedHostnames) {
$context->set('cors', function (array $allowedHostnames) {
$corsConfig = Config::getParam('cors');
return new Cors(
@@ -426,23 +394,23 @@ return function (Container $container): void {
);
}, ['allowedHostnames']);
$container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
return new URL();
}
$context->set(
'originValidator',
fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
? new Origin($allowedHostnames, $allowedSchemes)
: new URL(),
['devKey', 'allowedHostnames', 'allowedSchemes']
);
return new Origin($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
$context->set(
'redirectValidator',
fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
? new Redirect($allowedHostnames, $allowedSchemes)
: new URL(),
['devKey', 'allowedHostnames', 'allowedSchemes']
);
$container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
return new URL();
}
return new Redirect($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
$container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
$context->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
/**
* Handles user authentication and session validation.
*
@@ -613,7 +581,7 @@ return function (Container $container): void {
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
$container->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
$context->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
@@ -646,7 +614,7 @@ return function (Container $container): void {
return $project;
}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']);
$container->set('session', function (User $user, Store $store, Token $proofForToken) {
$context->set('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
return;
}
@@ -667,7 +635,7 @@ return function (Container $container): void {
return;
}, ['user', 'store', 'proofForToken']);
$container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
$context->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -943,7 +911,7 @@ return function (Container $container): void {
return $database;
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
$container->set('schema', function ($utopia, $dbForProject, $authorization) {
$context->set('schema', function ($utopia, $dbForProject, $authorization) {
$complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
@@ -1030,13 +998,9 @@ return function (Container $container): void {
);
}, ['utopia', 'dbForProject', 'authorization']);
$container->set('audit', function ($dbForProject) {
$adapter = new AdapterDatabase($dbForProject);
$context->set('audit', fn ($dbForProject) => new Audit(new AdapterDatabase($dbForProject)), ['dbForProject']);
return new Audit($adapter);
}, ['dbForProject']);
$container->set('mode', function ($request, Document $project) {
$context->set('mode', function ($request, Document $project) {
/** @var Appwrite\Utopia\Request $request */
/**
@@ -1054,7 +1018,7 @@ return function (Container $container): void {
return $mode;
}, ['request', 'project']);
$container->set('requestTimestamp', function ($request) {
$context->set('requestTimestamp', function ($request) {
// TODO: Move this to the Request class itself
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
$requestTimestamp = null;
@@ -1069,7 +1033,7 @@ return function (Container $container): void {
return $requestTimestamp;
}, ['request']);
$container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$context->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
// Check if given key match project's development keys
@@ -1118,7 +1082,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
$container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$context->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
@@ -1161,7 +1125,7 @@ return function (Container $container): void {
return $team;
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
$container->set('previewHostname', function (Request $request, ?Key $apiKey) {
$context->set('previewHostname', function (Request $request, ?Key $apiKey) {
$allowed = false;
if (Http::isDevelopment()) {
@@ -1180,7 +1144,7 @@ return function (Container $container): void {
return '';
}, ['request', 'apiKey']);
$container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$context->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
@@ -1214,7 +1178,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'team', 'user']);
$container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$context->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$tokenJWT = $request->getParam('token');
if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication
@@ -1281,7 +1245,7 @@ return function (Container $container): void {
return new Document([]);
}, ['project', 'dbForProject', 'request', 'authorization']);
$container->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
$context->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
@@ -1443,35 +1407,27 @@ return function (Container $container): void {
}, ['pools', 'cache', 'project', 'request', 'usage', 'authorization']);
$container->set('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
}, ['dbForProject', 'authorization', 'getDatabasesDB']);
$context->set(
'transactionState',
fn (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) => new TransactionState($dbForProject, $authorization, $getDatabasesDB),
['dbForProject', 'authorization', 'getDatabasesDB']
);
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
$context->set(
'executionsRetentionCount',
fn (Document $project, array $plan) => ($project->getId() === 'console' || empty($plan))
? 0
: (int) ($plan['executionsRetentionCount'] ?? 100),
['project', 'plan']
);
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$context->set('deviceForFiles', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForSites', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForMigrations', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForFunctions', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForBuilds', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())), ['project', 'telemetry']);
$container->set('deviceForFiles', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForSites', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForMigrations', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFunctions', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForBuilds', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('embeddingAgent', function ($register) {
$context->set('embeddingAgent', function ($register) {
$adapter = new Ollama();
$adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
$adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
-10
View File
@@ -4,8 +4,6 @@ use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
@@ -333,14 +331,6 @@ return function (Container $container): void {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
$container->set('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
+14
View File
@@ -881,6 +881,13 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -909,6 +916,13 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
+5 -5
View File
@@ -51,7 +51,7 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.20.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.2.*",
"utopia-php/abuse": "1.3.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "2.2.*",
@@ -67,15 +67,15 @@
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.6.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/http": "0.34.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/http": "^2.0@RC",
"utopia-php/fetch": "^1.1",
"utopia-php/validators": "0.2.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/logger": "0.8.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.*",
"utopia-php/platform": "0.13.*",
"utopia-php/platform": "^1.0@RC",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
Generated
+102 -115
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ec2ad489c60f0102f0dfab223b6d1fe4",
"content-hash": "9db08148f3a8f53bd972eb7b3a835b3b",
"packages": [
{
"name": "adhocore/jwt",
@@ -69,25 +69,25 @@
},
{
"name": "appwrite/appwrite",
"version": "19.1.0",
"version": "23.1.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "8738e812062f899c85b2598eef43d6a247f08a56"
"reference": "2f275921f10ceb7cff99f2d463f7328b296234fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56",
"reference": "8738e812062f899c85b2598eef43d6a247f08a56",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/2f275921f10ceb7cff99f2d463f7328b296234fa",
"reference": "2f275921f10ceb7cff99f2d463f7328b296234fa",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.1.0"
"php": ">=8.2.0"
},
"require-dev": {
"mockery/mockery": "^1.6.12",
"mockery/mockery": "1.6.12",
"phpunit/phpunit": "^10"
},
"type": "library",
@@ -100,14 +100,14 @@
"license": [
"BSD-3-Clause"
],
"description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API",
"description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API",
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0",
"source": "https://github.com/appwrite/sdk-for-php/tree/23.1.0",
"url": "https://appwrite.io/support"
},
"time": "2025-12-18T08:07:43+00:00"
"time": "2026-05-08T13:44:58+00:00"
},
{
"name": "appwrite/php-clamav",
@@ -3359,24 +3359,24 @@
},
{
"name": "utopia-php/abuse",
"version": "1.2.3",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c"
"reference": "5d7efbe5c6b0cf7d06003114fd86e24ba785582f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/53f4274939353522ba331f55bcff6e6011ffc56c",
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/5d7efbe5c6b0cf7d06003114fd86e24ba785582f",
"reference": "5d7efbe5c6b0cf7d06003114fd86e24ba785582f",
"shasum": ""
},
"require": {
"appwrite/appwrite": "19.*",
"appwrite/appwrite": "23.*",
"ext-curl": "*",
"ext-pdo": "*",
"ext-redis": "*",
"php": ">=8.0",
"php": ">=8.2",
"utopia-php/database": "5.*"
},
"require-dev": {
@@ -3405,27 +3405,27 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/1.2.3"
"source": "https://github.com/utopia-php/abuse/tree/1.3.0"
},
"time": "2026-04-29T11:19:08+00:00"
"time": "2026-05-11T08:07:02+00:00"
},
{
"name": "utopia-php/agents",
"version": "1.2.1",
"version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/agents.git",
"reference": "052227953678a30ecc4b5467401fcb0b2386471e"
"reference": "0703f4cae02261e09a1bf0d39a4b1ce649cae634"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e",
"reference": "052227953678a30ecc4b5467401fcb0b2386471e",
"url": "https://api.github.com/repos/utopia-php/agents/zipball/0703f4cae02261e09a1bf0d39a4b1ce649cae634",
"reference": "0703f4cae02261e09a1bf0d39a4b1ce649cae634",
"shasum": ""
},
"require": {
"php": ">=8.3",
"utopia-php/fetch": "0.5.*"
"utopia-php/fetch": "^1.1.0"
},
"require-dev": {
"laravel/pint": "^1.18",
@@ -3458,9 +3458,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/agents/issues",
"source": "https://github.com/utopia-php/agents/tree/1.2.1"
"source": "https://github.com/utopia-php/agents/tree/1.2.2"
},
"time": "2026-02-24T06:03:55+00:00"
"time": "2026-05-08T10:38:23+00:00"
},
{
"name": "utopia-php/analytics",
@@ -3510,22 +3510,22 @@
},
{
"name": "utopia-php/audit",
"version": "2.2.2",
"version": "2.2.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "90886c202e7983999e6b6a8201004d5ab61d4b57"
"reference": "95e9961fa286d2fdb6bf3eaa198f21d51bf58d9c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/90886c202e7983999e6b6a8201004d5ab61d4b57",
"reference": "90886c202e7983999e6b6a8201004d5ab61d4b57",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/95e9961fa286d2fdb6bf3eaa198f21d51bf58d9c",
"reference": "95e9961fa286d2fdb6bf3eaa198f21d51bf58d9c",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/database": "5.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/fetch": "^1.1",
"utopia-php/validators": "0.2.*"
},
"require-dev": {
@@ -3553,9 +3553,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/2.2.2"
"source": "https://github.com/utopia-php/audit/tree/2.2.3"
},
"time": "2026-05-04T06:48:58+00:00"
"time": "2026-05-08T10:38:23+00:00"
},
{
"name": "utopia-php/auth",
@@ -3614,16 +3614,16 @@
},
{
"name": "utopia-php/cache",
"version": "1.0.1",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cache.git",
"reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c"
"reference": "ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/05ceba981436a4022553f7aaa2a05fa049d0f71c",
"reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa",
"reference": "ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa",
"shasum": ""
},
"require": {
@@ -3660,9 +3660,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cache/issues",
"source": "https://github.com/utopia-php/cache/tree/1.0.1"
"source": "https://github.com/utopia-php/cache/tree/1.0.3"
},
"time": "2026-03-12T03:39:09+00:00"
"time": "2026-05-11T11:02:13+00:00"
},
{
"name": "utopia-php/cli",
@@ -4180,22 +4180,21 @@
},
{
"name": "utopia-php/emails",
"version": "0.6.9",
"version": "0.6.10",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/emails.git",
"reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf"
"reference": "2e397754ce68c2ba918564b9f31d9923c0a90429"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/emails/zipball/3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf",
"reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf",
"url": "https://api.github.com/repos/utopia-php/emails/zipball/2e397754ce68c2ba918564b9f31d9923c0a90429",
"reference": "2e397754ce68c2ba918564b9f31d9923c0a90429",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/domains": "^1.0",
"utopia-php/fetch": "^0.5",
"utopia-php/validators": "0.*"
},
"require-dev": {
@@ -4203,7 +4202,8 @@
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.3",
"utopia-php/cli": "^0.22",
"utopia-php/console": "0.*"
"utopia-php/console": "0.*",
"utopia-php/fetch": "^1.1"
},
"type": "library",
"autoload": {
@@ -4235,22 +4235,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/emails/issues",
"source": "https://github.com/utopia-php/emails/tree/0.6.9"
"source": "https://github.com/utopia-php/emails/tree/0.6.10"
},
"time": "2026-03-14T13:52:56+00:00"
"time": "2026-05-08T10:16:22+00:00"
},
{
"name": "utopia-php/fetch",
"version": "0.5.1",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/fetch.git",
"reference": "a96a010e1c273f3888765449687baf58cbc61fcd"
"reference": "64f2b3a789480f1deb102ce684dac4217d8e98d5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/a96a010e1c273f3888765449687baf58cbc61fcd",
"reference": "a96a010e1c273f3888765449687baf58cbc61fcd",
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/64f2b3a789480f1deb102ce684dac4217d8e98d5",
"reference": "64f2b3a789480f1deb102ce684dac4217d8e98d5",
"shasum": ""
},
"require": {
@@ -4259,7 +4259,8 @@
"require-dev": {
"laravel/pint": "^1.5.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.5"
"phpunit/phpunit": "^9.5",
"swoole/ide-helper": "^6.0"
},
"type": "library",
"autoload": {
@@ -4274,22 +4275,22 @@
"description": "A simple library that provides an interface for making HTTP Requests.",
"support": {
"issues": "https://github.com/utopia-php/fetch/issues",
"source": "https://github.com/utopia-php/fetch/tree/0.5.1"
"source": "https://github.com/utopia-php/fetch/tree/1.1.2"
},
"time": "2025-12-18T16:25:10+00:00"
"time": "2026-04-29T11:19:19+00:00"
},
{
"name": "utopia-php/http",
"version": "0.34.25",
"version": "2.0.0-rc1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "76be330d4197bae680eb4ccc29c573456fe91904"
"reference": "3e3b431d443844c6bf810120dee735f45880856f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/76be330d4197bae680eb4ccc29c573456fe91904",
"reference": "76be330d4197bae680eb4ccc29c573456fe91904",
"url": "https://api.github.com/repos/utopia-php/http/zipball/3e3b431d443844c6bf810120dee735f45880856f",
"reference": "3e3b431d443844c6bf810120dee735f45880856f",
"shasum": ""
},
"require": {
@@ -4330,9 +4331,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.34.25"
"source": "https://github.com/utopia-php/http/tree/2.0.0-rc1"
},
"time": "2026-05-05T04:39:15+00:00"
"time": "2026-05-05T15:00:03+00:00"
},
{
"name": "utopia-php/image",
@@ -4434,20 +4435,21 @@
},
{
"name": "utopia-php/logger",
"version": "0.6.2",
"version": "0.8.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/logger.git",
"reference": "25b5bd2ad8bb51292f76332faa7034644fd0941d"
"reference": "132236c42222cd614cb882938a48f8729ef3118b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/logger/zipball/25b5bd2ad8bb51292f76332faa7034644fd0941d",
"reference": "25b5bd2ad8bb51292f76332faa7034644fd0941d",
"url": "https://api.github.com/repos/utopia-php/logger/zipball/132236c42222cd614cb882938a48f8729ef3118b",
"reference": "132236c42222cd614cb882938a48f8729ef3118b",
"shasum": ""
},
"require": {
"php": ">=8.0"
"php": ">=8.1",
"utopia-php/fetch": "^1.1"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -4482,9 +4484,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/logger/issues",
"source": "https://github.com/utopia-php/logger/tree/0.6.2"
"source": "https://github.com/utopia-php/logger/tree/0.8.0"
},
"time": "2024-10-14T16:02:49+00:00"
"time": "2026-05-05T06:04:27+00:00"
},
{
"name": "utopia-php/messaging",
@@ -4539,24 +4541,24 @@
},
{
"name": "utopia-php/migration",
"version": "1.10.1",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "759d6d61b327313cbeeeb4ea0c3e2459164b4827"
"reference": "0fca44f40ad07bf2d56e9396afa6fa6d9b098ef1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/759d6d61b327313cbeeeb4ea0c3e2459164b4827",
"reference": "759d6d61b327313cbeeeb4ea0c3e2459164b4827",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/0fca44f40ad07bf2d56e9396afa6fa6d9b098ef1",
"reference": "0fca44f40ad07bf2d56e9396afa6fa6d9b098ef1",
"shasum": ""
},
"require": {
"appwrite/appwrite": "19.*",
"appwrite/appwrite": "23.*",
"ext-curl": "*",
"ext-openssl": "*",
"halaxa/json-machine": "^1.2",
"php": ">=8.1",
"php": ">=8.2",
"utopia-php/database": "5.*",
"utopia-php/dsn": "0.2.*",
"utopia-php/storage": "2.*"
@@ -4574,25 +4576,7 @@
"Utopia\\Migration\\": "src/Migration"
}
},
"autoload-dev": {
"psr-4": {
"Utopia\\Tests\\": "tests/Migration"
}
},
"scripts": {
"test": [
"./vendor/bin/phpunit"
],
"lint": [
"./vendor/bin/pint --test"
],
"format": [
"./vendor/bin/pint"
],
"check": [
"./vendor/bin/phpstan analyse --level 3 src tests --memory-limit 2G"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@@ -4605,10 +4589,10 @@
"utopia"
],
"support": {
"source": "https://github.com/utopia-php/migration/tree/1.10.1",
"issues": "https://github.com/utopia-php/migration/issues"
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.11.0"
},
"time": "2026-05-07T07:23:57+00:00"
"time": "2026-05-11T08:13:06+00:00"
},
{
"name": "utopia-php/mongo",
@@ -4673,16 +4657,16 @@
},
{
"name": "utopia-php/platform",
"version": "0.13.2",
"version": "1.0.0-rc1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9"
"reference": "36c0a8b2f3d96ca056d724701a302a127111e933"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/a20cb8b20a1e4c9886309c2d033a0292ba0937b9",
"reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/36c0a8b2f3d96ca056d724701a302a127111e933",
"reference": "36c0a8b2f3d96ca056d724701a302a127111e933",
"shasum": ""
},
"require": {
@@ -4690,7 +4674,7 @@
"ext-redis": "*",
"php": ">=8.3",
"utopia-php/cli": "0.23.3",
"utopia-php/http": "0.34.25",
"utopia-php/http": "^2.0@RC",
"utopia-php/queue": "0.18.2",
"utopia-php/servers": "0.4.0"
},
@@ -4718,9 +4702,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
"source": "https://github.com/utopia-php/platform/tree/0.13.2"
"source": "https://github.com/utopia-php/platform/tree/1.0.0-rc1"
},
"time": "2026-05-05T06:00:26+00:00"
"time": "2026-05-05T15:09:27+00:00"
},
{
"name": "utopia-php/pools",
@@ -5254,23 +5238,23 @@
},
{
"name": "utopia-php/vcs",
"version": "3.2.0",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "44a84ab52b42fc12f812b4d7331286b519d39db3"
"reference": "03ccd12b75d67d29094eb760b468fddde4b6b5e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/44a84ab52b42fc12f812b4d7331286b519d39db3",
"reference": "44a84ab52b42fc12f812b4d7331286b519d39db3",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/03ccd12b75d67d29094eb760b468fddde4b6b5e5",
"reference": "03ccd12b75d67d29094eb760b468fddde4b6b5e5",
"shasum": ""
},
"require": {
"adhocore/jwt": "^1.1",
"php": ">=8.0",
"utopia-php/cache": "1.0.*",
"utopia-php/fetch": "0.5.*"
"utopia-php/fetch": "^1.1"
},
"require-dev": {
"laravel/pint": "1.*.*",
@@ -5297,9 +5281,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/3.2.0"
"source": "https://github.com/utopia-php/vcs/tree/3.2.1"
},
"time": "2026-04-08T16:00:31+00:00"
"time": "2026-05-08T10:13:53+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5492,16 +5476,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.27.5",
"version": "1.28.4",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "9faa38b48d422f3da764a719712905c83b3922cb"
"reference": "38de925e8c9e7f0f720d45187be54a291aaf696b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9faa38b48d422f3da764a719712905c83b3922cb",
"reference": "9faa38b48d422f3da764a719712905c83b3922cb",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/38de925e8c9e7f0f720d45187be54a291aaf696b",
"reference": "38de925e8c9e7f0f720d45187be54a291aaf696b",
"shasum": ""
},
"require": {
@@ -5537,9 +5521,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.27.5"
"source": "https://github.com/appwrite/sdk-generator/tree/1.28.4"
},
"time": "2026-05-05T12:09:40+00:00"
"time": "2026-05-11T13:55:49+00:00"
},
{
"name": "brianium/paratest",
@@ -8471,7 +8455,10 @@
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {},
"stability-flags": {
"utopia-php/http": 5,
"utopia-php/platform": 5
},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
+15 -1
View File
@@ -1114,6 +1114,13 @@ services:
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1145,6 +1152,13 @@ services:
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1478,4 +1492,4 @@ volumes:
appwrite-sites:
appwrite-builds:
appwrite-config:
appwrite-models:
appwrite-models:
+53 -3
View File
@@ -55,7 +55,7 @@ class Google extends OAuth2
'state' => \json_encode($this->state),
'response_type' => 'code',
'access_type' => 'offline',
'prompt' => 'consent'
'prompt' => $this->getPrompt()
]);
}
@@ -72,7 +72,7 @@ class Google extends OAuth2
'https://oauth2.googleapis.com/token?' . \http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'client_secret' => $this->getClientSecret(),
'redirect_uri' => $this->callback,
'scope' => null,
'grant_type' => 'authorization_code'
@@ -95,7 +95,7 @@ class Google extends OAuth2
'https://oauth2.googleapis.com/token?' . \http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'client_secret' => $this->getClientSecret(),
'grant_type' => 'refresh_token'
])
), true);
@@ -177,4 +177,54 @@ class Google extends OAuth2
return $this->user;
}
/**
* Extracts the Client Secret from the JSON stored in appSecret
*
* @return string
*/
protected function getClientSecret(): string
{
$secret = $this->getAppSecret();
return $secret['clientSecret'] ?? $this->appSecret;
}
/**
* Extracts the prompt values from the JSON stored in appSecret
*
* @return string
*/
protected function getPrompt(): string
{
$secret = $this->getAppSecret();
$prompt = $secret['prompt'] ?? [];
if (empty($prompt)) {
$prompt = ['consent'];
}
return \implode(' ', $prompt);
}
/**
* Decode the JSON stored in appSecret.
* Falls back to treating the raw string as the client secret for backwards compatibility.
*
* @return array
*/
protected function getAppSecret(): array
{
try {
$secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
} catch (\Throwable $th) {
return ['clientSecret' => $this->appSecret];
}
if (!\is_array($secret)) {
return ['clientSecret' => $this->appSecret];
}
return $secret;
}
}
+28 -30
View File
@@ -4,14 +4,14 @@ namespace Appwrite\Bus\Listeners;
use Appwrite\Auth\MFA\Type;
use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Template\Template;
use Utopia\Bus\Listener;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Locale\Locale;
use Utopia\Queue\Publisher;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
@@ -31,14 +31,14 @@ class Mails extends Listener
{
$this
->desc('Sends session alert emails')
->inject('publisher')
->inject('publisherForMails')
->inject('locale')
->inject('platform')
->inject('dbForProject')
->callback($this->handle(...));
}
public function handle(SessionCreated $event, Publisher $publisher, Locale $locale, array $platform, Database $dbForProject): void
public function handle(SessionCreated $event, MailPublisher $publisherForMails, Locale $locale, array $platform, Database $dbForProject): void
{
$project = new Document($event->project);
@@ -124,34 +124,32 @@ class Mails extends Listener
];
}
$queueForMails = new Mail($publisher);
$smtpConfig = [];
if ($smtp['enabled'] ?? false) {
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '')
->setSmtpReplyToEmail($customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '') // Includes backwards compatibility
->setSmtpReplyToName($customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '')
->setSmtpSenderEmail($customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM))
->setSmtpSenderName($customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '', // Includes backwards compatibility
'replyToName' => $customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '',
'senderEmail' => $customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
'senderName' => $customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'),
];
}
$queueForMails
->setProject($project)
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl')
->appendVariables($emailVariables)
->setRecipient($event->user['email']);
if ($isBranded) {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $event->user['email'],
subject: $subject,
bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl',
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $isBranded ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
}
}
-576
View File
@@ -1,576 +0,0 @@
<?php
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Mail extends Event
{
protected string $recipient = '';
protected string $name = '';
protected string $subject = '';
protected string $body = '';
protected string $preview = '';
protected array $smtp = [];
protected array $variables = [];
protected string $bodyTemplate = '';
protected array $attachment = [];
protected array $customMailOptions = [];
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
->setClass(System::getEnv('_APP_MAILS_CLASS_NAME', Event::MAILS_CLASS_NAME));
}
/**
* Sets subject for the mail event.
*
* @param string $subject
* @return self
*/
public function setSubject(string $subject): self
{
$this->subject = $subject;
return $this;
}
/**
* Returns subject for the mail event.
*
* @return string
*/
public function getSubject(): string
{
return $this->subject;
}
/**
* Sets recipient for the mail event.
*
* @param string $recipient
* @return self
*/
public function setRecipient(string $recipient): self
{
$this->recipient = $recipient;
return $this;
}
/**
* Returns set recipient for mail event.
*
* @return string
*/
public function getRecipient(): string
{
return $this->recipient;
}
/**
* Sets body for the mail event.
*
* @param string $body
* @return self
*/
public function setBody(string $body): self
{
$this->body = $body;
return $this;
}
/**
* Returns body for the mail event.
*
* @return string
*/
public function getBody(): string
{
return $this->body;
}
/**
* Sets preview for the mail event.
*
* @return self
*/
public function setPreview(string $preview): self
{
$this->preview = $preview;
return $this;
}
/**
* Returns preview for the mail event.
*
* @return string
*/
public function getPreview(): string
{
return $this->preview;
}
/**
* Sets name for the mail event.
*
* @param string $name
* @return self
*/
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* Returns set name for the mail event.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Sets bodyTemplate for the mail event.
*
* @param string $bodyTemplate
* @return self
*/
public function setBodyTemplate(string $bodyTemplate): self
{
$this->bodyTemplate = $bodyTemplate;
return $this;
}
/**
* Returns subject for the mail event.
*
* @return string
*/
public function getBodyTemplate(): string
{
return $this->bodyTemplate;
}
/**
* Set SMTP Host
*
* @param string $host
* @return self
*/
public function setSmtpHost(string $host): self
{
$this->smtp['host'] = $host;
return $this;
}
/**
* Set SMTP port
*
* @param int $port
* @return self
*/
public function setSmtpPort(int $port): self
{
$this->smtp['port'] = $port;
return $this;
}
/**
* Set SMTP username
*
* @param string $username
* @return self
*/
public function setSmtpUsername(string $username): self
{
$this->smtp['username'] = $username;
return $this;
}
/**
* Set SMTP password
*
* @param string $password
* @return self
*/
public function setSmtpPassword(string $password): self
{
$this->smtp['password'] = $password;
return $this;
}
/**
* Set SMTP secure
*
* @param string $secure
* @return self
*/
public function setSmtpSecure(string $secure): self
{
$this->smtp['secure'] = $secure;
return $this;
}
/**
* Set SMTP sender email
*
* @param string $senderEmail
* @return self
*/
public function setSmtpSenderEmail(string $senderEmail): self
{
$this->smtp['senderEmail'] = $senderEmail;
return $this;
}
/**
* Set SMTP sender name
*
* @param string $senderName
* @return self
*/
public function setSmtpSenderName(string $senderName): self
{
$this->smtp['senderName'] = $senderName;
return $this;
}
/**
* Set SMTP reply-to email
*
* @param string $email
* @return self
*/
public function setSmtpReplyToEmail(string $email): self
{
$this->smtp['replyToEmail'] = $email;
return $this;
}
/**
* Set SMTP reply-to name
*
* @param string $name
* @return self
*/
public function setSmtpReplyToName(string $name): self
{
$this->smtp['replyToName'] = $name;
return $this;
}
/**
* Get SMTP
*
* @return string
*/
public function getSmtpHost(): string
{
return $this->smtp['host'] ?? '';
}
/**
* Get SMTP port
*
* @return integer
*/
public function getSmtpPort(): int
{
return $this->smtp['port'] ?? 0;
}
/**
* Get SMTP username
*
* @return string
*/
public function getSmtpUsername(): string
{
return $this->smtp['username'] ?? '';
}
/**
* Get SMTP password
*
* @return string
*/
public function getSmtpPassword(): string
{
return $this->smtp['password'] ?? '';
}
/**
* Get SMTP secure
*
* @return string
*/
public function getSmtpSecure(): string
{
return $this->smtp['secure'] ?? '';
}
/**
* Get SMTP sender email
*
* @return string
*/
public function getSmtpSenderEmail(): string
{
return $this->smtp['senderEmail'] ?? '';
}
/**
* Get SMTP sender name
*
* @return string
*/
public function getSmtpSenderName(): string
{
return $this->smtp['senderName'] ?? '';
}
/**
* Get SMTP reply-to email
*
* @return string
*/
public function getSmtpReplyToEmail(): string
{
return $this->smtp['replyToEmail'] ?? '';
}
/**
* Get SMTP reply-to name
*
* @return string
*/
public function getSmtpReplyToName(): string
{
return $this->smtp['replyToName'] ?? '';
}
/**
* Get Email Variables
*
* @return array
*/
public function getVariables(): array
{
return $this->variables;
}
/**
* Set Email Variables
*
* @param array $variables
* @return self
*/
public function setVariables(array $variables): self
{
$this->variables = $variables;
return $this;
}
/**
* Append variables to the email event.
*
* @param array $variables
* @return self
*/
public function appendVariables(array $variables): self
{
$this->variables = \array_merge($this->variables, $variables);
return $this;
}
/**
* Set attachment
* @param string $content
* @param string $filename
* @param string $encoding
* @param string $type
* @return self
*/
public function setAttachment(string $content, string $filename, string $encoding = 'base64', string $type = 'plain/text')
{
$this->attachment = [
'content' => base64_encode($content),
'filename' => $filename,
'encoding' => $encoding,
'type' => $type,
];
return $this;
}
/**
* Get attachment
*
* @return array
*/
public function getAttachment(): array
{
return $this->attachment;
}
/**
* Reset attachment
*
* @return self
*/
public function resetAttachment(): self
{
$this->attachment = [];
return $this;
}
/**
* Set sender email
*
* @param string $email
* @return self
*/
public function setSenderEmail(string $email): self
{
$this->customMailOptions['senderEmail'] = $email;
return $this;
}
/**
* Get sender email
*
* @return string
*/
public function getSenderEmail(): string
{
return $this->customMailOptions['senderEmail'] ?? '';
}
/**
* Set sender name
*
* @param string $name
* @return self
*/
public function setSenderName(string $name): self
{
$this->customMailOptions['senderName'] = $name;
return $this;
}
/**
* Get sender name
*
* @return string
*/
public function getSenderName(): string
{
return $this->customMailOptions['senderName'] ?? '';
}
/**
* Set reply-to email
*
* @param string $email
* @return self
*/
public function setReplyToEmail(string $email): self
{
$this->customMailOptions['replyToEmail'] = $email;
return $this;
}
/**
* Get reply-to email
*
* @return string
*/
public function getReplyToEmail(): string
{
return $this->customMailOptions['replyToEmail'] ?? '';
}
/**
* Set reply-to name
*
* @param string $name
* @return self
*/
public function setReplyToName(string $name): self
{
$this->customMailOptions['replyToName'] = $name;
return $this;
}
/**
* Get reply-to name
*
* @return string
*/
public function getReplyToName(): string
{
return $this->customMailOptions['replyToName'] ?? '';
}
/**
* Reset
*
* @return self
*/
public function reset(): self
{
$this->project = null;
$this->recipient = '';
$this->name = '';
$this->subject = '';
$this->body = '';
$this->variables = [];
$this->bodyTemplate = '';
$this->attachment = [];
$this->customMailOptions = [];
return $this;
}
/**
* Prepare the payload for the event
*
* @return array
*/
protected function preparePayload(): array
{
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'recipient' => $this->recipient,
'name' => $this->name,
'subject' => $this->subject,
'bodyTemplate' => $this->bodyTemplate,
'body' => $this->body,
'preview' => $this->preview,
'smtp' => $this->smtp,
'variables' => $this->variables,
'attachment' => $this->attachment,
'customMailOptions' => $this->customMailOptions,
'events' => Event::generateEvents($this->getEvent(), $this->getParams()),
'platform' => $platform,
];
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Config\Config;
use Utopia\Database\Document;
final class Mail extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly string $recipient = '',
public readonly string $name = '',
public readonly string $subject = '',
public readonly string $bodyTemplate = '',
public readonly string $body = '',
public readonly string $preview = '',
public readonly array $smtp = [],
public readonly array $variables = [],
public readonly array $attachment = [],
public readonly array $customMailOptions = [],
public readonly array $events = [],
public readonly array $platform = [],
) {
}
public function toArray(): array
{
$platform = !empty($this->platform) ? $this->platform : Config::getParam('platform', []);
return [
'project' => $this->project?->getArrayCopy(),
'recipient' => $this->recipient,
'name' => $this->name,
'subject' => $this->subject,
'bodyTemplate' => $this->bodyTemplate,
'body' => $this->body,
'preview' => $this->preview,
'smtp' => $this->smtp,
'variables' => $this->variables,
'attachment' => $this->attachment,
'customMailOptions' => $this->customMailOptions,
'events' => $this->events,
'platform' => $platform,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
recipient: $data['recipient'] ?? '',
name: $data['name'] ?? '',
subject: $data['subject'] ?? '',
bodyTemplate: $data['bodyTemplate'] ?? '',
body: $data['body'] ?? '',
preview: $data['preview'] ?? '',
smtp: $data['smtp'] ?? [],
variables: $data['variables'] ?? [],
attachment: $data['attachment'] ?? [],
customMailOptions: $data['customMailOptions'] ?? [],
events: $data['events'] ?? [],
platform: $data['platform'] ?? [],
);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Messaging extends Base
{
public function __construct(
public readonly string $type,
public readonly Document $project,
public readonly ?Document $user = null,
public readonly ?string $messageId = null,
public readonly ?Document $message = null,
public readonly ?array $recipients = null,
public readonly ?string $providerType = null,
) {
}
public function toArray(): array
{
return [
'type' => $this->type,
'project' => $this->project->getArrayCopy(),
'user' => $this->user?->getArrayCopy(),
'messageId' => $this->messageId,
'message' => $this->message?->getArrayCopy(),
'recipients' => $this->recipients,
'providerType' => $this->providerType,
];
}
public static function fromArray(array $data): static
{
return new self(
type: $data['type'] ?? '',
project: new Document($data['project'] ?? []),
user: !empty($data['user']) ? new Document($data['user']) : null,
messageId: $data['messageId'] ?? null,
message: !empty($data['message']) ? new Document($data['message']) : null,
recipients: $data['recipients'] ?? null,
providerType: $data['providerType'] ?? null,
);
}
}
-182
View File
@@ -1,182 +0,0 @@
<?php
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Messaging extends Event
{
protected string $type = '';
protected ?string $messageId = null;
protected ?Document $message = null;
protected ?array $recipients = null;
protected ?string $scheduledAt = null;
protected ?string $providerType = null;
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
->setClass(System::getEnv('_APP_MESSAGING_CLASS_NAME', Event::MESSAGING_CLASS_NAME));
}
/**
* Sets type for the build event.
*
* @param string $type Can be `MESSAGE_SEND_TYPE_INTERNAL` or `MESSAGE_SEND_TYPE_EXTERNAL`.
* @return self
*/
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
/**
* Returns set type for the function event.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets recipient for the messaging event.
*
* @param string[] $recipients
* @return self
*/
public function setRecipients(array $recipients): self
{
$this->recipients = $recipients;
return $this;
}
/**
* Returns set recipient for messaging event.
*
* @return string[]
*/
public function getRecipient(): array
{
return $this->recipients;
}
/**
* Sets message document for the messaging event.
*
* @param Document $message
* @return self
*/
public function setMessage(Document $message): self
{
$this->message = $message;
return $this;
}
/**
* Returns message document for the messaging event.
*
* @return Document
*/
public function getMessage(): Document
{
return $this->message;
}
/**
* Sets message ID for the messaging event.
*
* @param string $messageId
* @return self
*/
public function setMessageId(string $messageId): self
{
$this->messageId = $messageId;
return $this;
}
/**
* Returns set message ID for the messaging event.
*
* @return string
*/
public function getMessageId(): string
{
return $this->messageId;
}
/**
* Sets provider type for the messaging event.
*
* @param string $providerType
* @return self
*/
public function setProviderType(string $providerType): self
{
$this->providerType = $providerType;
return $this;
}
/**
* Returns set provider type for the messaging event.
*
* @return string
*/
public function getProviderType(): string
{
return $this->providerType;
}
/**
* Sets Scheduled delivery time for the messaging event.
*
* @param string $scheduledAt
* @return self
*/
public function setScheduledAt(string $scheduledAt): self
{
$this->scheduledAt = $scheduledAt;
return $this;
}
/**
* Returns set Delivery Time for the messaging event.
*
* @return string
*/
public function getScheduledAt(): string
{
return $this->scheduledAt;
}
/**
* Prepare the payload for the event
*
* @return array
*/
protected function preparePayload(): array
{
return [
'type' => $this->type,
'project' => $this->project,
'user' => $this->user,
'messageId' => $this->messageId,
'message' => $this->message,
'recipients' => $this->recipients,
'providerType' => $this->providerType,
];
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Mail as MailMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Mail extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(MailMessage $message, ?Queue $queue = null): string|bool
{
return $this->publish($queue ?? $this->queue, $message);
}
public function getSize(bool $failed = false, ?Queue $queue = null): int
{
return $this->getQueueSize($queue ?? $this->queue, $failed);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Messaging extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(MessagingMessage $message, ?Queue $queue = null): string|bool
{
return $this->publish($queue ?? $this->queue, $message);
}
public function getSize(bool $failed = false, ?Queue $queue = null): int
{
return $this->getQueueSize($queue ?? $this->queue, $failed);
}
}
+21 -39
View File
@@ -6,7 +6,6 @@ use Appwrite\GraphQL\Exception as GQLException;
use Appwrite\Promises\Swoole;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\DI\Container;
use Utopia\Http\Exception;
use Utopia\Http\Http;
use Utopia\Http\Route;
@@ -53,22 +52,6 @@ class Resolvers
}
}
/**
* Get the current request container.
*/
private static function getResolverContainer(Http $utopia): Container
{
$container = $utopia->getResource('container');
if ($container instanceof Container || (\is_object($container) && \method_exists($container, 'get') && \method_exists($container, 'set'))) {
/** @var Container $container */
return $container;
}
/** @var callable(): Container $container */
return $container();
}
/**
* Get the request-scoped lock shared by GraphQL resolver coroutines
* for the current HTTP request.
@@ -95,9 +78,9 @@ class Resolvers
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -167,9 +150,9 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -203,9 +186,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
$beforeResolve = function ($payload) {
return $payload['documents'];
@@ -245,9 +228,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -282,9 +265,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -317,9 +300,9 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -373,10 +356,9 @@ class Resolvers
}
/** @var Response $resolverResponse */
$resolverResponse = clone $utopia->getResource('response');
$container = self::getResolverContainer($utopia);
$container->set('request', static fn () => $request);
$container->set('response', static fn () => $resolverResponse);
$resolverResponse = clone $utopia->context()->get('response');
$utopia->context()->set('request', static fn () => $request);
$utopia->context()->set('response', static fn () => $resolverResponse);
$resolverResponse->setContentType(Response::CONTENT_TYPE_NULL);
$resolverResponse->setSent(false);
+1 -1
View File
@@ -84,7 +84,7 @@ class Schema
protected static function api(Http $utopia, callable $complexity): array
{
Mapper::init($utopia
->getResource('response')
->context()->get('response')
->getModels());
$queries = [];
+1 -1
View File
@@ -254,7 +254,7 @@ class Mapper
array $injections
): Type {
$validator = \is_callable($validator)
? \call_user_func_array($validator, $utopia->getResources($injections))
? \call_user_func_array($validator, \array_map($utopia->context()->get(...), $injections))
: $validator;
$isNullable = $validator instanceof Nullable;
+1 -1
View File
@@ -154,7 +154,7 @@ class Server
$nativeServer = $adapter->getNativeServer();
$container = $adapter->getContainer();
$container = $adapter->resources();
$container->set('installerState', fn () => $state);
$container->set('installerConfig', fn () => $config);
$container->set('installerPaths', fn () => $paths);
@@ -5,8 +5,10 @@ namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Challenges;
use Appwrite\Auth\MFA\Type;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -101,8 +103,8 @@ class Create extends Action
->inject('platform')
->inject('request')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForMails')
->inject('publisherForMessaging')
->inject('publisherForMails')
->inject('timelimit')
->inject('usage')
->inject('plan')
@@ -121,8 +123,8 @@ class Create extends Action
array $platform,
Request $request,
Event $queueForEvents,
Messaging $queueForMessaging,
Mail $queueForMails,
MessagingPublisher $publisherForMessaging,
MailPublisher $publisherForMails,
callable $timelimit,
Context $usage,
array $plan,
@@ -180,16 +182,18 @@ class Create extends Action
$message = $message->render();
$phone = $user->getAttribute('phone');
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage(new Document([
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_INTERNAL,
project: $project,
message: new Document([
'$id' => $challenge->getId(),
'data' => [
'content' => $code,
],
]))
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS);
]),
recipients: [$phone],
providerType: MESSAGE_TYPE_SMS,
));
$helper = PhoneNumberUtil::getInstance();
try {
@@ -252,6 +256,7 @@ class Create extends Action
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -269,13 +274,6 @@ class Create extends Action
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -296,11 +294,17 @@ class Create extends Action
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$emailVariables = [
@@ -327,20 +331,18 @@ class Create extends Action
]);
}
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate($bodyTemplate)
->appendVariables($emailVariables)
->setRecipient($user->getAttribute('email'));
// since this is console project, set email sender name!
if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $user->getAttribute('email'),
subject: $subject,
bodyTemplate: $bodyTemplate,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
break;
}
@@ -287,7 +287,29 @@ class Update extends Base
}
// Inform scheduler if function is still active
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $authorization->skip(fn () => $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')));
// Re-create schedule if missing
if ($schedule->isEmpty()) {
$schedule = $authorization->skip(
fn () => $dbForPlatform->createDocument('schedules', new Document([
'region' => $project->getAttribute('region'),
'resourceType' => SCHEDULE_RESOURCE_TYPE_FUNCTION,
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getSequence(),
'resourceUpdatedAt' => DateTime::now(),
'projectId' => $project->getId(),
'schedule' => $function->getAttribute('schedule'),
'active' => false,
]))
);
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'scheduleId' => $schedule->getId(),
'scheduleInternalId' => $schedule->getSequence(),
]));
}
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
@@ -109,9 +109,7 @@ class Screenshots extends Action
throw new \Exception("Rule for deployment not found");
}
$client = new FetchClient();
$client->setTimeout(\intval($site->getAttribute('timeout', '15')) * 1000);
$client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON);
$timeout = \intval($site->getAttribute('timeout', '15')) * 1000;
$bucket = $dbForPlatform->getDocument('buckets', 'screenshots');
@@ -162,8 +160,8 @@ class Screenshots extends Action
]);
$screenshotError = null;
$screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $site, $client, &$screenshotError) {
return function () use ($key, $configs, $apiKey, $site, $client, &$screenshotError) {
$screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $site, $timeout, &$screenshotError) {
return function () use ($key, $configs, $apiKey, $site, $timeout, &$screenshotError) {
try {
$config = $configs[$key];
@@ -179,6 +177,10 @@ class Screenshots extends Action
}
$browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1');
$client = new FetchClient();
$client->setTimeout($timeout);
$client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON);
$fetchResponse = $client->fetch(
url: $browserEndpoint . '/screenshots',
method: 'POST',
@@ -6,11 +6,11 @@ use Appwrite\Event\Database;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
@@ -77,14 +77,14 @@ class Get extends Base
->inject('queueForDatabase')
->inject('queueForDeletes')
->inject('publisherForAudits')
->inject('queueForMails')
->inject('publisherForMails')
->inject('queueForFunctions')
->inject('publisherForStatsResources')
->inject('publisherForUsage')
->inject('queueForWebhooks')
->inject('publisherForCertificates')
->inject('publisherForBuilds')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('publisherForMigrations')
->inject('publisherForScreenshots')
->callback($this->action(...));
@@ -97,14 +97,14 @@ class Get extends Base
Database $queueForDatabase,
Delete $queueForDeletes,
Audit $publisherForAudits,
Mail $queueForMails,
MailPublisher $publisherForMails,
Func $queueForFunctions,
StatsResourcesPublisher $publisherForStatsResources,
UsagePublisher $publisherForUsage,
Webhook $queueForWebhooks,
Certificate $publisherForCertificates,
BuildPublisher $publisherForBuilds,
Messaging $queueForMessaging,
MessagingPublisher $publisherForMessaging,
MigrationPublisher $publisherForMigrations,
Screenshot $publisherForScreenshots,
): void {
@@ -114,7 +114,7 @@ class Get extends Base
System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $queueForDatabase,
System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $queueForDeletes,
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $publisherForAudits,
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $queueForMails,
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $publisherForMails,
System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions,
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $publisherForStatsResources,
System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $publisherForUsage,
@@ -122,7 +122,7 @@ class Get extends Base
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $publisherForCertificates,
System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $publisherForBuilds,
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $publisherForScreenshots,
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $publisherForMessaging,
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations,
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unknown queue name: ' . $name),
};
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Mails;
use Appwrite\Event\Mail;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForMails')
->inject('publisherForMails')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Mail $queueForMails, Response $response): void
public function action(int|string $threshold, MailPublisher $publisherForMails, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForMails->getSize();
$size = $publisherForMails->getSize();
$this->assertQueueThreshold($size, $threshold);
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Messaging;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Messaging $queueForMessaging, Response $response): void
public function action(int|string $threshold, MessagingPublisher $publisherForMessaging, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForMessaging->getSize();
$size = $publisherForMessaging->getSize();
$this->assertQueueThreshold($size, $threshold);
@@ -33,7 +33,6 @@ class Delete extends Action
->desc('Delete project')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'project.delete')
->label('audits.event', 'project.delete')
->label('audits.resource', 'project/{project.$id}')
->label('sdk', new Method(
@@ -3,8 +3,22 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Google;
use Appwrite\Auth\OAuth2\Google;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Update extends Base
{
@@ -52,4 +66,118 @@ class Update extends Base
{
return 'GOCSPX-2k8gsR0000000000000000VNahJj';
}
public static function getParameters(): array
{
return \array_merge(parent::getParameters(), [
[
'$id' => 'prompt',
'name' => 'Prompt',
'example' => '["consent"]',
'hint' => '',
],
]);
}
public function __construct()
{
$providerId = static::getProviderId();
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
->label('scope', 'oauth2.write')
->label('event', 'oauth2.[providerId].update')
->label('audits.event', 'project.oauth2.[providerId].update')
->label('audits.resource', 'project.oauth2/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'oauth2',
name: static::getProviderSDKMethod(),
description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: static::getResponseModel(),
)
],
))
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('prompt', null, new Nullable(new ArrayList(new WhiteList(['none', 'consent', 'select_account'], true), 3)), 'Array of Google OAuth2 prompt values. If "none" is included, it must be the only element. "none" means: don\'t display any authentication or consent screens. Must not be specified with other values. "consent" means: prompt the user for consent. "select_account" means: prompt the user to select an account.', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->inject('queueForEvents')
->callback($this->handle(...));
}
public function buildReadResponse(Document $project): Document
{
$providerId = static::getProviderId();
$oAuthProviders = $project->getAttribute('oAuthProviders', []);
$decoded = $this->decodeStoredSecret($project);
return new Document([
'$id' => $providerId,
'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false,
static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '',
static::getClientSecretParamName() => '',
'prompt' => $decoded['prompt'] ?? ['consent'],
]);
}
/**
* Custom callback used instead of the parent's `action()` because Google
* takes an additional optional `prompt` parameter. The method is named
* differently to avoid an LSP-incompatible override of Base::action().
*/
public function handle(
?string $clientId,
?string $clientSecret,
?array $prompt,
?bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization,
QueueEvent $queueForEvents
): void {
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
if ($prompt !== null) {
if (empty($prompt)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Prompt array cannot be empty.');
}
if (\in_array('none', $prompt) && \count($prompt) > 1) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When "none" is used as a prompt value, it must be the only element in the array.');
}
}
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
$existing = $this->decodeStoredSecret($project);
// Backwards compatibility: secrets stored before the prompt feature
// were saved as plain strings. Treat the raw value as clientSecret.
if (!empty($storedRaw) && empty($existing)) {
$existing = ['clientSecret' => $storedRaw];
}
$encodedSecret = \json_encode([
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'prompt' => $prompt ?? ($existing['prompt'] ?? ['consent']),
]);
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
$response->dynamic($this->buildReadResponse($project), static::getResponseModel());
}
}
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Extend\Exception as Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +68,7 @@ class Create extends Action
->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', optional: true, deprecated: true) // Backwards compatibility
->inject('response')
->inject('project')
->inject('queueForMails')
->inject('publisherForMails')
->inject('plan')
->callback($this->action(...));
}
@@ -87,7 +88,7 @@ class Create extends Action
string $paramSecure, // Backwards compatibility
Response $response,
Document $project,
Mail $queueForMails,
MailPublisher $publisherForMails,
array $plan
): void {
// Backwards compatibility: use inline params if provided, otherwise fall back to project SMTP config.
@@ -153,23 +154,24 @@ class Create extends Action
->setParam('{{privacyUrl}}', $plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL);
foreach ($emails as $email) {
$queueForMails
->setSmtpHost($host)
->setSmtpPort($port)
->setSmtpUsername($username)
->setSmtpPassword($password)
->setSmtpSecure($secure)
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName)
->setRecipient($email)
->setName('')
->setBodyTemplate(APP_CE_CONFIG_DIR . '/locale/templates/email-base-styled.tpl')
->setBody($template->render())
->setVariables([])
->setSubject($subject)
->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $email,
subject: $subject,
bodyTemplate: APP_CE_CONFIG_DIR . '/locale/templates/email-base-styled.tpl',
body: $template->render(),
smtp: [
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
'secure' => $secure,
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
],
));
}
$response->noContent();
@@ -245,28 +245,43 @@ class Get extends Action
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED, $e->getMessage());
}
$image->crop((int) $width, (int) $height, $gravity);
if ($width > 0 || $height > 0 || $gravity !== Image::GRAVITY_CENTER) {
Span::add('storage.transform.crop.width', $width);
Span::add('storage.transform.crop.height', $height);
Span::add('storage.transform.crop.gravity', $gravity);
$image->crop($width, $height, $gravity);
}
if (!empty($opacity)) {
if ($opacity !== 1.0) {
Span::add('storage.transform.opacity', $opacity);
$image->setOpacity($opacity);
}
if (!empty($background)) {
Span::add('storage.transform.background', $background);
$image->setBackground('#' . $background);
}
if (!empty($borderWidth)) {
if ($borderWidth > 0) {
Span::add('storage.transform.border.width', $borderWidth);
Span::add('storage.transform.border.color', $borderColor);
$image->setBorder($borderWidth, '#' . $borderColor);
}
if (!empty($borderRadius)) {
if ($borderRadius > 0) {
Span::add('storage.transform.borderRadius', $borderRadius);
$image->setBorderRadius($borderRadius);
}
if (!empty($rotation)) {
if ($rotation !== 0) {
Span::add('storage.transform.rotation', $rotation);
$image->setRotation(($rotation + 360) % 360);
}
if ($quality !== -1) {
Span::add('storage.transform.quality', $quality);
}
$data = $image->output($output, $quality);
$renderingTime = \microtime(true) - $startTime - $downloadTime - $decryptionTime - $decompressionTime;
@@ -4,8 +4,10 @@ namespace Appwrite\Platform\Modules\Teams\Http\Memberships;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
@@ -87,18 +89,19 @@ class Create extends Action
->inject('dbForProject')
->inject('authorization')
->inject('locale')
->inject('queueForMails')
->inject('queueForMessaging')
->inject('publisherForMails')
->inject('publisherForMessaging')
->inject('queueForEvents')
->inject('timelimit')
->inject('usage')
->inject('plan')
->inject('platform')
->inject('proofForPassword')
->inject('proofForToken')
->callback($this->action(...));
}
public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, Password $proofForPassword, Token $proofForToken)
public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, MailPublisher $publisherForMails, MessagingPublisher $publisherForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, array $platform, Password $proofForPassword, Token $proofForToken)
{
$isAppUser = $user->isApp($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
@@ -345,6 +348,7 @@ class Create extends Action
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (! empty($smtp['senderEmail'])) {
@@ -362,13 +366,6 @@ class Create extends Action
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (! empty($customTemplate)) {
if (! empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -389,11 +386,17 @@ class Create extends Action
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$emailVariables = [
@@ -406,14 +409,17 @@ class Create extends Action
'project' => $projectName,
];
$queueForMails
->setSubject($subject)
->setBody($body)
->setPreview($preview)
->setRecipient($invitee->getAttribute('email'))
->setName($invitee->getAttribute('name', ''))
->appendVariables($emailVariables)
->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $invitee->getAttribute('email'),
name: $invitee->getAttribute('name', ''),
subject: $subject,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
platform: $platform,
));
} elseif (! empty($phone)) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
@@ -431,11 +437,13 @@ class Create extends Action
],
]);
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType('SMS');
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_INTERNAL,
project: $project,
message: $messageDoc,
recipients: [$phone],
providerType: 'SMS',
));
$helper = PhoneNumberUtil::getInstance();
try {
+4
View File
@@ -7,6 +7,7 @@ use Appwrite\SDK\Language\Android;
use Appwrite\SDK\Language\Apple;
use Appwrite\SDK\Language\ClaudePlugin;
use Appwrite\SDK\Language\CLI;
use Appwrite\SDK\Language\CodexPlugin;
use Appwrite\SDK\Language\CursorPlugin;
use Appwrite\SDK\Language\Dart;
use Appwrite\SDK\Language\Deno;
@@ -455,6 +456,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
case 'claude-plugin':
$config = new ClaudePlugin();
break;
case 'codex-plugin':
$config = new CodexPlugin();
break;
default:
throw new \Exception('Language "' . $language['key'] . '" not supported');
}
@@ -2,14 +2,20 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Messaging;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Utopia\Database\Database;
use Utopia\Queue\Queue;
use Utopia\System\System;
class ScheduleMessages extends ScheduleBase
{
public const UPDATE_TIMER = 3; // seconds
public const ENQUEUE_TIMER = 4; // seconds
private ?MessagingPublisher $publisherForMessaging = null;
public static function getName(): string
{
return 'schedule-messages';
@@ -27,6 +33,11 @@ class ScheduleMessages extends ScheduleBase
protected function enqueueResources(Database $dbForPlatform, callable $getProjectDB): void
{
$publisherForMessaging = $this->publisherForMessaging ??= new MessagingPublisher(
$this->publisherMessaging,
new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
);
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
continue;
@@ -39,16 +50,14 @@ class ScheduleMessages extends ScheduleBase
continue;
}
\go(function () use ($schedule, $scheduledAt, $dbForPlatform) {
$queueForMessaging = new Messaging($this->publisherMessaging);
\go(function () use ($schedule, $scheduledAt, $dbForPlatform, $publisherForMessaging) {
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($schedule['resourceId'])
->setProject($schedule['project'])
->trigger();
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $schedule['project'],
messageId: $schedule['resourceId'],
));
$dbForPlatform->deleteDocument(
'schedules',
+20 -19
View File
@@ -5,8 +5,9 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
@@ -50,7 +51,7 @@ class Certificates extends Action
->desc('Certificates worker')
->inject('message')
->inject('dbForPlatform')
->inject('queueForMails')
->inject('publisherForMails')
->inject('queueForEvents')
->inject('queueForWebhooks')
->inject('queueForFunctions')
@@ -66,7 +67,7 @@ class Certificates extends Action
/**
* @param Message $message
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param MailPublisher $publisherForMails
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
* @param Func $queueForFunctions
@@ -83,7 +84,7 @@ class Certificates extends Action
public function action(
Message $message,
Database $dbForPlatform,
Mail $queueForMails,
MailPublisher $publisherForMails,
Event $queueForEvents,
Webhook $queueForWebhooks,
Func $queueForFunctions,
@@ -116,7 +117,7 @@ class Certificates extends Action
break;
case \Appwrite\Event\Certificate::ACTION_GENERATION:
$this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
$this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $publisherForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
break;
default:
@@ -209,7 +210,7 @@ class Certificates extends Action
* @param Domain $domain
* @param ?string $domainType
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param MailPublisher $publisherForMails
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
* @param Func $queueForFunctions
@@ -233,7 +234,7 @@ class Certificates extends Action
Domain $domain,
?string $domainType,
Database $dbForPlatform,
Mail $queueForMails,
MailPublisher $publisherForMails,
Event $queueForEvents,
Webhook $queueForWebhooks,
Func $queueForFunctions,
@@ -358,7 +359,7 @@ class Certificates extends Action
$rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATION_FAILED);
// Send email to security email
$this->notifyError($domain->get(), $e->getMessage(), $attempts, $queueForMails, $plan);
$this->notifyError($domain->get(), $e->getMessage(), $attempts, $publisherForMails, $plan);
throw $e;
} finally {
@@ -524,12 +525,12 @@ class Certificates extends Action
* @param string $domain Domain that caused the error
* @param string $errorMessage Verbose error message
* @param int $attempt How many times it failed already
* @param Mail $queueForMails
* @param MailPublisher $publisherForMails
* @param array $plan
* @return void
* @throws Exception
*/
private function notifyError(string $domain, string $errorMessage, int $attempt, Mail $queueForMails, array $plan): void
private function notifyError(string $domain, string $errorMessage, int $attempt, MailPublisher $publisherForMails, array $plan): void
{
// Log error into console
Console::warning('Cannot renew domain (' . $domain . ') on attempt no. ' . $attempt . ' certificate: ' . $errorMessage);
@@ -560,14 +561,14 @@ class Certificates extends Action
$subject = $locale->getText("emails.certificate.subject");
$preview = $locale->getText("emails.certificate.preview");
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setName('Appwrite Administrator')
->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl')
->setVariables($emailVariables)
->setRecipient(System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')))
->trigger();
$publisherForMails->enqueue(new MailMessage(
recipient: System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')),
name: 'Appwrite Administrator',
subject: $subject,
bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl',
body: $body,
preview: $preview,
variables: $emailVariables,
));
}
}
+39 -23
View File
@@ -3,9 +3,10 @@
namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Migration;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Extend\Exception;
@@ -102,7 +103,7 @@ class Migrations extends Action
->inject('queueForRealtime')
->inject('deviceForMigrations')
->inject('deviceForFiles')
->inject('queueForMails')
->inject('publisherForMails')
->inject('usage')
->inject('publisherForUsage')
->inject('plan')
@@ -124,7 +125,7 @@ class Migrations extends Action
Realtime $queueForRealtime,
Device $deviceForMigrations,
Device $deviceForFiles,
Mail $queueForMails,
MailPublisher $publisherForMails,
Context $usage,
UsagePublisher $publisherForUsage,
array $plan,
@@ -163,7 +164,7 @@ class Migrations extends Action
$this->processMigration(
$migration,
$queueForRealtime,
$queueForMails,
$publisherForMails,
$usage,
$publisherForUsage,
$platform,
@@ -293,6 +294,7 @@ class Migrations extends Action
$this->getDatabasesDB,
Config::getParam('collections', [])['databases']['collections'],
OnDuplicate::tryFrom($options['onDuplicate'] ?? '') ?? OnDuplicate::Fail,
$this->resolveDestinationDatabaseDsn(...),
),
DestinationCSV::getName() => new DestinationCSV(
$this->deviceForFiles,
@@ -316,6 +318,19 @@ class Migrations extends Action
};
}
/**
* Legacy / tablesdb databases route to the destination project's DSN (same as a fresh
* Databases create), while documentsdb / vectorsdb keep the source DSN the dedicated-DB
* backfill that would re-point them is not run during migrations.
*/
private function resolveDestinationDatabaseDsn(ResourceDatabase $resource): string
{
return match ($resource->getType()) {
DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB => (string) $resource->getDatabase(),
default => (string) $this->project->getAttribute('database', ''),
};
}
/**
* @throws AuthorizationException
* @throws Structure
@@ -426,7 +441,7 @@ class Migrations extends Action
protected function processMigration(
Document $migration,
Realtime $queueForRealtime,
Mail $queueForMails,
MailPublisher $publisherForMails,
Context $usage,
UsagePublisher $publisherForUsage,
array $platform,
@@ -630,7 +645,7 @@ class Migrations extends Action
}
$destination_type = $migration->getAttribute('destination');
if ($destination_type === DestinationCSV::getName() || $destination_type === DestinationJSON::getName()) {
$this->handleDataExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization);
$this->handleDataExportComplete($project, $migration, $publisherForMails, $queueForRealtime, $platform, $authorization);
}
} finally {
$source?->cleanup();
@@ -657,7 +672,7 @@ class Migrations extends Action
*
* @param Document $project
* @param Document $migration
* @param Mail $queueForMails
* @param MailPublisher $publisherForMails
* @param Realtime $queueForRealtime
* @param array $platform
* @param Authorization $authorization
@@ -666,7 +681,7 @@ class Migrations extends Action
protected function handleDataExportComplete(
Document $project,
Document $migration,
Mail $queueForMails,
MailPublisher $publisherForMails,
Realtime $queueForRealtime,
array $platform,
Authorization $authorization,
@@ -718,7 +733,7 @@ class Migrations extends Action
project: $project,
user: $user,
options: $options,
queueForMails: $queueForMails,
publisherForMails: $publisherForMails,
platform: $platform,
exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV',
sizeMB: $sizeMB
@@ -781,7 +796,7 @@ class Migrations extends Action
project: $project,
user: $user,
options: $options,
queueForMails: $queueForMails,
publisherForMails: $publisherForMails,
platform: $platform,
exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV',
downloadUrl: $downloadUrl
@@ -795,7 +810,7 @@ class Migrations extends Action
* @param Document $project
* @param Document $user The user who triggered the operation
* @param array $options Migration options
* @param Mail $queueForMails
* @param MailPublisher $publisherForMails
* @param array $platform
* @param string $downloadUrl Download URL for successful exports
* @param float $sizeMB File size in MB for failed exports
@@ -807,7 +822,7 @@ class Migrations extends Action
Document $project,
Document $user,
array $options,
Mail $queueForMails,
MailPublisher $publisherForMails,
array $platform,
string $exportType = 'CSV',
string $downloadUrl = '',
@@ -877,17 +892,18 @@ class Migrations extends Action
'type' => $exportType,
];
$queueForMails
->setProject($project)
->setSubject($subject)
->setPreview($preview)
->setBody($emailBody)
->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl')
->setVariables($emailVariables)
->setName($user->getAttribute('name', $user->getAttribute('email')))
->setRecipient($user->getAttribute('email'))
->setSenderName($platform['emailSenderName'])
->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $user->getAttribute('email'),
name: $user->getAttribute('name', $user->getAttribute('email')),
subject: $subject,
bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl',
body: $emailBody,
preview: $preview,
variables: $emailVariables,
customMailOptions: ['senderName' => $platform['emailSenderName']],
platform: $platform,
));
Console::info("CSV export {$emailType} notification email sent to " . $user->getAttribute('email'));
}
+20 -21
View File
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Template\Template;
use Appwrite\Usage\Context as UsageContext;
@@ -36,7 +37,7 @@ class Webhooks extends Action
->inject('message')
->inject('project')
->inject('dbForPlatform')
->inject('queueForMails')
->inject('publisherForMails')
->inject('publisherForUsage')
->inject('log')
->inject('plan')
@@ -47,14 +48,14 @@ class Webhooks extends Action
* @param Message $message
* @param Document $project
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param MailPublisher $publisherForMails
* @param UsagePublisher $publisherForUsage
* @param Log $log
* @param array $plan
* @return void
* @throws Exception
*/
public function action(Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, UsagePublisher $publisherForUsage, Log $log, array $plan): void
public function action(Message $message, Document $project, Database $dbForPlatform, MailPublisher $publisherForMails, UsagePublisher $publisherForUsage, Log $log, array $plan): void
{
$this->errors = [];
$payload = $message->getPayload();
@@ -73,7 +74,7 @@ class Webhooks extends Action
foreach ($project->getAttribute('webhooks', []) as $webhook) {
if (array_intersect($webhook->getAttribute('events', []), $events)) {
$this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $publisherForUsage, $plan);
$this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $publisherForMails, $publisherForUsage, $plan);
}
}
@@ -89,11 +90,11 @@ class Webhooks extends Action
* @param Document $user
* @param Document $project
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param MailPublisher $publisherForMails
* @param array $plan
* @return void
*/
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Mail $queueForMails, UsagePublisher $publisherForUsage, array $plan): void
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, MailPublisher $publisherForMails, UsagePublisher $publisherForUsage, array $plan): void
{
if ($webhook->getAttribute('enabled') !== true) {
return;
@@ -171,7 +172,7 @@ class Webhooks extends Action
if ($attempts >= \intval(System::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) {
$webhook->setAttribute('enabled', false);
$updatePayload['enabled'] = false;
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForMails, $plan);
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $publisherForMails, $plan);
}
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), new Document($updatePayload));
@@ -203,11 +204,11 @@ class Webhooks extends Action
* @param Document $webhook
* @param Document $project
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param MailPublisher $publisherForMails
* @param array $plan
* @return void
*/
public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, Mail $queueForMails, array $plan): void
public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, MailPublisher $publisherForMails, array $plan): void
{
$memberships = $dbForPlatform->find('memberships', [
Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]),
@@ -251,18 +252,16 @@ class Webhooks extends Action
->setParam('{{message}}', $template->render())
->setParam('{{year}}', date("Y"));
$queueForMails
->setProject($project)
->setSubject($subject)
->setPreview($preview)
->setBody($body->render());
foreach ($users as $user) {
$queueForMails
->setVariables(['user' => $user->getAttribute('name', '')])
->setName($user->getAttribute('name', ''))
->setRecipient($user->getAttribute('email'))
->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $user->getAttribute('email'),
name: $user->getAttribute('name', ''),
subject: $subject,
body: $body->render(),
preview: $preview,
variables: ['user' => $user->getAttribute('name', '')],
));
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ use Utopia\DI\Container;
class Swoole extends Promise
{
private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container';
private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia__';
public function __construct(?callable $executor = null)
{
@@ -524,6 +524,7 @@ class OpenAPI3 extends Format
case \Appwrite\Utopia\Database\Validator\Queries\Identities::class:
case \Appwrite\Utopia\Database\Validator\Queries\Indexes::class:
case \Appwrite\Utopia\Database\Validator\Queries\Installations::class:
case \Appwrite\Utopia\Database\Validator\Queries\Branches::class:
case \Appwrite\Utopia\Database\Validator\Queries\Memberships::class:
case \Appwrite\Utopia\Database\Validator\Queries\Messages::class:
case \Appwrite\Utopia\Database\Validator\Queries\Migrations::class:
@@ -755,7 +756,18 @@ class OpenAPI3 extends Format
$node['schema']['default'] = $param['default'];
}
if (false !== \strpos($url, ':' . $name)) { // Param is in URL path
$pathAliases = [$name, ...($param['aliases'] ?? [])];
$pathAliasMap = \array_flip($pathAliases);
$isPathParam = false;
foreach (\explode('/', $url) as $segment) {
if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
$isPathParam = true;
break;
}
}
if ($isPathParam) { // Param is in URL path (directly or through alias)
$node['in'] = 'path';
$temp['parameters'][] = $node;
} elseif ($route->getMethod() == 'GET') { // Param is in query
@@ -796,7 +808,14 @@ class OpenAPI3 extends Format
}
}
$url = \str_replace(':' . $name, '{' . $name . '}', $url);
$segments = \explode('/', $url);
foreach ($segments as &$segment) {
if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
$segment = '{' . $name . '}';
}
}
unset($segment);
$url = \implode('/', $segments);
}
if (!empty($bodyRequired)) {
@@ -511,6 +511,7 @@ class Swagger2 extends Format
case \Utopia\Database\Validator\Queries::class:
case \Utopia\Database\Validator\Queries\Document::class:
case \Utopia\Database\Validator\Queries\Documents::class:
case \Appwrite\Utopia\Database\Validator\Queries\Branches::class:
case \Appwrite\Utopia\Database\Validator\Queries\Columns::class:
case \Appwrite\Utopia\Database\Validator\Queries\Tables::class:
$node['type'] = 'array';
@@ -722,7 +723,18 @@ class Swagger2 extends Format
$node['default'] = $param['default'];
}
if (\str_contains($url, ':' . $name)) { // Param is in URL path
$pathAliases = [$name, ...($param['aliases'] ?? [])];
$pathAliasMap = \array_flip($pathAliases);
$isPathParam = false;
foreach (\explode('/', $url) as $segment) {
if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
$isPathParam = true;
break;
}
}
if ($isPathParam) { // Param is in URL path (directly or through alias)
$node['in'] = 'path';
$temp['parameters'][] = $node;
} elseif ($route->getMethod() == 'GET') { // Param is in query
@@ -767,7 +779,14 @@ class Swagger2 extends Format
}
}
$url = \str_replace(':' . $name, '{' . $name . '}', $url);
$segments = \explode('/', $url);
foreach ($segments as &$segment) {
if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
$segment = '{' . $name . '}';
}
}
unset($segment);
$url = \implode('/', $segments);
}
if (!empty($bodyRequired)) {
@@ -25,6 +25,20 @@ class OAuth2Google extends OAuth2Base
return 'GOCSPX-2k8gsR0000000000000000VNahJj';
}
public function __construct()
{
parent::__construct();
$this->addRule('prompt', [
'type' => self::TYPE_ENUM,
'description' => 'Google OAuth2 prompt values.',
'default' => ['consent'],
'example' => ['consent'],
'array' => true,
'enum' => ['none', 'consent', 'select_account'],
]);
}
/**
* Get Name
*
+20 -5
View File
@@ -50,6 +50,8 @@ class Comment
protected string $statePrefix = '[appwrite]: #';
protected ?string $tip = null;
/**
* @var mixed[] $builds
*/
@@ -81,7 +83,14 @@ class Comment
public function generateComment(): string
{
$json = \json_encode($this->builds);
if ($this->tip === null) {
$this->tip = $this->tips[\array_rand($this->tips)];
}
$json = \json_encode([
'builds' => $this->builds,
'tip' => $this->tip,
]);
$text = $this->statePrefix . \base64_encode($json) . "\n\n";
@@ -226,8 +235,7 @@ class Comment
$i++;
}
$tip = $this->tips[array_rand($this->tips)];
$text .= "\n<br>\n\n> [!TIP]\n> $tip\n\n";
$text .= "\n<br>\n\n> [!TIP]\n> {$this->tip}\n\n";
return $text;
}
@@ -252,8 +260,15 @@ class Comment
$json = \base64_decode($state);
$builds = \json_decode($json, true);
$this->builds = \is_array($builds) ? $builds : [];
$data = \json_decode($json, true);
if (\is_array($data) && \array_key_exists('builds', $data)) {
$this->builds = \is_array($data['builds']) ? $data['builds'] : [];
$this->tip = $data['tip'] ?? null;
} else {
// Backward compatibility with old state format (builds array only)
$this->builds = \is_array($data) ? $data : [];
}
return $this;
}
@@ -10,7 +10,6 @@ use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
class DatabasesConsoleClientTest extends Scope
{
@@ -258,55 +257,4 @@ class DatabasesConsoleClientTest extends Scope
$this->assertIsArray($response['body']['documents']);
}
#[Depends('testCreateCollection')]
public function testGetCollectionLogs(array $data)
{
$databaseId = $data['databaseId'];
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::limit(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::offset(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
}
}
@@ -10,7 +10,6 @@ use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
class VectorsDBConsoleClientTest extends Scope
{
@@ -258,55 +257,4 @@ class VectorsDBConsoleClientTest extends Scope
$this->assertIsArray($response['body']['documents']);
}
#[Depends('testCreateCollection')]
public function testGetCollectionLogs(array $data)
{
$databaseId = $data['databaseId'];
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::limit(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::offset(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
}
}
+18 -2
View File
@@ -4,6 +4,7 @@ namespace Tests\E2E\Services\GraphQL;
use CURLFile;
use Utopia\Console;
use Utopia\Image\Image;
trait Base
{
@@ -516,6 +517,21 @@ trait Base
}
';
protected function assertFilePreviewResponse(array $file): void
{
$this->assertEquals(200, $file['headers']['status-code']);
$this->assertEquals('image/png', $file['headers']['content-type']);
$this->assertNotEmpty($file['body']);
$image = new Image($file['body']);
$dimensions = \getimagesizefromstring($file['body']);
$this->assertNotEmpty($image->output('png'));
$this->assertIsArray($dimensions);
$this->assertEquals(100, $dimensions[0]);
$this->assertEquals(100, $dimensions[1]);
}
public function getQuery(string $name): string
{
switch ($name) {
@@ -2388,8 +2404,8 @@ trait Base
}
}';
case self::GET_FILE_PREVIEW:
return 'query getFilePreview($bucketId: String!, $fileId: String!) {
storageGetFilePreview(bucketId: $bucketId, fileId: $fileId) {
return 'query getFilePreview($bucketId: String!, $fileId: String!, $width: Int, $height: Int) {
storageGetFilePreview(bucketId: $bucketId, fileId: $fileId, width: $width, height: $height) {
status
}
}';
@@ -200,7 +200,7 @@ class StorageClientTest extends Scope
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertEquals(46719, \strlen($file['body']));
$this->assertFilePreviewResponse($file);
return $file;
}
@@ -262,7 +262,7 @@ class StorageServerTest extends Scope
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertEquals(46719, \strlen($file['body']));
$this->assertFilePreviewResponse($file);
return $file;
}
+153
View File
@@ -2564,6 +2564,159 @@ trait OAuth2Base
]);
}
// =========================================================================
// Update Google (clientId + clientSecret + optional prompt)
// =========================================================================
/**
* Default prompt MUST run before any other Google test that sets a custom
* prompt value. The global resetProjectOAuth2() only clears Amazon state,
* so Google state leaks across tests in the same class. Running this first
* guarantees the stored JSON blob has no pre-existing "prompt" key.
*/
public function testUpdateOAuth2GoogleDefaultPrompt(): void
{
// When prompt is omitted and nothing is stored, the default is ['consent'].
$response = $this->updateOAuth2('google', [
'clientId' => 'google-default-client',
'clientSecret' => 'google-default-secret',
'enabled' => false,
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame(['consent'], $response['body']['prompt']);
// Cleanup
$this->updateOAuth2('google', [
'clientId' => '',
'clientSecret' => '',
'enabled' => false,
]);
}
public function testUpdateOAuth2Google(): void
{
$response = $this->updateOAuth2('google', [
'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
'prompt' => ['select_account'],
'enabled' => false,
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('google', $response['body']['$id']);
$this->assertSame('120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com', $response['body']['clientId']);
$this->assertSame(['select_account'], $response['body']['prompt']);
// Cleanup
$this->updateOAuth2('google', [
'clientId' => '',
'clientSecret' => '',
'enabled' => false,
]);
}
public function testUpdateOAuth2GooglePartialPreservesPrompt(): void
{
// Seed clientSecret + prompt.
$this->updateOAuth2('google', [
'clientId' => 'google-seed-client',
'clientSecret' => 'google-seed-secret',
'prompt' => ['consent', 'select_account'],
'enabled' => false,
]);
// Update only clientId.
$response = $this->updateOAuth2('google', [
'clientId' => 'google-rotated-client',
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('google-rotated-client', $response['body']['clientId']);
$this->assertSame(['consent', 'select_account'], $response['body']['prompt']);
// Cleanup
$this->updateOAuth2('google', [
'clientId' => '',
'clientSecret' => '',
'enabled' => false,
]);
}
public function testUpdateOAuth2GooglePromptNoneAloneRejected(): void
{
$response = $this->updateOAuth2('google', [
'clientId' => 'whatever',
'clientSecret' => 'whatever',
'prompt' => ['none', 'consent'],
'enabled' => false,
]);
$this->assertSame(400, $response['headers']['status-code']);
$this->assertSame('general_argument_invalid', $response['body']['type']);
}
public function testUpdateOAuth2GooglePromptEmptyArrayRejected(): void
{
$response = $this->updateOAuth2('google', [
'clientId' => 'whatever',
'clientSecret' => 'whatever',
'prompt' => [],
'enabled' => false,
]);
$this->assertSame(400, $response['headers']['status-code']);
$this->assertSame('general_argument_invalid', $response['body']['type']);
}
public function testUpdateOAuth2GooglePromptNoneAloneAccepted(): void
{
$response = $this->updateOAuth2('google', [
'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
'prompt' => ['none'],
'enabled' => false,
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame(['none'], $response['body']['prompt']);
// Cleanup
$this->updateOAuth2('google', [
'clientId' => '',
'clientSecret' => '',
'enabled' => false,
]);
}
public function testUpdateOAuth2GoogleEnableAndReadBack(): void
{
$update = $this->updateOAuth2('google', [
'clientId' => 'google-enable-client',
'clientSecret' => 'google-enable-secret',
'prompt' => ['select_account'],
'enabled' => true,
]);
$this->assertSame(200, $update['headers']['status-code']);
$this->assertTrue($update['body']['enabled']);
// GET must hide clientSecret while keeping clientId and prompt.
$get = $this->getOAuth2Provider('google');
$this->assertSame(200, $get['headers']['status-code']);
$this->assertTrue($get['body']['enabled']);
$this->assertSame('google-enable-client', $get['body']['clientId']);
$this->assertSame(['select_account'], $get['body']['prompt']);
$this->assertSame('', $get['body']['clientSecret']);
// Cleanup
$this->updateOAuth2('google', [
'clientId' => '',
'clientSecret' => '',
'enabled' => false,
]);
}
// =========================================================================
// Smoke test: every plain (clientId + clientSecret) provider
//
@@ -2633,6 +2633,7 @@ class SitesCustomServerTest extends Scope
// Poll for execution logs to be written (async)
// Filter by requestPath to avoid picking up screenshot worker executions
// Wait for both the execution entry AND its logs field to be populated
$logs = null;
$timeout = 120;
$start = \time();
@@ -2642,12 +2643,13 @@ class SitesCustomServerTest extends Scope
Query::equal('requestPath', ['/logs-inline'])->toString(),
Query::limit(1)->toString(),
]);
if (!empty($logs['body']['executions'])) {
if (!empty($logs['body']['executions']) && !empty($logs['body']['executions'][0]['logs'])) {
break;
}
\usleep(500000);
}
$this->assertNotEmpty($logs['body']['executions'], 'Execution logs were not available within timeout');
$this->assertNotNull($logs['body']['executions'][0]['logs'], 'Execution logs content was not populated within timeout');
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertStringContainsString($deploymentId, $logs['body']['executions'][0]['deploymentId']);
$this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']);
@@ -2681,11 +2683,21 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Action logs printed.", $response['body']);
$logs = $this->listLogs($siteId, [
Query::orderDesc('$createdAt')->toString(),
Query::equal('requestPath', ['/logs-action'])->toString(),
Query::limit(1)->toString(),
]);
$logs = null;
$start = \time();
while (\time() - $start < $timeout) {
$logs = $this->listLogs($siteId, [
Query::orderDesc('$createdAt')->toString(),
Query::equal('requestPath', ['/logs-action'])->toString(),
Query::limit(1)->toString(),
]);
if (!empty($logs['body']['executions']) && !empty($logs['body']['executions'][0]['logs'])) {
break;
}
\usleep(500000);
}
$this->assertNotEmpty($logs['body']['executions'], 'Action execution logs were not available within timeout');
$this->assertNotNull($logs['body']['executions'][0]['logs'], 'Action execution logs content was not populated within timeout');
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertStringContainsString($deploymentId, $logs['body']['executions'][0]['deploymentId']);
$this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']);
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace Tests\Unit\Vcs;
use Appwrite\Vcs\Comment;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Document;
class CommentTest extends TestCase
{
public function testTipIsPreservedAcrossMultipleGenerations(): void
{
$comment = new Comment(['consoleHostname' => 'localhost']);
$comment->addBuild(
new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']),
new Document(['$id' => 'func1', 'name' => 'Test Function']),
'function',
'ready',
'dep1',
['type' => 'logs'],
''
);
$first = $comment->generateComment();
$firstTip = $this->extractTip($first);
$this->assertNotNull($firstTip);
$this->assertNotEmpty($firstTip);
$second = $comment->generateComment();
$secondTip = $this->extractTip($second);
$this->assertEquals($firstTip, $secondTip);
}
public function testTipIsRestoredFromParsedComment(): void
{
$comment = new Comment(['consoleHostname' => 'localhost']);
$comment->addBuild(
new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']),
new Document(['$id' => 'func1', 'name' => 'Test Function']),
'function',
'ready',
'dep1',
['type' => 'logs'],
''
);
$original = $comment->generateComment();
$originalTip = $this->extractTip($original);
$parsed = new Comment(['consoleHostname' => 'localhost']);
$parsed->parseComment($original);
$parsed->addBuild(
new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']),
new Document(['$id' => 'func2', 'name' => 'Another Function']),
'function',
'building',
'dep2',
['type' => 'logs'],
''
);
$regenerated = $parsed->generateComment();
$regeneratedTip = $this->extractTip($regenerated);
$this->assertEquals($originalTip, $regeneratedTip);
}
public function testBackwardCompatibilityWithOldStateFormat(): void
{
$oldBuilds = [
'project1_func1' => [
'projectName' => 'Test Project',
'projectId' => 'project1',
'region' => 'default',
'resourceName' => 'Test Function',
'resourceId' => 'func1',
'resourceType' => 'function',
'buildStatus' => 'ready',
'deploymentId' => 'dep1',
'action' => ['type' => 'logs'],
'previewUrl' => '',
],
];
$oldState = '[appwrite]: #' . \base64_encode(\json_encode($oldBuilds)) . "\n\n";
$oldState .= "> [!TIP]\n> Old tip that should be ignored\n\n";
$comment = new Comment(['consoleHostname' => 'localhost']);
$comment->parseComment($oldState);
$new = $comment->generateComment();
$newTip = $this->extractTip($new);
$this->assertNotNull($newTip);
$this->assertNotEquals('Old tip that should be ignored', $newTip);
$this->assertContains($newTip, $this->getTips());
}
public function testParseOldStateFormatWithOnlyBuilds(): void
{
$oldBuilds = [
'project1_func1' => [
'projectName' => 'Test Project',
'projectId' => 'project1',
'region' => 'default',
'resourceName' => 'Test Function',
'resourceId' => 'func1',
'resourceType' => 'function',
'buildStatus' => 'ready',
'deploymentId' => 'dep1',
'action' => ['type' => 'logs'],
'previewUrl' => '',
],
];
$state = '[appwrite]: #' . \base64_encode(\json_encode($oldBuilds)) . "\n\n";
$comment = new Comment(['consoleHostname' => 'localhost']);
$comment->parseComment($state);
$this->assertEquals(false, $comment->isEmpty());
$first = $comment->generateComment();
$firstTip = $this->extractTip($first);
$this->assertNotNull($firstTip);
$this->assertNotEmpty($firstTip);
$this->assertContains($firstTip, $this->getTips());
$second = $comment->generateComment();
$secondTip = $this->extractTip($second);
$this->assertEquals($firstTip, $secondTip);
}
private function extractTip(string $comment): ?string
{
if (\preg_match('/> \[!TIP\]\n> (.+)/', $comment, $matches)) {
return $matches[1];
}
return null;
}
private function getTips(): array
{
$reflection = new \ReflectionClass(Comment::class);
$property = $reflection->getProperty('tips');
return $property->getValue(new Comment(['consoleHostname' => 'localhost']));
}
}