Compare commits

..
Author SHA1 Message Date
Matej Bačo e7f78b3e01 Fix shutdown event errors 2026-04-15 15:29:05 +02:00
Matej Bačo 7376c5b517 Fix protocol endpoint causing InvalidArgumentException 2026-04-15 15:09:40 +02:00
ArnabChatterjee20kandGitHub db84c79802 Merge pull request #11907 from appwrite/update-events-not-firing
added backward compat
2026-04-15 18:32:21 +05:30
ArnabChatterjee20k 6ba810a4da fix unit tests 2026-04-15 17:49:33 +05:30
ArnabChatterjee20k 6d9b787816 updated string replacement 2026-04-15 17:38:21 +05:30
ArnabChatterjee20k 7b8fb409b1 added database filtering 2026-04-15 17:33:57 +05:30
ArnabChatterjee20k 1fb78115e8 added backward compat 2026-04-15 17:23:18 +05:30
Chirag AggarwalandGitHub 49c93c635d Merge pull request #11851 from appwrite/chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-15 15:13:18 +05:30
Chirag AggarwalandGitHub 5010cdedbb Merge pull request #11903 from appwrite/fix/ci-mongodb-retry 2026-04-15 14:33:15 +05:30
Chirag AggarwalandGitHub 13413c05c3 Merge pull request #11880 from appwrite/fix/graphql-coroutine-safe-response 2026-04-15 14:14:57 +05:30
Chirag Aggarwal dc185be836 collapse 2026-04-15 13:52:20 +05:30
Chirag AggarwalandGitHub 29be9b6019 Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-15 13:26:52 +05:30
ArnabChatterjee20kandGitHub 1247b1c719 Merge pull request #11852 from appwrite/docsdb-fixes
Docsdb fixes
2026-04-15 13:18:41 +05:30
Chirag AggarwalandGitHub 445e86361c Merge pull request #11904 from appwrite/fix/sdk-push-no-force 2026-04-15 10:27:40 +05:30
Chirag Aggarwal 80197b566c fix: replace force-push with regular push in SDK release task
The SDK push task used `git push --force-with-lease` which fails on
repos with branch protection rules that disallow force pushes. Instead,
checkout the existing remote dev branch and commit on top of it so a
regular push is always a fast-forward.
2026-04-15 10:22:50 +05:30
Chirag Aggarwal 8671533878 fix: remove orphaned docblock from deleted test 2026-04-15 10:13:37 +05:30
Chirag Aggarwal f51f02375a test: remove flaky concurrent session race condition test
testEmailPasswordSessionNotCorruptedByConcurrentRequests relies on
timing-sensitive curl_multi orchestration with hardcoded delays to
reproduce a cache race window. This makes it inherently flaky in CI
where resource pressure shifts the timing unpredictably.
2026-04-15 09:52:32 +05:30
Chirag Aggarwal d40df613de fix: run ProjectWebhooks tests sequentially in CI
ProjectWebhooks tests have shared state dependencies (e.g. index
creation must complete before assertions). Running with --functional
(parallel methods) causes flaky failures where indexes are still
'processing' instead of 'available'.
2026-04-15 09:51:26 +05:30
ArnabChatterjee20kandGitHub ebc3febc38 Merge branch '1.9.x' into docsdb-fixes 2026-04-15 09:42:05 +05:30
Chirag Aggarwal e77bfae091 fix: add restart policy to MongoDB container for flaky CI starts
MongoDB's official Docker entrypoint uses a two-phase startup: a
temporary mongod for user/db init, then the real mongod. Under CI
resource pressure the port may not be released between the two
phases, causing mongod to exit with code 48 (address already in use).

Adding restart: on-failure:3 lets Docker handle the transient failure
natively. On restart the data directory already exists so the
entrypoint skips the two-phase init entirely, avoiding the race.
2026-04-15 09:19:02 +05:30
Chirag Aggarwal f9efd803c1 refactor: remove unnecessary IIFE wrappers in resolver methods 2026-04-15 08:55:00 +05:30
Chirag Aggarwal 54d6470163 refactor: move acquire/release into ResolverLock as instance methods 2026-04-15 08:51:14 +05:30
Chirag Aggarwal 50640b5bb9 refactor: inline createResolverResponse — only called once 2026-04-15 08:50:14 +05:30
Chirag Aggarwal 4d4ba508ef fix: use spl_object_hash for lock keys instead of WeakMap
Replace WeakMap with a plain array keyed by spl_object_hash($utopia)
as suggested in review. Entry is cleaned up in the finally block to
prevent leaks.
2026-04-15 08:47:41 +05:30
Chirag Aggarwal 4f2f9fedfa fix: resolve merge conflict keeping both lock and route restore
Merge conflict in Resolvers.php between the coroutine lock
(fix/graphql-coroutine-safe-response) and the otel route restore
(fix-gql-route-reset from 1.9.x). Both changes are needed:
the lock serialises concurrent resolvers while the route restore
prevents otel span clobbering.
2026-04-15 08:40:12 +05:30
Chirag AggarwalandGitHub 8fcba8b5c9 Merge pull request #11893 from appwrite/codex/disable-graphql-functional 2026-04-14 19:19:14 +05:30
Jake BarnbyandGitHub 6c8b34c230 Merge pull request #11885 from appwrite/fix-gql-route-reset
(fix): reset route to avoid clobbering otel
2026-04-15 01:26:02 +12:00
Chirag Aggarwal b2884ddb88 Use audit message context helper 2026-04-14 18:23:24 +05:30
Chirag Aggarwal 82798fa5a3 Simplify audit message construction 2026-04-14 18:18:25 +05:30
Shimon NewmanandGitHub 377ade3c44 Merge pull request #11892 from appwrite/schedule-functions-debug-2
Schedule functions debug
2026-04-14 15:32:29 +03:00
Shimon NewmanGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
71533aaaf3 Update app/cli.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-14 15:17:37 +03:00
shimon ccbe7e5d3f Merge branch '1.9.x' of github.com:appwrite/appwrite into schedule-functions-debug-2 2026-04-14 15:11:56 +03:00
Chirag AggarwalandGitHub bea7739d7e Merge branch '1.9.x' into codex/disable-graphql-functional 2026-04-14 17:21:55 +05:30
Jake BarnbyandGitHub 76320d652a Merge pull request #11887 from appwrite/fix/composer-audit-graphql-php
Upgrade graphql-php to v15.31.5
2026-04-14 23:46:45 +12:00
Chirag Aggarwal 6d5968e2ea ci: disable functional GraphQL e2e 2026-04-14 16:51:46 +05:30
shimon 70c380fa36 feat: add tracing support for execution events in log and worker classes 2026-04-14 13:36:29 +03:00
shimon 512a7ae2bd feat: enhance function scheduling with tracing support 2026-04-14 13:36:19 +03:00
Chirag AggarwalandGitHub 49d3cd980f Merge branch '1.9.x' into fix/composer-audit-graphql-php 2026-04-14 15:07:22 +05:30
Luke B. SilverandGitHub 39a396810a Merge pull request #11881 from appwrite/feat/45-min-build-timeout
feat: increase default build timeout to 45 minutes
2026-04-14 09:28:29 +01:00
Jake BarnbyandGitHub 1d307178bb Merge pull request #11691 from appwrite/delete-project-shared-table-v1
Safe delete shared tables v1
2026-04-14 18:00:00 +12:00
fogelito c3ed2ff6ce Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-14 08:05:44 +03:00
Chirag Aggarwal efadf17bfe Fix GraphQL 15 static analysis 2026-04-14 10:26:59 +05:30
Chirag Aggarwal bcfec8d5de Align graphql version pinning style 2026-04-14 09:35:25 +05:30
Chirag Aggarwal 4b2e22d9da Fix graphql-php audit vulnerability 2026-04-14 09:27:14 +05:30
ArnabChatterjee20k 45e22e2243 Merge remote-tracking branch 'origin/1.9.x' into docsdb-fixes 2026-04-13 22:44:07 +05:30
Matej BačoandGitHub 0a21f3b139 Merge pull request #11884 from appwrite/fix-webhook-api-security
Fix: Webhook API secret rotation and security
2026-04-13 15:52:55 +02:00
Chirag AggarwalandGitHub 5d53685976 Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-13 19:11:43 +05:30
Chirag AggarwalandGitHub d869c39783 Merge branch '1.9.x' into fix/graphql-coroutine-safe-response 2026-04-13 19:02:06 +05:30
Chirag Aggarwal fe02964ebd fix: finalize graphql coroutine response isolation 2026-04-13 19:01:20 +05:30
Jake Barnby cc8eb62c83 (chore): rename 2026-04-14 01:15:06 +12:00
Luke B. SilverandGitHub b1ad6ea87c Merge pull request #11883 from appwrite/fix/large-execution-payload
fix: trim execution queue payload to project ID only
2026-04-13 14:13:43 +01:00
Jake Barnby d52d6c0bf0 (fix): reset route to avoid clobbering otel 2026-04-14 01:13:32 +12:00
Chirag AggarwalandGitHub 86cfea0edb Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-13 18:41:52 +05:30
Matej Bačo db406b0a27 Fix tests 2026-04-13 15:08:20 +02:00
Matej Bačo 2585518e33 Fix bugs 2026-04-13 15:08:13 +02:00
Chirag Aggarwal a1342b4b9d fix: update audit context usage 2026-04-13 18:32:38 +05:30
Matej Bačo c9fceb870c Fix folder structure 2026-04-13 14:57:24 +02:00
Matej Bačo 9f1ec356d1 Formatting fix 2026-04-13 14:53:25 +02:00
Matej Bačo 28d285d5c5 Improved tests for webhook edge cases 2026-04-13 14:53:06 +02:00
loks0nandClaude Sonnet 4.6 a941d8b855 fix: trim execution queue payload to project ID only
The v1-executions queue was serialising the full project document
(OAuth providers, webhooks, keys, auths config, permissions, etc.)
into every message. The worker DI system already re-fetches the
complete project from the platform database using only the project ID,
so the full document was wasted bytes.

Override trimPayload() in the Execution event class to include only
$id in the project stub, reducing message size significantly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:52:09 +01:00
Chirag Aggarwal d40a355d9d refactor: simplify audit event context 2026-04-13 18:21:39 +05:30
Matej Bačo 3263133e5f Implement secure webhook interfaces 2026-04-13 14:50:06 +02:00
Chirag Aggarwal f4f5494b85 fix: isolate graphql resolver responses 2026-04-13 17:48:48 +05:30
Chirag Aggarwal fc0fd2f6ac fix: scope graphql resources to resolver coroutine 2026-04-13 17:10:38 +05:30
Chirag Aggarwal 70a75c2e7b fix: scope graphql resolver lock to request 2026-04-13 16:47:33 +05:30
Chirag Aggarwal 6bc2168e29 fix: isolate graphql resolver request state 2026-04-13 16:34:27 +05:30
fogelito a3ba66c90a Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1
# Conflicts:
#	composer.lock
2026-04-13 13:29:03 +03:00
ArnabChatterjee20kandGitHub c24d724000 Merge pull request #11749 from appwrite/db-workers-memory
added reset in db worker for queue for realtime
2026-04-13 15:47:54 +05:30
Luke B. SilverGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
df5ccc10ad Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-13 10:48:20 +01:00
loks0nandClaude Sonnet 4.6 2807d6cd9a feat: increase default build timeout to 45 minutes
Raises _APP_COMPUTE_BUILD_TIMEOUT default from 900s (15 min) to
2700s (45 min) to support longer-running builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 09:57:27 +01:00
ArnabChatterjee20kandGitHub 20e2f2284f Merge branch '1.9.x' into db-workers-memory 2026-04-13 13:43:14 +05:30
Matej BačoandGitHub 7a0d69c826 Merge pull request #11839 from appwrite/feat-services-protocols-apis
Feat: services protocols public apis
2026-04-13 09:38:17 +02:00
Matej Bačo feedec80f2 Merge branch '1.9.x' into feat-services-protocols-apis 2026-04-13 09:17:09 +02:00
Damodar LohaniandClaude Opus 4.6 18b1c344e6 fix: serialize batched GraphQL queries for coroutine safety
When Swoole coroutine hooks are enabled (SWOOLE_HOOK_ALL), batched
GraphQL queries execute in parallel coroutines that share a single
Response object. Concurrent coroutines interleave writes to the
shared response payload, causing data mixing between queries.

Cloning the response is not viable because cookies/headers written
by the action (e.g. session tokens) must reach the real HTTP response.

Instead, serialize the critical section (execute → getPayload) using
a Swoole Channel as a coroutine-safe mutex. This ensures only one
batched query writes to the Response at a time while preserving
cookie/header propagation. The lock is released before resolve/reject
callbacks so downstream processing remains concurrent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 06:54:15 +00:00
Damodar LohaniandGitHub f4d40a1289 Merge pull request #11879 from appwrite/fix/graphql-batch-sent-reset
fix: reset response sent state between batched GraphQL queries
2026-04-13 11:35:22 +05:45
Damodar LohaniandGitHub 1e65f075e6 Merge branch '1.9.x' into fix/graphql-batch-sent-reset 2026-04-13 11:23:23 +05:45
Jake BarnbyandGitHub 68725d9262 Merge pull request #11860 from appwrite/fix-cache-fallback
(fix): cache fallback
2026-04-13 17:34:51 +12:00
Damodar LohaniandClaude Opus 4.6 5b805d686b fix: reset response sent state between batched GraphQL queries
utopia-php/http 0.34.20 added a guard that skips the action if
$response->isSent() is true. In batched GraphQL requests the resolver
reuses a single Response across all queries — after the first query's
action calls send(), subsequent queries hit the guard, their actions
are skipped, and stale/null payloads are returned.

Add Response::clearSent() to the Appwrite Response subclass (which can
access the protected $sent property from the parent) and call it in
Resolvers::resolve() before each execute(). This ensures each batched
query gets a fresh sent state while keeping the guard active for normal
request paths.

Also bumps utopia-php/http from 0.34.19 to 0.34.20 so CE CI tests
against the same version used by downstream consumers (cloud).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 05:32:04 +00:00
Chirag AggarwalandGitHub 584acafb1d Merge branch '1.9.x' into feat-services-protocols-apis 2026-04-13 10:45:42 +05:30
Chirag AggarwalandGitHub dce7856b77 Merge pull request #11848 from appwrite/fix/spec-generator-console-pr82 2026-04-13 10:40:47 +05:30
Chirag Aggarwal a6af609317 Remove scopes spec override, now fixed at source in #11839 2026-04-13 10:33:46 +05:30
Chirag Aggarwal 035f6244e1 Revert "fix: require scopes for project keys"
This reverts commit 8deafcaf4d52a59cc2e1b27c7a128e8b7843afa4.
2026-04-13 10:33:46 +05:30
Chirag Aggarwal 723cb1a488 fix: require scopes for project keys 2026-04-13 10:33:46 +05:30
Chirag Aggarwal 815209ebb0 fix: address sdk spec review feedback 2026-04-13 10:33:46 +05:30
Chirag Aggarwal 53c74582fc refactor: simplify request parameter spec overrides 2026-04-13 10:33:46 +05:30
Chirag Aggarwal 78bbe77580 fix: align project sdk spec generation 2026-04-13 10:33:45 +05:30
0c3871a681 fix: pass response to Http::execute() in GraphQL resolver (#11876)
Http::execute() now requires a Response parameter as of utopia-php/http
0.34.20. The GraphQL resolver was only passing route and request,
causing all GraphQL queries to fail with "Internal server error".

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:33:32 +12:00
Chirag AggarwalandGitHub b59eba4ec6 Merge pull request #11874 from appwrite/fix/11763-installer-compose-executor 2026-04-13 08:52:39 +05:30
Chirag Aggarwal cb4c97f2ee chore: remove installer compose regression test 2026-04-12 14:11:51 +05:30
Chirag Aggarwal 2ee2ea09a0 fix(installer): sync compose template executor image 2026-04-12 13:56:49 +05:30
Chirag AggarwalandGitHub a31cb67ec5 Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-11 22:56:10 +05:30
Chirag AggarwalandGitHub 4ec84a147f Merge pull request #11861 from appwrite/fix-edge-pzj-rule-deployment-resource-type-optional
Make rule deploymentResourceType optional for non-deployment rules
2026-04-11 22:08:17 +05:30
Chirag AggarwalandGitHub e583de4650 Merge pull request #11858 from appwrite/fix-cve-2026-40194-phpseclib-bump
Bump phpseclib to 3.0.51 for CVE-2026-40194
2026-04-11 22:07:47 +05:30
Chirag Aggarwal 98af2a5eb3 fix: make rule deploymentResourceType optional 2026-04-11 22:05:01 +05:30
Jake BarnbyandClaude Opus 4.6 e3ad0f85de fix: narrow cache try-catch to avoid swallowing query exceptions
Wrap only cache load/save calls in try-catch instead of the entire
cache block. This prevents OrderException, QueryException, and Timeout
from $find() being caught and retried, which would double DB calls and
hide real query errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 03:42:47 +12:00
Jake Barnby a26382ac51 (chore): lockfile 2026-04-12 03:25:07 +12:00
Jake Barnby 9ba182d8a0 (fix): cache fallback 2026-04-12 03:22:21 +12:00
Matej Bačo 27fc8058b9 Fix failing tests 2026-04-11 14:19:05 +02:00
Matej Bačo c5bd8c712f Upgrade libs 2026-04-11 11:31:35 +02:00
Matej Bačo a1267b1bff Backwards compatibiltiy tests 2026-04-11 11:16:43 +02:00
Matej Bačo 18d17ea945 Webhook endpoints quality improvements 2026-04-11 11:00:56 +02:00
Matej Bačo fabd9559c4 Tests for backwards compatibility 2026-04-11 10:22:03 +02:00
Matej Bačo ec637d4417 Mark key scopes required 2026-04-11 10:19:14 +02:00
Chirag Aggarwal 5ecd15a5f5 fix: register certificate publisher in cli 2026-04-11 09:07:51 +05:30
Chirag Aggarwal 4523e86b91 fix: bump phpseclib to 3.0.51 2026-04-11 09:01:42 +05:30
Chirag Aggarwal ec5472f1ed chore: remove unrelated queue resources 2026-04-11 08:57:06 +05:30
Chirag AggarwalandGitHub 9ae804f8ae Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-11 08:49:23 +05:30
Luke B. SilverandGitHub 1ea108c2ce Merge pull request #11854 from appwrite/feat/remove-err
feat: remove error logs
2026-04-10 14:18:33 +01:00
loks0n 0a864e51b8 feat: remove error logs 2026-04-10 14:17:24 +01:00
ArnabChatterjee20k 0cce480592 typo 2026-04-10 17:23:43 +05:30
ArnabChatterjee20k b622b092a8 updated 2026-04-10 17:22:22 +05:30
ArnabChatterjee20k 96fe989f6d update composer dependencies and remove obsolete log classes 2026-04-10 17:15:31 +05:30
Chirag Aggarwal 0de26be6e6 chore: address review feedback 2026-04-10 16:44:24 +05:30
Chirag Aggarwal dc0a5c88b7 refactor: migrate audits certificates screenshots to publishers 2026-04-10 16:44:00 +05:30
Chirag AggarwalandGitHub c6dd7de216 Merge pull request #11850 from appwrite/chore-migrate-selected-queues-to-publishers
Migrate executions, migrations, and stats resources to publishers
2026-04-10 15:13:15 +05:30
Chirag Aggarwal f77a64bff9 chore: address publisher PR nits 2026-04-10 14:00:57 +05:30
ArnabChatterjee20k d13dbae0fe added allowance of empty payload for documentsdb 2026-04-10 13:59:45 +05:30
Luke B. SilverandGitHub f552a1ba15 Merge pull request #11844 from appwrite/feat/storage-cache-spans
feat: add tracing spans for storage file preview timing and cache state
2026-04-10 09:18:42 +01:00
Chirag Aggarwal 7282c5d51f chore: remove unused execution exclusion 2026-04-10 13:25:32 +05:30
Chirag Aggarwal 82ec75d582 chore: address PR review feedback 2026-04-10 13:12:08 +05:30
Chirag Aggarwal 6bf6142667 refactor: migrate selected queues to publishers 2026-04-10 13:02:00 +05:30
Chirag AggarwalandGitHub 938e65cb02 Merge pull request #11831 from appwrite/codex/remove-realtime-http-dependency
Use dedicated connection resources in realtime
2026-04-10 12:54:45 +05:30
ArnabChatterjee20kandGitHub 114de91f48 Merge pull request #11767 from appwrite/realtime-query-message-payload
Realtime query message payload
2026-04-10 12:06:00 +05:30
ArnabChatterjee20k 2e6f3f5c14 typo 2026-04-10 11:13:03 +05:30
ArnabChatterjee20k 7b3d9bae03 updated authorization 2026-04-10 11:04:44 +05:30
Chirag Aggarwal d81a1154e3 refactor: isolate realtime connection resources 2026-04-10 10:19:41 +05:30
Chirag Aggarwal a944c65660 refactor: move worker message resources 2026-04-10 09:43:32 +05:30
Chirag Aggarwal 856046dc82 shrink the size 2026-04-10 09:28:17 +05:30
Chirag Aggarwal 2ca551123d use connection container 2026-04-10 09:25:00 +05:30
Chirag Aggarwal c861d45749 Merge branch '1.9.x' into codex/remove-realtime-http-dependency 2026-04-10 09:02:23 +05:30
loks0nandClaude Sonnet 4.6 4a43969da9 fix: use consistent dot notation for all storage span attribute names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:53:20 +01:00
loks0nandClaude Sonnet 4.6 6fa4122910 fix: rename storage span attributes to use dot notation for ids
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:52:25 +01:00
loks0nandClaude Sonnet 4.6 1d27101770 feat: add tracing spans for storage file preview timing and cache state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:49:57 +01:00
Luke B. SilverandGitHub 9214decc8d Merge pull request #11843 from appwrite/fix/session-mails
fix: set project on mail queue in session mails listener
2026-04-09 18:30:24 +01:00
loks0nandClaude Sonnet 4.6 ee4ae3bd47 fix: set project on mail queue in session mails listener
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 18:26:07 +01:00
Luke B. SilverandGitHub 53a114e7e4 Merge pull request #11842 from appwrite/fix/storage-cache
fix: storage preview cache misses and stale cache eviction
2026-04-09 17:27:27 +01:00
loks0nandClaude Sonnet 4.6 f2df9cb93a fix: storage preview cache misses and stale cache eviction
Three bugs causing storage preview cache to be ineffective:

1. Cache keys included the `token` auth parameter, so requests using
   resource tokens always generated unique keys and never hit cache.
   Introduced `cache.params` label for routes to opt-in specific params
   into the cache key; preview now declares only the transform params.

2. Cache hits never refreshed `accessedAt` in the DB or the filesystem
   file mtime, because `$response->send()` in the init hook skips the
   shutdown hook. After 30 days the maintenance job evicted still-active
   cache entries, and after the original 30-day filesystem TTL the cache
   file expired — causing periodic full re-renders. The cache-hit path
   now updates both on the APP_CACHE_UPDATE (24h) interval.

3. `updateDocument` in the preview action passed the full file document
   instead of a sparse one when updating `transformedAt`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 17:05:14 +01:00
Matej Bačo d69726487e PR review fixes 2026-04-09 16:58:42 +02:00
Matej Bačo 5fccb8cc28 Improve tests 2026-04-09 16:57:44 +02:00
Luke B. SilverandGitHub f371237fd5 Merge pull request #11642 from appwrite/feat/mails-listener
feat: extract session alert email into Mails listener
2026-04-09 15:29:16 +01:00
Matej Bačo 21a0d60c98 Fix tests 2026-04-09 16:13:54 +02:00
Matej Bačo 4eb8534294 Fix tests 2026-04-09 16:08:11 +02:00
Matej Bačo c95f905bce New services and protocols tests 2026-04-09 15:58:28 +02:00
Matej Bačo 0293da1e22 Improve test for backwards compatibility 2026-04-09 15:54:00 +02:00
Matej Bačo a4a0c4175d Implement new endpoints in /v1/project for services and protocols 2026-04-09 15:45:06 +02:00
Harsh MahajanandGitHub 9548d18a3e Merge pull request #11838 from appwrite/ix-missing-worker-executions-template
fix(installer): add missing worker-executions service to compose template
2026-04-09 19:06:42 +05:30
Matej Bačo d3c73fbb49 Add endpoints to control protocols and services 2026-04-09 15:34:50 +02:00
Harsh MahajanandGitHub 7928175387 Merge branch '1.9.x' into ix-missing-worker-executions-template 2026-04-09 18:53:39 +05:30
Matej Bačo 8818187740 Introduce req&res filters for 1.9.1 2026-04-09 15:21:58 +02:00
Matej Bačo 75324b24fc Improve skill 2026-04-09 15:21:24 +02:00
Matej Bačo d6d118f4ab Bump version to 1.9.1 2026-04-09 15:19:58 +02:00
Matej Bačo e998739998 Add agent skill to increase patch version 2026-04-09 15:18:26 +02:00
loks0nandClaude Sonnet 4.6 dd29967e99 refactor: tighten Mails listener with guard clauses and lean event
- SessionCreated event now carries only domain data (no isFirstSession)
- Mails listener uses ordered guard clauses, deferring the DB query
  until cheaper checks pass
- Drop $user Document allocation in favour of direct array access
- Inline FileName validator and $smtpEnabled into their use sites
- Extract $isBranded to eliminate duplicate APP_BRANDED_EMAIL_BASE_TEMPLATE check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:01:13 +01:00
loks0nandClaude Sonnet 4.6 e7f5ae9306 fix: remove stale @param platform from SessionCreated docblock
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:01:13 +01:00
loks0nandClaude Sonnet 4.6 4133ec99ae feat: extract session alert email into Mails listener
Moves session alert email side effect out of the account controller
into a dedicated `Mails` listener that reacts to a new `SessionCreated`
bus event. The event is now always dispatched on session creation; the
listener owns all conditional logic (first session, sessionAlerts flag,
email-link sessions, user email presence).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:01:13 +01:00
Matej BačoandGitHub 6dd63ee152 Merge pull request #11655 from appwrite/copilot/add-test-for-deleting-partially-uploaded-file
Fix deletion of partially-uploaded (pending) files
2026-04-09 14:58:57 +02:00
ArnabChatterjee20k 912dbda159 updated type 2026-04-09 18:16:09 +05:30
ArnabChatterjee20k 410a050244 updated 2026-04-09 18:04:01 +05:30
ArnabChatterjee20k 9cf45816c2 added triggering stats for messaging based subscription during the start 2026-04-09 17:38:25 +05:30
Harsh MahajanandGitHub 386fc995e6 Update compose.phtml 2026-04-09 17:36:07 +05:30
Matej Bačo d6451b8fad Merge branch '1.9.x' into copilot/add-test-for-deleting-partially-uploaded-file 2026-04-09 14:02:06 +02:00
ArnabChatterjee20kandGitHub 920ddd18e6 Merge branch '1.9.x' into realtime-query-message-payload 2026-04-09 17:24:57 +05:30
Harsh MahajanGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
ff9334ab78 Update app/views/install/compose.phtml
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-09 17:20:55 +05:30
Harsh MahajanandGitHub c494e96f79 Merge branch '1.9.x' into ix-missing-worker-executions-template 2026-04-09 17:18:22 +05:30
harsh mahajan fd78f0f7df fix(installer): add missing worker-executions service to compose template 2026-04-09 17:13:54 +05:30
Chirag AggarwalandGitHub 4ff10dcacc Merge pull request #11837 from appwrite/fix-11795-revert-project-variableid
Revert "fix: make variableId optional in createProjectVariable endpoint"
2026-04-09 16:53:10 +05:30
Chirag Aggarwal 7a995fe759 Revert "Merge pull request #11795 from rathi-yash/fix-11765-global-variable-creation"
This reverts commit 597b20a6cb, reversing
changes made to 20f80ac067.
2026-04-09 16:25:59 +05:30
Damodar LohaniandGitHub 227606baef Merge pull request #11835 from appwrite/fix/map-deprecated-platform-types-in-project-response
fix: map deprecated platform types in project response
2026-04-09 14:23:31 +05:45
Damodar LohaniandClaude Opus 4.6 f315e759f3 fix: map deprecated platform types in subQueryPlatforms filter
The subQueryPlatforms database filter loads platforms as a sub-attribute
when project documents are fetched. Old platform type values stored in
the database (e.g. flutter-android, flutter-ios) were not being mapped
to the new consolidated types before being included in the project
response sent to the frontend/console.

This adds Platform::mapDeprecatedType() to the filter so all platforms
returned as part of a project document have their types mapped
consistently, complementing the existing mapping in the dedicated
platform Get and List endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 08:31:39 +00:00
Damodar LohaniandGitHub 8a6fba75a5 Merge pull request #11833 from appwrite/fix/deprecated-platform-origin-validation
fix: Add backwards-compat origin validation for deprecated platform types
2026-04-09 13:41:30 +05:45
Damodar LohaniandClaude Opus 4.6 196c76ac70 fix: map deprecated platform types in read endpoints for backwards compatibility
Old platform types stored in the database (e.g. flutter-web, apple-ios,
react-native-android) are now mapped to the new consolidated types (web,
apple, android, windows, linux) before being sent in API responses. This
ensures the response models' $conditions correctly select the right model
for each platform document.

Adds Platform::mapDeprecatedType() as a reusable static method and applies
the mapping in both Get and XList platform endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 07:47:42 +00:00
Damodar LohaniandClaude Opus 4.6 7f82484436 remove migration changes from V24.php
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 07:36:58 +00:00
Damodar LohaniandClaude Opus 4.6 db18caf739 fix: add backwards-compatible origin validation for deprecated platform types
Platform::getHostnames() and Platform::getSchemes() only handled the new
consolidated type names, causing "invalid origin" errors for projects still
using old granular types (flutter-*, apple-*, react-native-*, unity) stored
in the database. Add switch fall-through cases for all deprecated type values
and a V24 migration to convert old types to their new equivalents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 07:31:58 +00:00
Chirag AggarwalandGitHub 6693e28c93 Merge pull request #11830 from appwrite/feat/specs-task-no-http
remove HTTP server bootstrap from specs task
2026-04-09 11:46:37 +05:30
Chirag Aggarwal bf489ce13b Fix requestRoutePath fallback 2026-04-09 11:09:21 +05:30
Chirag Aggarwal 357d6482f9 Remove realtime HTTP app dependency 2026-04-09 10:52:31 +05:30
Chirag Aggarwal eec72a915a inline specs container setup 2026-04-09 10:45:24 +05:30
Chirag Aggarwal ae29e4c424 drop specs task format test 2026-04-09 10:43:39 +05:30
Chirag Aggarwal 05dc264df9 remove specs task HTTP bootstrap 2026-04-09 10:41:04 +05:30
Chirag AggarwalandGitHub 597b20a6cb Merge pull request #11795 from rathi-yash/fix-11765-global-variable-creation
fix: make variableId optional in createProjectVariable endpoint
2026-04-09 09:34:39 +05:30
Chirag AggarwalandGitHub 6b7d9853f9 Merge branch '1.9.x' into fix-11765-global-variable-creation 2026-04-09 09:24:52 +05:30
Damodar LohaniandGitHub 20f80ac067 Merge pull request #11580 from appwrite/feat-audit-user-type-distinction
feat: distinguish user types in audit logs
2026-04-09 06:55:43 +05:45
My Name 8bfa659120 test: add test case for createProjectVariable without variableId 2026-04-08 15:18:13 -04:00
Yash RathiandGitHub 65e44d76a1 Merge branch '1.9.x' into fix-11765-global-variable-creation 2026-04-08 14:50:58 -04:00
Jake BarnbyandGitHub bb07808661 Merge pull request #11820 from appwrite/fix-ttl-message 2026-04-09 03:39:48 +12:00
Luke B. SilverandGitHub d18550ff54 Merge pull request #11826 from appwrite/fix/cookie-test-hack
fix: use cURL cookie engine for RFC 6265 compliant cookie handling in e2e client
2026-04-08 16:17:04 +01:00
loks0nandClaude Sonnet 4.6 84dc921d41 fix: replace utopia-php/framework with http, fix RFC 6265 cookie handling
utopia-php/framework was the old name for utopia-php/http. Replacing it
with utopia-php/http 0.34.19 which fixes getCookie() to use Swoole's
native cookie store (populated via php_raw_url_decode) instead of
re-parsing the raw Cookie header without URL-decoding.

This fixes a production auth bug where Swoole's setcookie() URL-encodes
base64 session values (+ → %2B, / → %2F, = → %3D) in Set-Cookie headers.
RFC 6265 clients (Dart, Swift) reflect these verbatim; the old getCookie()
returned %2B/%2F/%3D to base64_decode() which produced corrupted output,
rejecting valid sessions.

Also updates the e2e test client to use cURL's built-in RFC 6265 cookie
engine (CURLOPT_COOKIEFILE) instead of parse_str() which silently
URL-decoded values, masking the bug in tests. Adds a cookie roundtrip
assertion to testCreateAccountSession.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 15:07:46 +01:00
loks0nandClaude Sonnet 4.6 e2d7dd837d fix: use cURL cookie engine instead of parse_str for RFC 6265 compliance
parse_str() URL-decodes cookie values, causing the test client to behave
differently from real clients (Dart, Swift) which store values verbatim
per RFC 6265. This masked a production bug where base64 session values
containing %3D%3D would fail to decode on real devices.

Replaces the manual Set-Cookie header parsing with cURL's built-in cookie
engine (CURLOPT_COOKIEFILE='') and reads cookies via CURLINFO_COOKIELIST,
which stores and returns values verbatim without any decoding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 15:07:46 +01:00
Jake BarnbyandGitHub 9feb204fd5 Merge pull request #11611 from appwrite/feat-x-oauth2-provider 2026-04-09 01:32:31 +12:00
Jake Barnby 3880f181b3 fix(databases): propagate purge parameter to documentsdb updateCollection 2026-04-09 01:19:18 +12:00
Harsh Mahajan 44a37e9e20 Use Exception for X OAuth2 PKCE encryption errors
Align with other OAuth2 adapters that throw base Exception for
configuration and crypto failures instead of RuntimeException.

Made-with: Cursor
2026-04-08 18:41:42 +05:30
Harsh Mahajan e6cfedd340 addressed greptile comment 2026-04-08 18:27:36 +05:30
Harsh Mahajan e4d1178e71 simplified code 2026-04-08 17:56:37 +05:30
Harsh Mahajan 929bdcef25 Merge branch '1.9.x' into feat-x-oauth2-provider 2026-04-08 17:55:00 +05:30
Harsh Mahajan 3f725c6be9 changes 2026-04-08 17:44:49 +05:30
Jake BarnbyGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
990a32dd9e Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-08 23:31:55 +12:00
Jake Barnby c5b8ed9cc1 feat(databases): cache list responses without requiring a select query 2026-04-08 23:02:34 +12:00
Matej BačoandGitHub fec573d23b Merge pull request #11821 from appwrite/chore-public-project-api
Chore: Mark project response format sub-formats as public
2026-04-08 12:32:58 +02:00
Matej Bačo a144968d70 Fix formatting 2026-04-08 12:08:32 +02:00
Matej Bačo 6fa0724404 Mark project response format sub-formats as public 2026-04-08 11:52:12 +02:00
Jake Barnby b1ce71e6b0 (chore): fmt 2026-04-08 21:35:49 +12:00
Jake Barnby 939092726c test(databases): add regression for purge=true list cache invalidation 2026-04-08 21:31:42 +12:00
Jake Barnby c6f8599c75 feat(databases): add purge parameter to updateCollection and updateTable 2026-04-08 21:31:38 +12:00
Jake Barnby 91d8519940 refactor(databases): restructure list response cache and clarify ttl description 2026-04-08 21:31:34 +12:00
Matej BačoandGitHub a90f79f1c1 Merge pull request #11650 from appwrite/feat-public-project-keys
Feat: Public keys API
2026-04-08 11:15:04 +02:00
Matej Bačo f880b6e8c3 Fix failing tests 2026-04-08 10:52:20 +02:00
Matej Bačo b8d65326e6 Fix failing tests 2026-04-08 10:34:18 +02:00
Matej Bačo a9fd82e406 New tests 2026-04-08 10:32:20 +02:00
Matej Bačo a8c2491fbb Fix platform scopes 2026-04-08 10:17:48 +02:00
Jake BarnbyandGitHub 7f6486ec80 Merge pull request #11762 from bhardwajparth51/fix-10923-realtime-atomic-payload 2026-04-08 20:16:55 +12:00
Matej Bačo 388cec1737 Merge branch '1.9.x' into feat-public-project-keys 2026-04-08 10:16:22 +02:00
Parth BhardwajandGitHub 2f5a49a37d Merge branch '1.9.x' into fix-10923-realtime-atomic-payload 2026-04-08 13:39:54 +05:30
Matej BačoandGitHub 1f93184c42 Merge pull request #11615 from appwrite/feat-public-platform-api
Feat: public platform API
2026-04-08 10:08:10 +02:00
Jake BarnbyandGitHub 7d9cf48ca3 Merge pull request #11750 from appwrite/bump-database-version2 2026-04-08 20:04:25 +12:00
Matej Bačo eef2a7abdf Fix scopes 2026-04-08 10:01:52 +02:00
Matej Bačo c7a022ba43 Simplify after discussions 2026-04-08 09:54:57 +02:00
Matej Bačo 96a84a8fd7 Merge branch '1.9.x' into feat-public-project-keys 2026-04-08 09:45:15 +02:00
Matej Bačo cea242c66f Merge branch '1.9.x' into feat-public-platform-api 2026-04-08 09:29:54 +02:00
Matej Bačo ce4eb563b3 AI review fixes 2026-04-08 09:29:12 +02:00
Matej Bačo 2307d637fb Revert new patch version 2026-04-08 09:10:02 +02:00
premtsd-codeandGitHub 0aa72aafae Merge branch '1.9.x' into bump-database-version2 2026-04-08 11:32:22 +05:30
Chirag AggarwalandGitHub 6e1f0d57af Merge pull request #11817 from appwrite/fix/specs-unresolved-model
fix: throw RuntimeException for unresolved response models in spec generation
2026-04-08 11:08:30 +05:30
Chirag Aggarwal 62b6ef06e6 fix: add swoole extension to specs CI job 2026-04-08 10:49:50 +05:30
Chirag Aggarwal f5ab593261 fix: make Project model public for server SDK spec generation
The project.updateLabels route uses AuthType::KEY which makes it
available on the server platform, but the Project model had public=false
causing it to be filtered out during spec generation.
2026-04-08 10:47:37 +05:30
Chirag Aggarwal dd4a43b78c fix: throw RuntimeException for unresolved response models in spec generation
Spec generation silently produced a fatal error when a response model
string could not be resolved to a registered model object. Now throws a
clear RuntimeException in both Swagger2 and OpenAPI3 formats, for both
single and array model responses.

Also adds a CI job to run spec generation on every PR so unresolved
models are caught before merge.
2026-04-08 10:41:43 +05:30
premtsd-codeandGitHub c4c56283e5 Merge branch '1.9.x' into bump-database-version2 2026-04-08 10:33:58 +05:30
Chirag AggarwalandGitHub f71a98a527 Merge pull request #11816 from appwrite/fix/email-session-cache-purge
fix: persist session before purging user cache in email/password login
2026-04-08 10:25:19 +05:30
premtsd-codeandGitHub 2258668769 Merge branch '1.9.x' into bump-database-version2 2026-04-08 10:15:53 +05:30
Chirag Aggarwal 6dba407aed test: add E2E test for email/password session cache race condition
Adds testEmailPasswordSessionNotCorruptedByConcurrentRequests which
reproduces the cross-worker Redis cache race that caused 401s after
login. The test fires a login request, waits for it to reach the cache
purge point, then injects concurrent GET /v1/account requests that
re-cache a stale user document. Verifies the new session is immediately
usable.

Fails against the old ordering (purge before create), passes with the
fix (create before purge).
2026-04-08 10:10:16 +05:30
Luke B. SilverandGitHub 7eef845556 Merge pull request #11794 from blueberry-adii/doc-11793-fix-readme-auth-link
fix appwrite auth broken link in readme
2026-04-07 22:15:29 +01:00
loks0nandClaude Sonnet 4.6 7781d377ae fix: persist session before purging user cache in email/password login
Swap the order of createDocument('sessions') and purgeCachedDocument('users')
in the email/password session creation flow. Previously, the cache was purged
before the session was written, opening a race window in Swoole's async
environment where a concurrent account.get() could re-cache the user with no
sessions, causing sessionVerify to fail with a 401. This matches the correct
ordering already used by the token-based flows (magic URL, OTP, phone).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 21:44:24 +01:00
Chirag AggarwalandGitHub 59bf4663ca Merge pull request #11808 from appwrite/fix/sdk-dev-branch-sync
fix: reset SDK dev branch to base branch before pushing
2026-04-08 00:40:56 +05:30
premtsd-codeandGitHub 6653dbbb92 Merge branch '1.9.x' into bump-database-version2 2026-04-07 20:47:13 +05:30
Prem Palanisamy d7d20ccb29 Remove (int) cast from setTenant in getDatabasesDB same-pool branch 2026-04-07 15:35:20 +01:00
Yash RathiandGitHub 2d54986b09 Merge branch '1.9.x' into fix-11765-global-variable-creation 2026-04-07 10:25:08 -04:00
Matej Bačo 9ea0b2bc2f formatting fix 2026-04-07 15:56:54 +02:00
Matej Bačo 715727853b Fix unit test 2026-04-07 15:56:42 +02:00
Matej Bačo 43d4f709d5 Revert composer changes 2026-04-07 15:00:03 +02:00
Matej Bačo f40050fe6f Revert lockfile changes (failing tests) 2026-04-07 14:51:40 +02:00
Prem Palanisamy 308a534d98 Merge branch 'bump-database-version2' of https://github.com/appwrite/appwrite into bump-database-version2 2026-04-07 13:16:56 +01:00
Prem Palanisamy 35a72c4f08 Remove (int) cast from setTenant in separate-pool branches 2026-04-07 13:10:16 +01:00
Matej Bačo 34dfcba45c Linter fix 2026-04-07 14:08:16 +02:00
Chirag AggarwalandGitHub cb1e31bcd4 Merge pull request #11814 from appwrite/codex/bump-utopia-framework-0-34-18
[codex] Bump utopia-php/framework to 0.34.18
2026-04-07 17:38:06 +05:30
Matej Bačo 3e4e7fc0cd Merge branch '1.9.x' into feat-public-platform-api 2026-04-07 14:08:00 +02:00
ArnabChatterjee20kandGitHub da2444ede2 Merge branch '1.9.x' into realtime-query-message-payload 2026-04-07 17:36:29 +05:30
ArnabChatterjee20k bc224de751 Add userId to connection info in Realtime adapter and simplify userId fetching 2026-04-07 17:35:48 +05:30
Chirag Aggarwal 9403e4d65d Bump utopia-php/framework to 0.34.18 2026-04-07 17:35:32 +05:30
Matej BačoandGitHub 07a49835a6 Merge pull request #11812 from appwrite/chore-migrate-labels-api
Chore: Migrat elabels API
2026-04-07 14:05:31 +02:00
Matej Bačo d32de6f217 Improve tests 2026-04-07 13:30:35 +02:00
Matej Bačo 9b00ce4f1d Add new tests 2026-04-07 13:28:35 +02:00
ArnabChatterjee20k ca62504b5a Enhance realtime message handling to support initial connection payload and improve query subscription logic 2026-04-07 16:55:10 +05:30
Matej Bačo 8f6c8f9d8d Migrate to new endpoint 2026-04-07 13:21:36 +02:00
Matej Bačo 23fcb284a1 Fix backwards QA 2026-04-07 12:46:25 +02:00
premtsd-codeandGitHub ffdd0815a7 Merge branch '1.9.x' into bump-database-version2 2026-04-07 15:56:47 +05:30
Prem Palanisamy 4260324153 Merge branch '1.9.x' into bump-database-version2
Resolve merge conflicts in app/init/resources.php and app/worker.php
caused by the DI container migration (Http::setResource/Server::setResource
to $container->set). Port separate-pool shared tables logic for
getDatabasesDB to the new file locations (request.php and message.php)
with the correct $databaseDSN->getParam('namespace') fix.
2026-04-07 11:25:06 +01:00
Matej Bačo d66813d3cf Fix tests + QA fixes 2026-04-07 12:07:21 +02:00
Chirag AggarwalandGitHub 0ce92da41f Merge pull request #11810 from appwrite/codex/fix-post-merge-test-regressions
[codex] Fix post-merge e2e test regressions
2026-04-07 15:10:53 +05:30
Chirag Aggarwal e8ef4e40d7 fix post-merge e2e test regressions 2026-04-07 15:05:07 +05:30
Chirag AggarwalandGitHub d200b07466 Merge pull request #11809 from appwrite/codex/fix-console-null-route
[codex] Handle null console routes in API middleware
2026-04-07 14:42:08 +05:30
Chirag Aggarwal 92abfb31aa fix null route guard placement 2026-04-07 14:40:18 +05:30
Chirag Aggarwal 6c56eee0f4 test console route not found error type 2026-04-07 14:39:48 +05:30
Chirag Aggarwal 399c37d943 fix console null route handling 2026-04-07 14:33:43 +05:30
Matej Bačo f3acadd53c Fix linter 2026-04-07 10:43:57 +02:00
Matej Bačo 331fcee710 Merge branch '1.9.x' into feat-public-platform-api 2026-04-07 10:43:11 +02:00
Chirag AggarwalandGitHub 951a96ae01 Merge pull request #11564 from appwrite/feat/migrate-di-container
feat: migrate from static Http::setResource() to DI Container
2026-04-07 13:58:10 +05:30
Damodar LohaniandGitHub ed1680837d Merge branch '1.9.x' into feat-audit-user-type-distinction 2026-04-07 14:11:22 +05:45
Chirag Aggarwal 7864a5b9d1 fix: use --force-with-lease on SDK dev branch push
After resetting dev to the base branch, the remote dev may have
diverged history from squash merges. A plain push would be rejected
as non-fast-forward. Using --force-with-lease safely overwrites
the remote since we just fetched.
2026-04-07 12:52:09 +05:30
Chirag Aggarwal f11bd7ce0e fix: reset SDK dev branch to base branch before pushing
The dev branch was being reset to origin/dev, which retains stale
commits after squash merges. This caused recurring merge conflicts
and inflated PR diffs. Using `checkout -B dev <baseBranch>` ensures
dev always starts fresh from the default branch.
2026-04-07 12:50:18 +05:30
Parth BhardwajandGitHub fffc6795a7 Merge branch '1.9.x' into fix-10923-realtime-atomic-payload 2026-04-07 08:49:25 +05:30
Shimon NewmanandGitHub ed91549681 Merge pull request #11805 from appwrite/revert-schedule-functions-updates
revert ScheduleFunctions.php updates
2026-04-06 22:38:05 +03:00
shimon 04173600ae revert ScheduleFunctions.php updates 2026-04-06 22:33:18 +03:00
ArnabChatterjee20k d5fe5c34af Validate subscribe payload format in realtime message handling 2026-04-06 17:14:12 +05:30
ArnabChatterjee20k 6bc9adece8 Refactor realtime message handling to send subscriber keys and add comprehensive tests for subscription message upsert behavior 2026-04-06 17:10:57 +05:30
ArnabChatterjee20k 97d46c6273 Remove redundant subscription removal call in realtime message handling 2026-04-06 16:58:08 +05:30
ArnabChatterjee20k 9d78a8e6b6 Add stats tracking for outbound subscription messages in realtime 2026-04-06 16:57:06 +05:30
ArnabChatterjee20k d12a6f5168 Refactor realtime message payload handling for improved validation and parsing 2026-04-06 16:41:42 +05:30
ArnabChatterjee20k 0f47e6ea28 Enhance subscription message documentation for clarity on upsertion behavior 2026-04-06 15:59:22 +05:30
premtsd-codeandGitHub 77ceb15d7d Merge branch '1.9.x' into bump-database-version2 2026-04-06 15:44:40 +05:30
ArnabChatterjee20k 592629587d Remove unused query assertion methods and improve comment clarity in RealtimeQueryBase 2026-04-06 14:07:25 +05:30
ArnabChatterjee20k 187fde4a4e Refactor realtime subscription handling and enhance query validation in tests 2026-04-06 14:05:42 +05:30
Chirag Aggarwal b74d4d45f9 Merge request-scoped cookie resources 2026-04-06 13:21:33 +05:30
Chirag AggarwalandGitHub aa9a58b44d Merge pull request #11799 from appwrite/codex/request-scoped-cookie-domain
Use request-scoped cookie domain resource
2026-04-06 13:18:27 +05:30
Chirag Aggarwal 59a773e9a0 Document migration host local-domain handling 2026-04-06 12:47:06 +05:30
Chirag Aggarwal e3053bb83d Remove dead cookie config defaults 2026-04-06 12:44:48 +05:30
Chirag Aggarwal 1f7fc4bd40 Use request-scoped domain verification 2026-04-06 12:43:05 +05:30
Chirag Aggarwal d1b59ff3f3 Remove unused cookie domain locals 2026-04-06 12:30:48 +05:30
Chirag Aggarwal 221b52bac0 Add request-scoped cookie domain resource 2026-04-06 12:30:25 +05:30
Chirag Aggarwal b8ed30db55 Fix CORS header override for analyze 2026-04-06 12:23:50 +05:30
Chirag AggarwalandGitHub be56317bf2 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-06 12:13:31 +05:30
fogelito 31728c9b72 Update lock 2026-04-06 09:29:58 +03:00
fogelito 9d0fc9e5b1 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-06 09:19:38 +03:00
Chirag AggarwalandGitHub d1549b225b Merge branch '1.9.x' into feat/migrate-di-container 2026-04-06 08:35:58 +05:30
My Name 2cd91f509b fix: make variableId optional in createProjectVariable endpoint 2026-04-05 18:23:58 -04:00
Aditya a06ba57384 fix appwrite auth broken link in readme 2026-04-05 23:39:13 +05:30
Chirag AggarwalandGitHub dffcfe8ee4 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-05 21:16:30 +05:30
Chirag Aggarwal 452440f3c0 fix: use released cli container support 2026-04-05 21:03:17 +05:30
Chirag Aggarwal b236e2546b lock file 2026-04-05 20:10:00 +05:30
Chirag Aggarwal 412d09b801 remove unrelated changes 2026-04-05 20:06:13 +05:30
fogelito a332eb5f32 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-05 11:42:25 +03:00
fogelito 775d21e5ee lock 2026-04-05 10:08:29 +03:00
Damodar LohaniandGitHub f6484294da Merge branch '1.9.x' into feat-audit-user-type-distinction 2026-04-05 08:04:38 +05:45
Chirag AggarwalandGitHub f3b9121a14 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-04 15:57:12 +05:30
bhardwajparth51 2c1813198d Simplify comments in Realtime E2E test 2026-04-03 22:12:24 +05:30
bhardwajparth51 912ea37af6 Address review feedback: Remove redundant Realtime triggers, correctly reorder hydration, and add E2E tests 2026-04-03 22:07:24 +05:30
Parth BhardwajandGitHub ccc74dea74 Merge branch '1.9.x' into fix-10923-realtime-atomic-payload 2026-04-03 20:28:49 +05:30
bhardwajparth51 a6c6a5624a fix: ensure realtime event payload is populated for all atomic subclasses 2026-04-02 21:04:01 +05:30
bhardwajparth51 ee5bb6d73d fix: ensure realtime event payload is populated for atomic operations 2026-04-02 20:47:23 +05:30
ArnabChatterjee20k bfbf180aee Refactor realtime message handling and enhance query validation tests 2026-04-02 18:32:27 +05:30
ArnabChatterjee20k d8a3b53641 Refactor code structure for improved readability and maintainability 2026-04-02 17:35:55 +05:30
Chirag Aggarwal 3018b478ba Fix database transaction and vectors migration flakiness 2026-04-02 17:25:13 +05:30
ArnabChatterjee20k df4dbcf607 updated user roles 2026-04-02 17:02:46 +05:30
Chirag Aggarwal 094fe90499 Merge remote-tracking branch 'origin/1.9.x' into feat/migrate-di-container
# Conflicts:
#	app/worker.php
2026-04-02 16:37:58 +05:30
ArnabChatterjee20k 29b0ebb3bd updated query subscription 2026-04-02 16:28:00 +05:30
ArnabChatterjee20k f0ccd1f586 added message based query payload to realtime 2026-04-02 15:56:56 +05:30
Chirag Aggarwal 4df5f4a18f fix: scope composer lock to intended package updates 2026-04-02 14:06:45 +05:30
Chirag Aggarwal e8bcc94187 fix: keep composer lock scoped to intended updates 2026-04-02 13:59:52 +05:30
Prem Palanisamy a071c715bc Merge remote-tracking branch 'origin/1.9.x' into bump-database-version2 2026-04-02 10:10:03 +02:00
Prem Palanisamy 52981e0164 fix: restore full CI matrix and revert test/endpoint changes to 1.9.x 2026-04-02 10:09:36 +02:00
Chirag Aggarwal e576cd5082 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-02 13:32:31 +05:30
Chirag Aggarwal 30befc6a60 fix: remove strict pool size exception 2026-04-02 11:41:35 +05:30
Chirag Aggarwal d13644a47a fix: make pool sizing runtime-aware 2026-04-02 11:38:19 +05:30
Chirag Aggarwal 4a905a6ac9 Merge branch '1.9.x' into feat/migrate-di-container
Resolve conflicts keeping DI container migration (container->set pattern)
while incorporating 1.9.x fixes: PHPStan unused variable cleanup in
GraphQL Resolvers, (int) casts in Builds.php, and phpstan-baseline removal.
2026-04-02 11:17:32 +05:30
Prem Palanisamy cb9d3869f0 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version2
# Conflicts:
#	src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php
2026-04-02 06:53:05 +02:00
Prem Palanisamy d17ae517d3 fix: replace exists+create with try/catch for vectorsdb metadata init 2026-04-02 06:26:55 +02:00
Prem Palanisamy 25d9891f2b fix: use email probe in JSON export test, format fixes, add requireAdapter skips to Databases VectorsDB/DocumentsDB tests 2026-04-02 04:39:56 +02:00
Damodar LohaniandGitHub a928fcd40f Merge pull request #11754 from appwrite/claude/sync-1.9.x-fix-tests-VoCBt
Fix PHPStan baseline and code quality issues
2026-04-02 08:17:54 +05:45
Claude 2d34301834 fix: add missing userType field to legacy log endpoints
The users.php and messaging.php legacy controllers were missing the
userType field in their log output, creating an inconsistency with the
new audit user type distinction feature. Also adds missing mode field
to users.php logs endpoint.

https://claude.ai/code/session_01J9gKXwbHoLggsGwJi6KUnM
2026-04-02 01:13:01 +00:00
Claude cf7259e060 Merge remote-tracking branch 'origin/1.9.x' into claude/sync-1.9.x-fix-tests-VoCBt 2026-04-02 01:05:56 +00:00
Prem Palanisamy 8c9f9c6f58 fix: inherit project shared tables for separate pools, skip cross-engine migration tests 2026-04-02 02:05:10 +02:00
Prem Palanisamy c322cc3ffe Merge remote-tracking branch 'origin/1.9.x' into bump-database-version
# Conflicts:
#	src/Appwrite/Event/Message/Usage.php
2026-04-01 17:34:13 +02:00
ArnabChatterjee20k cf99269bc5 Add reset method to Realtime class for clearing event state
This new method resets the event state for long-lived worker processes by clearing subscribers, context, and user-related fields, ensuring no stale state affects subsequent triggers.
2026-04-01 18:08:45 +05:30
ArnabChatterjee20k 2a184288f4 removed throw 2026-04-01 18:05:56 +05:30
ArnabChatterjee20k 9237f387fc added reset in db worker for queue for realtime 2026-04-01 17:59:28 +05:30
Matej Bačo b554243447 Merge branch '1.9.x' into feat-public-platform-api 2026-04-01 13:35:46 +02:00
Chirag Aggarwal 15b2ab321e fix: restore pool size validation to prevent silent connection exhaustion
The old guard that threw when workerCount > instanceConnections was
removed during the DI migration, causing pool size to silently floor
to 1. This can lead to connection exhaustion on multi-core hosts.
2026-04-01 16:34:58 +05:30
Chirag Aggarwal 789870b545 fix: preserve multi-value headers like Set-Cookie instead of comma-joining
addHeader() already accumulates multiple values for the same key into an
array internally, so calling it once per value is the correct approach.
Comma-joining violates RFC 6265 for Set-Cookie headers.
2026-04-01 15:43:14 +05:30
Chirag Aggarwal c9f7b7f0d9 fix: address PR review findings from code review
- Add Console::error() fallback in Bus::dispatch() so listener failures
  are visible even without telemetry (C1/M7)
- Remove duplicate $max/$sleep assignments in createDatabase (M1)
- Remove duplicate @param in Event::generateEvents docblock (M2)
- Remove unused $plan parameter from plan resource factory (M3)
- Fix inconsistent indentation in certificate init block (L2)
- Add explicit return null in session resource factory (M6)
2026-04-01 15:42:15 +05:30
Chirag Aggarwal cba7e53898 chore: fix composer 2026-04-01 15:37:34 +05:30
Chirag Aggarwal fb26da5df1 analyze fixes 2026-04-01 15:15:48 +05:30
Chirag Aggarwal eb8455bd76 revert 2026-04-01 14:29:20 +05:30
Chirag AggarwalandGitHub a76a03d988 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-01 14:22:13 +05:30
Prem Palanisamy d9eb69aa47 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version 2026-04-01 10:41:39 +02:00
Damodar LohaniandGitHub e250b413f0 Merge branch '1.9.x' into feat-audit-user-type-distinction 2026-04-01 12:58:09 +05:45
Harsh MahajanandGitHub 8ccfb1aebb Merge branch '1.9.x' into feat-x-oauth2-provider 2026-04-01 12:17:05 +05:30
Harsh Mahajan 9da4f19d4f fix: pkce flow 2026-04-01 12:11:40 +05:30
Chirag Aggarwal cc04c682b0 chore: use phpstan-baseline.neon from 1.9.x 2026-04-01 11:49:43 +05:30
Chirag Aggarwal 66ba483b6a chore: remove inapplicable phpstan baseline entries from 1.9.x merge
$register variable.undefined (app/http.php) and binary op (app/worker.php) suppressions don't apply to this branch's rewritten DI container code.
2026-04-01 11:48:41 +05:30
Chirag Aggarwal 908e408480 Merge remote-tracking branch 'origin/1.9.x' into feat/migrate-di-container
# Conflicts:
#	app/init/resources.php
#	composer.json
#	composer.lock
#	phpstan-baseline.neon
2026-04-01 11:46:13 +05:30
Prem Palanisamy 7f9ce1ca85 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version 2026-04-01 07:37:57 +02:00
Prem Palanisamy 168166b9c1 bump utopia-php/database to 5.3.19 and fix shared-mode CI failures
- Bump utopia-php/database from 5.3.17 to 5.3.19

- Remove invalid (int) cast on tenant sequence in shared tables mode

- Fix DSN construction for documentsdb/vectorsdb: filter empty strings
  from explode(), skip pool filtering when shared tables env vars unset,
  fail fast when no pool found

- Use dedicated mode for separate database pools in getDatabasesDB
  since shared tables can't work across engines (PostgreSQL integer
  _tenant vs MongoDB UUID tenant). Auto-init schema on first use.

- Add documentsdb/vectorsdb shared tables env vars to CI workflow

- Fix testChannelTablesDBRowUpdate race condition with deterministic
  event drain loop
2026-03-31 21:24:22 +02:00
fogelito e42a26fdad comment 2026-03-31 19:20:13 +03:00
fogelito 2c4770d29b Database 5.3.19 2026-03-31 19:18:32 +03:00
fogelito 2a9e423cb1 Temporary disabling deletes from internal collections 2026-03-31 12:54:33 +03:00
fogelito 14869cc6d6 Safe delete shared tables v1 2026-03-31 09:59:53 +03:00
Harsh Mahajan fe99470374 revert test env change 2026-03-30 16:09:42 +05:30
Chirag Aggarwal ff903e7cbb lock file 2026-03-28 20:39:08 +05:30
Chirag AggarwalandGitHub 10ebf942b1 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-28 20:36:08 +05:30
Matej Bačo d9a75c5938 Improved backwards compatibility 2026-03-27 15:34:34 +01:00
Matej Bačo bb80e50d01 Self review after refactor 2026-03-27 14:00:44 +01:00
Matej Bačo 644840ec66 Refactor to new platforms interfaces 2026-03-27 13:45:58 +01:00
Matej Bačo 71e714c66b Remove extra logging 2026-03-27 12:49:16 +01:00
Matej Bačo b2b187c57d review fixes 2026-03-27 12:35:48 +01:00
copilot-swe-agent[bot]andMeldiron ecc76c7520 fix: address review feedback - log abort exception, fix test cleanup
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/045e63f6-9a81-447b-ba79-7391ab97fb01

Co-authored-by: Meldiron <19310830+Meldiron@users.noreply.github.com>
2026-03-26 18:34:19 +00:00
copilot-swe-agent[bot]andMeldiron 898e8e214b fix: allow deletion of partially-uploaded (pending) files and add test
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/8d14b17a-78d8-48c6-b6d8-51d9697adde0

Co-authored-by: Meldiron <19310830+Meldiron@users.noreply.github.com>
2026-03-26 18:04:41 +00:00
Matej Bačo 43db2ddf6e Fix default null 2026-03-26 16:24:28 +01:00
Matej Bačo 7371b68418 Fix tests compatibility 2026-03-26 15:51:03 +01:00
Matej Bačo 7f4d5f692d Fix non-expiry key actions 2026-03-26 15:44:50 +01:00
Matej Bačo 8113854a89 AI review fixes 2026-03-26 15:37:57 +01:00
Matej Bačo eb097a037b Finish security todos 2026-03-26 15:35:40 +01:00
Matej Bačo cbfdd27834 Public keys Apis 2026-03-26 15:00:20 +01:00
Matej Bačo 113c266881 Fix audit resource ID missing 2026-03-26 14:43:38 +01:00
Harsh Mahajan 614db7388e fix: push 2026-03-26 17:59:30 +05:30
Harsh MahajanandGitHub cbc5259db3 Merge branch '1.9.x' into feat-x-oauth2-provider 2026-03-25 23:29:04 +05:30
Matej Bačo 0d6d6a0a35 Make tests pass 2026-03-25 16:11:01 +01:00
Matej Bačo e8d4065af5 Improve tests human review 2026-03-25 15:54:10 +01:00
Matej Bačo 008110b3f7 Platform API tests 2026-03-25 15:47:11 +01:00
Harsh MahajanandGitHub efb0cae4e0 Merge branch '1.9.x' into feat-x-oauth2-provider 2026-03-25 16:49:54 +05:30
Matej Bačo ae99d59aba CodeQL review 2026-03-24 14:52:58 +01:00
Matej Bačo a06aaaf9ca Remove Db schema changes 2026-03-24 13:55:54 +01:00
Chirag AggarwalandGitHub 4970d1b6f2 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-24 18:15:45 +05:30
Matej Bačo 094cd180b5 Remove leftover 2026-03-24 13:41:12 +01:00
Matej Bačo a8f43f3486 Update DB schema 2026-03-24 13:36:11 +01:00
Matej Bačo 038f4b5992 More backwards compatibility fixes 2026-03-24 13:33:15 +01:00
Matej Bačo c903fb87ac Fix backwards compatibility 2026-03-24 12:53:25 +01:00
Matej Bačo e5841c4cc0 Fix syntax error 2026-03-24 11:29:24 +01:00
Matej Bačo bb7815bd57 Merge branch '1.9.x' into feat-public-platform-api 2026-03-24 11:18:18 +01:00
Chirag Aggarwal 0c33d981a7 fix analyze 2026-03-24 10:47:41 +05:30
Chirag Aggarwal 36d8ad6e7c lock file 2026-03-24 10:21:40 +05:30
Chirag Aggarwal fbce66d500 fix merge conflict 2026-03-24 10:19:34 +05:30
Chirag Aggarwal 89db65299d Merge remote-tracking branch 'origin/1.9.x' into feat/migrate-di-container 2026-03-24 10:15:38 +05:30
Chirag Aggarwal ea6a05be4f fix analyze 2026-03-24 07:51:23 +05:30
Chirag Aggarwal 355acf6824 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-23 23:01:30 +05:30
Chirag Aggarwal c2988efa08 chore: use stable 2026-03-23 22:51:49 +05:30
Matej Bačo 8ccf5094f8 fix multi-model conditions 2026-03-23 16:42:14 +01:00
Matej Bačo 39f2d24907 AI code review fixes 2026-03-23 16:10:12 +01:00
Matej Bačo d3c5a425e7 Implement public platform API 2026-03-23 15:46:11 +01:00
Harsh Mahajan 85703d29e1 addressed greptile suggestions 2026-03-23 19:08:12 +05:30
Harsh MahajanandGitHub d3c11b5c93 Merge branch '1.9.x' into feat-x-oauth2-provider 2026-03-23 18:51:10 +05:30
Harsh MahajanandGitHub 6f177a0a7a Merge branch '1.9.x' into feat-x-oauth2-provider 2026-03-23 17:50:29 +05:30
Harsh Mahajan dc48bb35ef added pkce to base 2026-03-23 17:49:42 +05:30
Harsh Mahajan 8218f36d34 code rabbit comment 2026-03-23 13:32:06 +05:30
Harsh Mahajan 0fe906c538 feat: Add X OAuth 2.0 provider 2026-03-23 13:21:04 +05:30
Chirag Aggarwal 4641596a6d use stable 2026-03-23 10:28:26 +05:30
Chirag Aggarwal d932527561 add null collacing 2026-03-23 10:27:10 +05:30
Chirag Aggarwal 89c072e223 fix analyze 2026-03-23 10:20:45 +05:30
Chirag Aggarwal 6421bc8689 fn name 2026-03-23 10:08:19 +05:30
Chirag Aggarwal d008d9bff0 merge conficts 2026-03-23 10:01:27 +05:30
Chirag Aggarwal f6dc6359bb Merge branch '1.9.x' into feat/migrate-di-container 2026-03-23 10:01:03 +05:30
Damodar LohaniandGitHub 31410879a7 Merge branch '1.9.x' into feat-audit-user-type-distinction 2026-03-22 08:36:13 +05:45
Damodar Lohani 343e352b17 fix: prevent overwriting user type in audit queue if already set 2026-03-22 02:31:46 +00:00
Damodar LohaniandGitHub b38ea72407 Merge branch '1.8.x' into feat-audit-user-type-distinction 2026-03-22 08:05:29 +05:45
Chirag Aggarwal 032638e896 fix 2026-03-20 15:35:28 +05:30
Chirag Aggarwal 10cc6a8040 fix global pools state 2026-03-20 14:09:43 +05:30
Chirag Aggarwal 9ecdbf5950 func exists 2026-03-20 13:13:07 +05:30
Chirag Aggarwal 55b436c67b lock file 2026-03-20 12:17:28 +05:30
Chirag Aggarwal 05defcc6e0 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-20 12:12:29 +05:30
Chirag Aggarwal 6700340ef3 fix realtime 2026-03-19 23:26:49 +05:30
Chirag Aggarwal 4224f6ea5a merge conficts 2026-03-19 22:15:31 +05:30
Chirag Aggarwal 8d925f3670 merge conficts 2026-03-19 21:35:19 +05:30
Chirag Aggarwal d2875c9bf6 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-19 21:35:06 +05:30
Chirag Aggarwal c002f4afe3 fix specs generation 2026-03-19 21:23:00 +05:30
Chirag Aggarwal ecb8104340 register resources 2026-03-19 17:37:47 +05:30
Chirag Aggarwal e6090800e2 register resources 2026-03-19 17:35:31 +05:30
Damodar LohaniandGitHub 92b7760e94 Merge branch '1.8.x' into feat-audit-user-type-distinction 2026-03-19 17:44:50 +05:45
Damodar LohaniGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
fe988f4489 Update app/controllers/shared/api.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-19 17:44:35 +05:45
Chirag AggarwalandGitHub 0237cced6b Merge branch '1.8.x' into feat/migrate-di-container 2026-03-19 14:10:20 +05:30
Chirag Aggarwal 625ec4ce91 sync changes 2026-03-19 14:09:04 +05:30
Chirag Aggarwal 5339592b3f update http 2026-03-19 10:20:47 +05:30
Chirag Aggarwal 9f1d092717 analyze fixes 2026-03-19 10:17:25 +05:30
Chirag Aggarwal e38a4e4347 update queue 2026-03-19 10:08:40 +05:30
Chirag Aggarwal fdbc5b6737 update queue 2026-03-19 10:07:08 +05:30
Chirag Aggarwal fa2b7955d0 update dependencies 2026-03-19 10:04:47 +05:30
Damodar LohaniandClaude Opus 4.6 8b3d3c6f8b feat: distinguish user types in audit logs
Introduce granular audit user types to differentiate between regular
users, console admins, guests, and the various API key scopes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 03:55:22 +00:00
Chirag Aggarwal ea11af1e15 chore: worker changes 2026-03-18 17:50:25 +05:30
Chirag AggarwalandGitHub 6cbae88131 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-18 17:14:45 +05:30
Chirag Aggarwal 03b2755aaf fix async execution 2026-03-18 16:43:14 +05:30
Chirag AggarwalandGitHub 2095a94bc5 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-18 14:19:59 +05:30
Chirag Aggarwal df7796d5cb restore phpunit 2026-03-18 14:19:43 +05:30
Chirag Aggarwal d177e3adfc fix pool issue 2026-03-18 11:27:26 +05:30
Chirag Aggarwal d04ac5c3e6 fix pool issue 2026-03-18 09:42:18 +05:30
Chirag Aggarwal 60939da801 fix graphql 2026-03-17 21:39:50 +05:30
Chirag Aggarwal cdb301a293 fix PHPStan errors without regenerating baseline
- Fix dispatch() type hint to use \Swoole\Http\Server instead of Utopia adapter
- Remove unused $register from go() closure in http.php
- Remove unnecessary ?? '' on non-nullable $hostname
- Remove unsupported override: param from addHeader() call
- Update Resolvers.php for new getResource()/execute() signatures
- Migrate Installer/Server.php from static Http::setResource() to container
- Remove stale baseline entries, add 1 for pre-existing Deployment.php issue
2026-03-17 17:30:42 +05:30
Chirag Aggarwal 14a9aa890f fix issues 2026-03-17 17:18:57 +05:30
Chirag Aggarwal d3833d06d5 Merge 1.8.x (accept theirs) 2026-03-17 17:15:54 +05:30
Chirag Aggarwal 5ba19ec763 fix alias 2026-03-17 17:14:35 +05:30
Chirag Aggarwal 0564936c4f update file name 2026-03-17 16:29:21 +05:30
Chirag Aggarwal c2795376d8 update server 2026-03-17 16:25:52 +05:30
Chirag AggarwalandGitHub 2b898dfe50 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-17 15:22:02 +05:30
Chirag Aggarwal fa1404be52 cleanup 2026-03-17 15:20:29 +05:30
Chirag Aggarwal be87675e1c fix realtime 2026-03-17 15:03:55 +05:30
Chirag Aggarwal b1edf75c4e phpunit 2026-03-17 14:29:04 +05:30
Chirag Aggarwal 8a0f092342 fix: only throw container lookup errors 2026-03-17 14:28:37 +05:30
Chirag Aggarwal 87b02de612 baseline 2026-03-17 11:32:15 +05:30
Chirag Aggarwal f8e9f71de3 move resources 2026-03-17 11:19:17 +05:30
Chirag Aggarwal c055c32371 enable coroutines 2026-03-17 10:52:35 +05:30
Chirag Aggarwal d60858cd8f fix phpstan 2026-03-17 10:37:49 +05:30
Chirag Aggarwal 22bd655ad1 fix autoload 2026-03-17 10:28:08 +05:30
Chirag Aggarwal c900b22dc0 fix connection container and view class 2026-03-17 09:52:55 +05:30
Chirag Aggarwal 27e5ade92b fix login 2026-03-17 09:43:54 +05:30
Chirag Aggarwal d9c1b9db2a chore: register request resources seperately 2026-03-17 08:49:43 +05:30
Chirag Aggarwal eb399c7bd0 wip 2026-03-16 23:45:18 +05:30
Chirag Aggarwal b8e51366d7 add missing compression methods 2026-03-16 23:32:31 +05:30
Chirag Aggarwal b75cd993be missing trusted ip headers 2026-03-16 23:27:34 +05:30
Chirag Aggarwal a1bc503ce4 formatting 2026-03-16 23:23:03 +05:30
Chirag Aggarwal e475a7ac5a lock file 2026-03-16 23:19:07 +05:30
Chirag Aggarwal 2dc24dabfd Merge branch '1.8.x' into feat/migrate-di-container 2026-03-16 23:10:09 +05:30
Chirag Aggarwal aaa2a0525f feat: migrate from static Http::setResource() to DI Container
Upgrade utopia-php/framework from 0.33.x to 0.34.x which removes the
static Http::setResource() API. Resources are now registered on a
Utopia\DI\Container instance.

- Replace 81 Http::setResource() calls in resources.php with $container->set()
- Refactor http.php to use Swoole HttpServer adapter with shared container
- Refactor realtime.php to use FPM adapter with global container
- Refactor cli.php to use direct $cli->setResource() calls
- Update Specs.php to use local container + FPM adapter
- Update Migrate.php to inject console document instead of creating Http instance
- Update GraphQL Schema.php to use instance setResource()
2026-03-16 23:00:36 +05:30
226 changed files with 17264 additions and 7211 deletions
@@ -0,0 +1,29 @@
# Patch Release Checklist for Appwrite
When bumping a patch version (e.g., `1.9.0` -> `1.9.1`), follow this checklist.
## Checklist
### Bump console image
Update the console Docker image tag in both files:
- [ ] `docker-compose.yml` -- update `image: appwrite/console:X.Y.Z`
- [ ] `app/views/install/compose.phtml` -- update `image: <?php echo $organization; ?>/console:X.Y.Z`
### Bump Appwrite version
- [ ] **`app/init/constants.php`** -- update `APP_VERSION_STABLE` to the new version (e.g., `'1.9.1'`). In same file, increment `APP_CACHE_BUSTER` by 1.
- [ ] **`README.md`** -- update the Docker image tag `appwrite/appwrite:X.Y.Z` in all 3 install code blocks (Unix, Windows CMD, PowerShell).
- [ ] **`README-CN.md`** -- same Docker image tag update in all 3 install code blocks.
- [ ] **`src/Appwrite/Migration/Migration.php`** -- add the new version to the `$versions` array, mapping it to a migration class. If new class exists, use that, otherwise use sle same class as previous version
### Update CHANGES.md
- [ ] Add a new `# Version X.Y.Z` section at the top of `CHANGES.md` with subsections: `### Notable changes`, `### Fixes`, `### Miscellaneous`
## Final review
- [ ] Ask user to review changes before commiting
- [ ] Ask user to update `CHANGES.md` with PRs
- [ ] Ask user to generate specs, if needed
- [ ] Ask user to add request and response filters, if needed
+34 -1
View File
@@ -161,6 +161,27 @@ jobs:
- name: Run PHPStan
run: composer analyze -- --no-progress
specs:
name: Checks / Specs
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: swoole
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Generate specs
run: _APP_STORAGE_LIMIT=5368709120 php app/cli.php specs --version=latest --git=no
locale:
name: Checks / Locale
runs-on: ubuntu-latest
@@ -459,6 +480,10 @@ jobs:
_APP_BROWSER_HOST: http://invalid-browser/v1
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
@@ -487,7 +512,7 @@ jobs:
# Services that rely on sequential test method execution (shared static state)
FUNCTIONAL_FLAG="--functional"
case "${{ matrix.service }}" in
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
esac
docker compose exec -T \
@@ -533,6 +558,10 @@ jobs:
_APP_OPTIONS_ABUSE: enabled
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
@@ -590,6 +619,10 @@ jobs:
env:
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
+4
View File
@@ -115,6 +115,10 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
- Never hardcode credentials -- use environment variables.
- Code changes may require container restart. No central log location -- check relevant containers.
## Patch release process
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
## Cross-repo context
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
+3 -3
View File
@@ -72,7 +72,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
### Windows
@@ -84,7 +84,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
#### PowerShell
@@ -94,7 +94,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+4 -4
View File
@@ -44,7 +44,7 @@ Table of Contents:
## Products
- **[Appwrite Auth](https://appwrite.io/docs/products/authentication)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
- **[Appwrite Auth](https://appwrite.io/docs/products/auth)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
- **[Appwrite Databases](https://appwrite.io/docs/products/databases)** - Scalable structured data storage with support for databases, tables, and rows. Includes querying, pagination, indexing, and relationships to model complex application data.
@@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
### Windows
@@ -88,7 +88,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
#### PowerShell
@@ -99,7 +99,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
+42 -44
View File
@@ -2,12 +2,12 @@
require_once __DIR__ . '/init.php';
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\StatsResources;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Appwrite\Usage\Context as UsageContext;
@@ -18,13 +18,15 @@ use Swoole\Timer;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\Adapters\Generic;
use Utopia\CLI\CLI;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Dependency;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Service;
@@ -47,7 +49,7 @@ require_once __DIR__ . '/controllers/general.php';
global $register;
$platform = new Appwrite();
$args = $platform->getEnv('argv');
$args = $_SERVER['argv'] ?? [];
\array_shift($args);
if (! isset($args[0])) {
@@ -56,21 +58,15 @@ if (! isset($args[0])) {
}
$taskName = $args[0];
$container = new Container();
$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container);
$platform->setCli($cli);
$platform->init(Service::TYPE_TASK);
$cli = $platform->getCli();
$setResource = function (string $name, callable $callback, array $injections = []) use ($cli) {
$dependency = new Dependency();
$dependency->setName($name)->setCallback($callback);
foreach ($injections as $injection) {
$dependency->inject($injection);
}
$cli->setResource($dependency);
};
$container->set('register', fn () => $register, []);
$setResource('register', fn () => $register, []);
$setResource('cache', function ($pools) {
$container->set('cache', function ($pools) {
$list = Config::getParam('pools-cache', []);
$adapters = [];
@@ -81,18 +77,18 @@ $setResource('cache', function ($pools) {
return new Cache(new Sharding($adapters));
}, ['pools']);
$setResource('pools', function (Registry $register) {
$container->set('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
$setResource('authorization', function () {
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
$setResource('dbForPlatform', function ($pools, $cache, $authorization) {
$container->set('dbForPlatform', function ($pools, $cache, $authorization) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -135,17 +131,17 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) {
return $dbForPlatform;
}, ['pools', 'cache', 'authorization']);
$setResource('console', function () {
$container->set('console', function () {
return new Document(Config::getParam('console'));
}, []);
$setResource(
$container->set(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false,
[]
);
$setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
@@ -207,10 +203,10 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
@@ -235,41 +231,43 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
return $database;
};
}, ['pools', 'cache', 'authorization']);
$setResource('publisher', function (Group $pools) {
$container->set('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
$setResource('publisherDatabases', function (BrokerPool $publisher) {
$container->set('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherFunctions', function (BrokerPool $publisher) {
$container->set('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherMigrations', function (BrokerPool $publisher) {
$container->set('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherMessaging', function (BrokerPool $publisher) {
$container->set('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('usage', function () {
$container->set('usage', function () {
return new UsageContext();
}, []);
$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
$setResource('queueForFunctions', function (Publisher $publisher) {
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$setResource('queueForDeletes', function (Publisher $publisher) {
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$setResource('logError', function (Registry $register) {
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
Console::error('[Error] Type: ' . get_class($error));
@@ -321,13 +319,13 @@ $setResource('logError', function (Registry $register) {
};
}, ['register']);
$setResource('executor', fn () => new Executor(), []);
$container->set('executor', fn () => new Executor(), []);
$setResource('bus', function (Registry $register) use ($cli) {
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
$container->set('bus', function (Registry $register) use ($container) {
return $register->get('bus')->setResolver(fn (string $name) => $container->get($name));
}, ['register']);
$setResource('telemetry', fn () => new NoTelemetry(), []);
$container->set('telemetry', fn () => new NoTelemetry(), []);
$exitCode = 0;
+3 -3
View File
@@ -594,7 +594,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('key'),
'$id' => ID::custom('key'), // For app platforms
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -605,7 +605,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('store'),
'$id' => ID::custom('store'), // Unused at the moment
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
@@ -616,7 +616,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('hostname'),
'$id' => ID::custom('hostname'), // For web platforms
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
+10
View File
@@ -1179,6 +1179,16 @@ return [
'description' => 'Platform with the requested ID could not be found.',
'code' => 404,
],
Exception::PLATFORM_METHOD_UNSUPPORTED => [
'name' => Exception::PLATFORM_METHOD_UNSUPPORTED,
'description' => 'The requested platform has invalid type. Please use corresponding update method for the platform type.',
'code' => 400,
],
Exception::PLATFORM_ALREADY_EXISTS => [
'name' => Exception::PLATFORM_ALREADY_EXISTS,
'description' => 'Platform with the same ID already exists in this project. Try again with a different ID.',
'code' => 409,
],
Exception::VARIABLE_NOT_FOUND => [
'name' => Exception::VARIABLE_NOT_FOUND,
'description' => 'Variable with the requested ID could not be found.',
+11
View File
@@ -376,6 +376,17 @@ return [
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Wordpress',
],
'x' => [
'name' => 'X',
'developers' => 'https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code',
'icon' => 'icon-twitter',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\X',
],
'yahoo' => [
'name' => 'Yahoo',
'developers' => 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
@@ -9,8 +9,8 @@ return [
'key' => 'graphql',
'name' => 'GraphQL',
],
'realtime' => [
'key' => 'realtime',
'name' => 'Realtime',
'websocket' => [
'key' => 'websocket',
'name' => 'Websocket',
],
];
-14
View File
@@ -3,13 +3,6 @@
// List of scopes for organization (teams) API keys
return [
"platforms.read" => [
"description" => 'Access to read project\'s platforms',
],
"platforms.write" => [
"description" =>
'Access to create, update, and delete project\'s platforms',
],
"projects.read" => [
"description" => 'Access to read organization\'s projects',
],
@@ -17,13 +10,6 @@ return [
"description" =>
"Access to create, update, and delete projects in organization",
],
"keys.read" => [
"description" => 'Access to read project\'s API keys',
],
"keys.write" => [
"description" =>
"Access to create, update, and delete project\'s API keys",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
],
+16
View File
@@ -188,4 +188,20 @@ return [ // List of publicly visible scopes
"description" =>
"Access to update project\'s information",
],
"keys.read" => [
"description" =>
"Access to read project\'s keys",
],
"keys.write" => [
"description" =>
"Access to create, update, and delete project\'s keys",
],
"platforms.read" => [
"description" =>
"Access to read project\'s platforms",
],
"platforms.write" => [
"description" =>
"Access to create, update, and delete project\'s platforms",
],
];
+4 -4
View File
@@ -137,7 +137,7 @@ return [
'docs' => true,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -193,7 +193,7 @@ return [
'docs' => false,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -235,7 +235,7 @@ return [
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/proxy',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '/images/services/proxy.png',
'platforms' => ['client', 'server', 'console'],
],
@@ -291,7 +291,7 @@ return [
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/migrations',
'tests' => true,
'optional' => false,
'optional' => true,
'icon' => '/images/services/migrations.png',
'platforms' => ['client', 'server', 'console'],
],
+4 -4
View File
@@ -872,18 +872,18 @@ return [
],
[
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
'introduction' => '0.13.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
'introduction' => '1.7.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
+74 -197
View File
@@ -9,6 +9,7 @@ use Appwrite\Auth\Validator\PasswordDictionary;
use Appwrite\Auth\Validator\PasswordHistory;
use Appwrite\Auth\Validator\PersonalData;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@@ -41,6 +42,7 @@ use Utopia\Auth\Proofs\Code as ProofsCode;
use Utopia\Auth\Proofs\Password as ProofsPassword;
use Utopia\Auth\Proofs\Token as ProofsToken;
use Utopia\Auth\Store;
use Utopia\Bus\Bus;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -75,139 +77,8 @@ use Utopia\Validator\WhiteList;
$oauthDefaultSuccess = '/console/auth/oauth2/success';
$oauthDefaultFailure = '/console/auth/oauth2/failure';
function sendSessionAlert(Locale $locale, Document $user, Document $project, array $platform, Document $session, Mail $queueForMails)
{
$subject = $locale->getText("emails.sessionAlert.subject");
$preview = $locale->getText("emails.sessionAlert.preview");
$customTemplate = $project->getAttribute('templates', [])['email.sessionAlert-' . $locale->default] ?? [];
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
$validator = new FileName();
if (!$validator->isValid($smtpBaseTemplate)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path');
}
$bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl';
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-session-alert.tpl');
$message
->setParam('{{hello}}', $locale->getText("emails.sessionAlert.hello"))
->setParam('{{body}}', $locale->getText("emails.sessionAlert.body"))
->setParam('{{listDevice}}', $locale->getText("emails.sessionAlert.listDevice"))
->setParam('{{listIpAddress}}', $locale->getText("emails.sessionAlert.listIpAddress"))
->setParam('{{listCountry}}', $locale->getText("emails.sessionAlert.listCountry"))
->setParam('{{footer}}', $locale->getText("emails.sessionAlert.footer"))
->setParam('{{thanks}}', $locale->getText("emails.sessionAlert.thanks"))
->setParam('{{signature}}', $locale->getText("emails.sessionAlert.signature"));
$body = $message->render();
$smtp = $project->getAttribute('smtp', []);
$smtpEnabled = $smtp['enabled'] ?? false;
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
$senderEmail = $smtp['senderEmail'];
}
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
}
$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'];
}
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
}
$body = $customTemplate['message'] ?? '';
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
// session alerts should always have a client name!
$clientName = $session->getAttribute('clientName');
if (empty($clientName)) {
// fallback to the user agent and then unknown!
$userAgent = $session->getAttribute('userAgent');
$clientName = !empty($userAgent) ? $userAgent : 'UNKNOWN';
$session->setAttribute('clientName', $clientName);
}
$projectName = $project->getAttribute('name');
if ($project->getId() === 'console') {
$projectName = $platform['platformName'];
}
$emailVariables = [
'direction' => $locale->getText('settings.direction'),
'date' => (new \DateTime())->format('F j'),
'year' => (new \DateTime())->format('YYYY'),
'time' => (new \DateTime())->format('H:i:s'),
'user' => $user->getAttribute('name'),
'project' => $projectName,
'device' => $session->getAttribute('clientName'),
'ipAddress' => $session->getAttribute('ip'),
'country' => $locale->getText('countries.' . $session->getAttribute('countryCode'), $locale->getText('locale.country.unknown')),
];
if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
$emailVariables = array_merge($emailVariables, [
'accentColor' => $platform['accentColor'],
'logoUrl' => $platform['logoUrl'],
'twitter' => $platform['twitterUrl'],
'discord' => $platform['discordUrl'],
'github' => $platform['githubUrl'],
'terms' => $platform['termsUrl'],
'privacy' => $platform['privacyUrl'],
'platform' => $platform['platformName'],
]);
}
$email = $user->getAttribute('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();
}
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Bus $bus, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
// Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
$oauthProvider = null;
@@ -318,23 +189,12 @@ $createSession = function (string $userId, string $secret, Request $request, Res
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
}
$isAllowedTokenType = match ($verifiedToken->getAttribute('type')) {
TOKEN_TYPE_MAGIC_URL,
TOKEN_TYPE_EMAIL => false,
default => true
};
$hasUserEmail = $user->getAttribute('email', false) !== false;
$isSessionAlertsEnabled = $project->getAttribute('auths', [])['sessionAlerts'] ?? false;
$isNotFirstSession = $dbForProject->count('sessions', [
Query::equal('userId', [$user->getId()]),
]) !== 1;
if ($isAllowedTokenType && $hasUserEmail && $isSessionAlertsEnabled && $isNotFirstSession) {
sendSessionAlert($locale, $user, $project, $platform, $session, $queueForMails);
}
$bus->dispatch(new SessionCreated(
user: $user->getArrayCopy(),
project: $project->getArrayCopy(),
session: $session->getArrayCopy(),
locale: $locale->default,
));
$queueForEvents
->setParam('userId', $user->getId())
@@ -345,7 +205,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
->setProperty('secret', $sessionSecret)
->encode();
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
@@ -353,8 +213,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$protocol = $request->getProtocol();
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED);
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
@@ -719,7 +579,9 @@ Http::delete('/v1/account/sessions')
->inject('queueForDeletes')
->inject('store')
->inject('proofForToken')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
->inject('domainVerification')
->inject('cookieDomain')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
@@ -728,7 +590,7 @@ Http::delete('/v1/account/sessions')
foreach ($sessions as $session) {/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
@@ -741,8 +603,8 @@ Http::delete('/v1/account/sessions')
// If current session delete the cookies too
$response
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
// Use current session for events.
$currentSession = $session;
@@ -849,7 +711,9 @@ Http::delete('/v1/account/sessions/:sessionId')
->inject('queueForDeletes')
->inject('store')
->inject('proofForToken')
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
->inject('domainVerification')
->inject('cookieDomain')
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
@@ -875,13 +739,13 @@ Http::delete('/v1/account/sessions/:sessionId')
->setAttribute('current', true)
->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')));
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
$response
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
$dbForProject->purgeCachedDocument('users', $user->getId());
@@ -1030,13 +894,15 @@ Http::post('/v1/account/sessions/email')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->inject('queueForMails')
->inject('bus')
->inject('hooks')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Bus $bus, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
$email = \strtolower($email);
$protocol = $request->getProtocol();
@@ -1097,28 +963,28 @@ Http::post('/v1/account/sessions/email')
]));
}
$dbForProject->purgeCachedDocument('users', $user->getId());
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->purgeCachedDocument('users', $user->getId());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
@@ -1135,15 +1001,12 @@ Http::post('/v1/account/sessions/email')
->setParam('sessionId', $session->getId())
;
if ($project->getAttribute('auths', [])['sessionAlerts'] ?? false) {
if (
$dbForProject->count('sessions', [
Query::equal('userId', [$user->getId()]),
]) !== 1
) {
sendSessionAlert($locale, $user, $project, $platform, $session, $queueForMails);
}
}
$bus->dispatch(new SessionCreated(
user: $user->getArrayCopy(),
project: $project->getArrayCopy(),
session: $session->getArrayCopy(),
locale: $locale->default,
));
$response->dynamic($session, Response::MODEL_SESSION);
});
@@ -1184,8 +1047,10 @@ Http::post('/v1/account/sessions/anonymous')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
$protocol = $request->getProtocol();
if ('console' === $project->getId()) {
@@ -1276,15 +1141,15 @@ Http::post('/v1/account/sessions/anonymous')
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
@@ -1335,11 +1200,13 @@ Http::post('/v1/account/sessions/token')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->inject('queueForMails')
->inject('bus')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('authorization')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action($createSession);
Http::get('/v1/account/sessions/oauth2/:provider')
@@ -1538,8 +1405,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('proofForPassword')
->inject('proofForToken')
->inject('plan')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) {
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@@ -2055,7 +1924,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
@@ -2068,14 +1937,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
// TODO: Remove this deprecated workaround - support only token
if ($state['success']['path'] == $oauthDefaultSuccess) {
$query['project'] = $project->getId();
$query['domain'] = Config::getParam('cookieDomain');
$query['domain'] = $cookieDomain;
$query['key'] = $store->getKey();
$query['secret'] = $encoded;
}
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
@@ -2173,7 +2042,7 @@ Http::get('/v1/account/tokens/oauth2/:provider')
}
$host = $platform['consoleHostname'] ?? '';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$redirectBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
@@ -2196,10 +2065,12 @@ Http::get('/v1/account/tokens/oauth2/:provider')
'token' => true,
], $scopes);
$loginURL = $oauth2->getLoginURL();
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($oauth2->getLoginURL());
->redirect($loginURL);
});
Http::post('/v1/account/tokens/magic-url')
@@ -2883,14 +2754,16 @@ Http::put('/v1/account/sessions/magic-url')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->inject('queueForMails')
->inject('bus')
->inject('store')
->inject('proofForCode')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) {
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $bus, $store, $proofForCode, $domainVerification, $cookieDomain, $authorization) use ($createSession) {
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
$proofForToken->setHash(new Sha());
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization);
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $bus, $store, $proofForToken, $proofForCode, $domainVerification, $cookieDomain, $authorization);
});
Http::put('/v1/account/sessions/phone')
@@ -2932,10 +2805,12 @@ Http::put('/v1/account/sessions/phone')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->inject('queueForMails')
->inject('bus')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action($createSession);
@@ -3727,7 +3602,9 @@ Http::patch('/v1/account/status')
->inject('dbForProject')
->inject('queueForEvents')
->inject('store')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store) {
->inject('domainVerification')
->inject('cookieDomain')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, bool $domainVerification, ?string $cookieDomain) {
$user->setAttribute('status', false);
@@ -3737,14 +3614,14 @@ Http::patch('/v1/account/status')
->setParam('userId', $user->getId())
->setPayload($response->output($user, Response::MODEL_ACCOUNT));
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
$protocol = $request->getProtocol();
$response
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic($user, Response::MODEL_ACCOUNT);
+1 -1
View File
@@ -231,7 +231,7 @@ function execute(
$validations = GraphQL::getStandardValidationRules();
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
$validations[] = new DisableIntrospection();
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
}
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
+4
View File
@@ -1180,6 +1180,7 @@ Http::get('/v1/messaging/providers/:providerId/logs')
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -2585,6 +2586,7 @@ Http::get('/v1/messaging/topics/:topicId/logs')
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -3000,6 +3002,7 @@ Http::get('/v1/messaging/subscribers/:subscriberId/logs')
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -3813,6 +3816,7 @@ Http::get('/v1/messaging/messages/:messageId/logs')
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
+64 -74
View File
@@ -1,7 +1,8 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Event\Message\Migration as MigrationMessage;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Extend\Exception;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\SDK\AuthType;
@@ -90,10 +91,9 @@ Http::post('/v1/migrations/appwrite')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
@@ -114,12 +114,11 @@ Http::post('/v1/migrations/appwrite')
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -151,10 +150,9 @@ Http::post('/v1/migrations/firebase')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
$serviceAccountData = json_decode($serviceAccount, true);
if (empty($serviceAccountData)) {
@@ -183,12 +181,11 @@ Http::post('/v1/migrations/firebase')
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -225,10 +222,9 @@ Http::post('/v1/migrations/supabase')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
@@ -252,12 +248,11 @@ Http::post('/v1/migrations/supabase')
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -295,10 +290,9 @@ Http::post('/v1/migrations/nhost')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
@@ -323,12 +317,11 @@ Http::post('/v1/migrations/nhost')
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -368,7 +361,7 @@ Http::post('/v1/migrations/csv/imports')
->inject('deviceForFiles')
->inject('deviceForMigrations')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('publisherForMigrations')
->action(function (
string $bucketId,
string $fileId,
@@ -383,7 +376,7 @@ Http::post('/v1/migrations/csv/imports')
Device $deviceForFiles,
Device $deviceForMigrations,
Event $queueForEvents,
Migration $queueForMigrations
MigrationPublisher $publisherForMigrations
) {
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) {
@@ -479,11 +472,10 @@ Http::post('/v1/migrations/csv/imports')
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setProject($project)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -526,7 +518,7 @@ Http::post('/v1/migrations/csv/exports')
->inject('project')
->inject('platform')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('publisherForMigrations')
->action(function (
string $resourceId,
string $filename,
@@ -545,7 +537,7 @@ Http::post('/v1/migrations/csv/exports')
Document $project,
array $platform,
Event $queueForEvents,
Migration $queueForMigrations
MigrationPublisher $publisherForMigrations
) {
try {
$parsedQueries = Query::parseQueries($queries);
@@ -630,11 +622,11 @@ Http::post('/v1/migrations/csv/exports')
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -673,7 +665,7 @@ Http::post('/v1/migrations/json/imports')
->inject('deviceForFiles')
->inject('deviceForMigrations')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('publisherForMigrations')
->action(function (
string $bucketId,
string $fileId,
@@ -688,7 +680,7 @@ Http::post('/v1/migrations/json/imports')
Device $deviceForFiles,
Device $deviceForMigrations,
Event $queueForEvents,
Migration $queueForMigrations
MigrationPublisher $publisherForMigrations
) {
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) {
@@ -783,11 +775,11 @@ Http::post('/v1/migrations/json/imports')
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -826,7 +818,7 @@ Http::post('/v1/migrations/json/exports')
->inject('project')
->inject('platform')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('publisherForMigrations')
->action(function (
string $resourceId,
string $filename,
@@ -841,7 +833,7 @@ Http::post('/v1/migrations/json/exports')
Document $project,
array $platform,
Event $queueForEvents,
Migration $queueForMigrations
MigrationPublisher $publisherForMigrations
) {
try {
$parsedQueries = Query::parseQueries($queries);
@@ -915,11 +907,11 @@ Http::post('/v1/migrations/json/exports')
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -1216,9 +1208,8 @@ Http::patch('/v1/migrations/:migrationId')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForMigrations')
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, MigrationPublisher $publisherForMigrations) {
$migration = $dbForProject->getDocument('migrations', $migrationId);
if ($migration->isEmpty()) {
@@ -1234,12 +1225,11 @@ Http::patch('/v1/migrations/:migrationId')
->setAttribute('dateUpdated', \time());
// Trigger Migration
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response->noContent();
});
+4 -744
View File
@@ -5,28 +5,18 @@ use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Event\Delete;
use Appwrite\Event\Mail;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Keys;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Http;
@@ -81,202 +71,20 @@ Http::get('/v1/projects/:projectId')
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/service')
->desc('Update service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateServiceStatus',
description: '/docs/references/projects/update-service-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name.')
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$services = $project->getAttribute('services', []);
$services[$service] = $status;
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/service/all')
->desc('Update all service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateServiceStatusAll',
description: '/docs/references/projects/update-service-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allServices = array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']));
$services = [];
foreach ($allServices as $service) {
$services[$service] = $status;
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/api')
->desc('Update API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateApiStatus',
description: '/docs/references/projects/update-api-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateAPIStatus',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateAPIStatus',
description: '/docs/references/projects/update-api-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('api', '', new WhiteList(array_keys(Config::getParam('apis')), true), 'API name.')
->param('status', null, new Boolean(), 'API status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$apis = $project->getAttribute('apis', []);
$apis[$api] = $status;
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
$response->dynamic($project, Response::MODEL_PROJECT);
->action(function () {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
});
Http::patch('/v1/projects/:projectId/api/all')
->desc('Update all API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateApiStatusAll',
description: '/docs/references/projects/update-api-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateAPIStatusAll',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateAPIStatusAll',
description: '/docs/references/projects/update-api-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('status', null, new Boolean(), 'API status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allApis = array_keys(Config::getParam('apis'));
$apis = [];
foreach ($allApis as $api) {
$apis[$api] = $status;
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
$response->dynamic($project, Response::MODEL_PROJECT);
->action(function () {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
});
Http::patch('/v1/projects/:projectId/oauth2')
@@ -769,288 +577,6 @@ Http::delete('/v1/projects/:projectId')
$response->noContent();
});
// Keys
Http::post('/v1/projects/:projectId/keys')
->desc('Create key')
->groups(['api', 'projects'])
->label('scope', 'keys.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'createKey',
description: '/docs/references/projects/create-key.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_KEY,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
// TODO: When migrating to Platform API, mark keyId required for consistency
->param('keyId', 'unique()', fn (Database $dbForPlatform) => new CustomId($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true, ['dbForPlatform'])->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
$keyId = $keyId == 'unique()' ? ID::unique() : $keyId;
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = new Document([
'$id' => $keyId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $project->getSequence(),
'resourceId' => $project->getId(),
'resourceType' => 'projects',
'name' => $name,
'scopes' => $scopes,
'expire' => $expire,
'sdks' => [],
'accessedAt' => null,
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
]);
try {
$key = $dbForPlatform->createDocument('keys', $key);
} catch (Duplicate) {
throw new Exception(Exception::KEY_ALREADY_EXISTS);
}
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($key, Response::MODEL_KEY);
});
Http::get('/v1/projects/:projectId/keys')
->desc('List keys')
->groups(['api', 'projects'])
->label('scope', 'keys.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'listKeys',
description: '/docs/references/projects/list-keys.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_KEY_LIST,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('queries', [], new Keys(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Keys::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Backwards compatibility
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
$queries[] = Query::limit(5000);
}
$queries[] = Query::equal('resourceType', ['projects']);
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$keyId = $cursor->getValue();
$cursorDocument = $dbForPlatform->getDocument('keys', $keyId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$keys = $dbForPlatform->find('keys', $queries);
$response->dynamic(new Document([
'keys' => $keys,
'total' => $includeTotal ? $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT) : 0,
]), Response::MODEL_KEY_LIST);
});
Http::get('/v1/projects/:projectId/keys/:keyId')
->desc('Get key')
->groups(['api', 'projects'])
->label('scope', 'keys.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'getKey',
description: '/docs/references/projects/get-key.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_KEY,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$response->dynamic($key, Response::MODEL_KEY);
});
Http::put('/v1/projects/:projectId/keys/:keyId')
->desc('Update key')
->groups(['api', 'projects'])
->label('scope', 'keys.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'updateKey',
description: '/docs/references/projects/update-key.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_KEY,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$key
->setAttribute('name', $name)
->setAttribute('scopes', $scopes)
->setAttribute('expire', $expire);
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->dynamic($key, Response::MODEL_KEY);
});
Http::delete('/v1/projects/:projectId/keys/:keyId')
->desc('Delete key')
->groups(['api', 'projects'])
->label('scope', 'keys.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'keys',
name: 'deleteKey',
description: '/docs/references/projects/delete-key.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
$dbForPlatform->deleteDocument('keys', $key->getId());
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->noContent();
});
// JWT Keys
Http::post('/v1/projects/:projectId/jwts')
@@ -1093,272 +619,6 @@ Http::post('/v1/projects/:projectId/jwts')
])]), Response::MODEL_JWT);
});
// Platforms
Http::post('/v1/projects/:projectId/platforms')
->desc('Create platform')
->groups(['api', 'projects'])
->label('audits.event', 'platforms.create')
->label('audits.resource', 'project/{request.projectId}')
->label('scope', 'platforms.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'createPlatform',
description: '/docs/references/projects/create-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param(
'type',
null,
new WhiteList([
Platform::TYPE_WEB,
Platform::TYPE_FLUTTER_WEB,
Platform::TYPE_FLUTTER_IOS,
Platform::TYPE_FLUTTER_ANDROID,
Platform::TYPE_FLUTTER_LINUX,
Platform::TYPE_FLUTTER_MACOS,
Platform::TYPE_FLUTTER_WINDOWS,
Platform::TYPE_APPLE_IOS,
Platform::TYPE_APPLE_MACOS,
Platform::TYPE_APPLE_WATCHOS,
Platform::TYPE_APPLE_TVOS,
Platform::TYPE_ANDROID,
Platform::TYPE_UNITY,
Platform::TYPE_REACT_NATIVE_IOS,
Platform::TYPE_REACT_NATIVE_ANDROID,
], true),
'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, flutter-linux, flutter-macos, flutter-windows, apple-ios, apple-macos, apple-watchos, apple-tvos, android, unity, react-native-ios, react-native-android.'
)
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', true)
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
->param('hostname', '', new Hostname(), 'Platform client hostname. Max length: 256 chars.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $type, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => $type,
'name' => $name,
'key' => $key,
'store' => $store,
'hostname' => $hostname
]);
$platform = $dbForPlatform->createDocument('platforms', $platform);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM);
});
Http::get('/v1/projects/:projectId/platforms')
->desc('List platforms')
->groups(['api', 'projects'])
->label('scope', 'platforms.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'listPlatforms',
description: '/docs/references/projects/list-platforms.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_LIST,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platforms = $dbForPlatform->find('platforms', [
Query::equal('projectInternalId', [$project->getSequence()]),
Query::limit(5000),
]);
$response->dynamic(new Document([
'platforms' => $platforms,
'total' => $includeTotal ? count($platforms) : 0,
]), Response::MODEL_PLATFORM_LIST);
});
Http::get('/v1/projects/:projectId/platforms/:platformId')
->desc('Get platform')
->groups(['api', 'projects'])
->label('scope', 'platforms.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'getPlatform',
description: '/docs/references/projects/get-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]);
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$response->dynamic($platform, Response::MODEL_PLATFORM);
});
Http::put('/v1/projects/:projectId/platforms/:platformId')
->desc('Update platform')
->groups(['api', 'projects'])
->label('scope', 'platforms.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'updatePlatform',
description: '/docs/references/projects/update-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('key', '', new Text(256), 'Package name for android or bundle ID for iOS. Max length: 256 chars.', true)
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
->param('hostname', '', new Hostname(), 'Platform client URL. Max length: 256 chars.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $platformId, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]);
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$platform
->setAttribute('name', $name)
->setAttribute('key', $key)
->setAttribute('store', $store)
->setAttribute('hostname', $hostname);
$dbForPlatform->updateDocument('platforms', $platform->getId(), $platform);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM);
});
Http::delete('/v1/projects/:projectId/platforms/:platformId')
->desc('Delete platform')
->groups(['api', 'projects'])
->label('audits.event', 'platforms.delete')
->label('audits.resource', 'project/{request.projectId}/platform/${request.platformId}')
->label('scope', 'platforms.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'deletePlatform',
description: '/docs/references/projects/delete-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]);
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$dbForPlatform->deleteDocument('platforms', $platformId);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->noContent();
});
// CUSTOM SMTP and Templates
Http::patch('/v1/projects/:projectId/smtp')
->desc('Update SMTP')
+2
View File
@@ -1008,6 +1008,8 @@ Http::get('/v1/users/:userId/logs')
'userId' => ID::custom($log['data']['userId']),
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
+25 -53
View File
@@ -7,9 +7,9 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -25,6 +25,7 @@ use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -32,6 +33,7 @@ use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
@@ -61,8 +63,6 @@ use Utopia\System\System;
use Utopia\Validator;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
@@ -754,11 +754,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
if (\is_array($values)) {
$count = 0;
foreach ($values as $value) {
$override = $count === 0;
$response->addHeader($name, $value, override: $override);
$count++;
$response->addHeader($name, $value);
}
} else {
$response->addHeader($name, $values);
@@ -897,6 +894,9 @@ Http::init()
if (version_compare($requestFormat, '1.9.0', '<')) {
$request->addFilter(new RequestV21());
}
if (version_compare($requestFormat, '1.9.1', '<')) {
$request->addFilter(new RequestV22());
}
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -904,40 +904,16 @@ Http::init()
$locale->setDefault($localeParam);
}
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string)$origin);
Config::setParam(
'domainVerification',
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
$endDomain->getRegisterable() !== ''
);
$localHosts = ['localhost','localhost:'.$request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
if (!empty($migrationHost)) {
// Treat the migration host like localhost because internal migration and
// CI traffic may use it before a public domain is configured.
$localHosts[] = $migrationHost;
$localHosts[] = $migrationHost.':'.$request->getPort();
}
$isLocalHost = in_array($request->getHostname(), $localHosts);
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
$isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
Config::setParam(
'cookieDomain',
$isLocalHost || $isIpAddress
? null
: (
$isConsoleProject && $isConsoleRootSession
? '.' . $selfDomain->getRegisterable()
: '.' . $request->getHostname()
)
);
$warnings = [];
/*
@@ -945,6 +921,9 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.1', '<')) {
$response->addFilter(new ResponseV22());
}
if (version_compare($responseFormat, '1.9.0', '<')) {
$response->addFilter(new ResponseV21());
}
@@ -1035,11 +1014,11 @@ Http::init()
->inject('request')
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('platform')
->inject('authorization')
->inject('certifiedDomains')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
$hostname = $request->getHostname();
$platformHostnames = $platform['hostnames'] ?? [];
@@ -1065,7 +1044,7 @@ Http::init()
}
// 4. Check/create rule (requires DB access)
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) {
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $publisherForCertificates, $certifiedDomains) {
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
@@ -1121,10 +1100,11 @@ Http::init()
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $console,
domain: $document,
skipRenewCheck: true,
));
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {
@@ -1197,15 +1177,6 @@ Http::error()
->inject('devKey')
->inject('authorization')
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
$trace = $error->getTrace();
foreach (array_slice($trace, 0, 100) as $index => $traceEntry) {
$file = isset($traceEntry['file']) ? $traceEntry['file'] : '[internal function]';
$line = isset($traceEntry['line']) ? $traceEntry['line'] : '';
$function = isset($traceEntry['function']) ? $traceEntry['function'] : '';
Console::error("[$index] $file : $line -> $function()");
}
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
@@ -1215,9 +1186,7 @@ Http::error()
$line = $error->getLine();
$trace = $error->getTrace();
if (php_sapi_name() === 'cli') {
Span::error($error);
}
Span::error($error);
switch ($class) {
case Utopia\Http\Exception::class:
@@ -1459,6 +1428,7 @@ Http::error()
case 402: // Error allowed publicly
case 403: // Error allowed publicly
case 404: // Error allowed publicly
case 405: // Error allowed publicly
case 408: // Error allowed publicly
case 409: // Error allowed publicly
case 412: // Error allowed publicly
@@ -1500,7 +1470,9 @@ Http::error()
try {
$cors = $utopia->getResource('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response->addHeader($name, $value, override: true);
$response
->removeHeader($name)
->addHeader($name, $value);
}
} catch (Throwable) {
// Degrade gracefully - error response without CORS is no worse than before.
+74 -40
View File
@@ -3,15 +3,17 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
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;
use Appwrite\Event\Webhook;
@@ -37,6 +39,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Roles;
use Utopia\Http\Http;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
@@ -87,7 +90,7 @@ Http::init()
->inject('request')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('queueForAudits')
->inject('auditContext')
->inject('project')
->inject('user')
->inject('session')
@@ -96,8 +99,11 @@ Http::init()
->inject('team')
->inject('apiKey')
->inject('authorization')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
/**
* Handle user authentication and session validation.
@@ -183,13 +189,13 @@ Http::init()
$user = new User([
'$id' => '',
'status' => true,
'type' => ACTIVITY_TYPE_APP,
'type' => ACTIVITY_TYPE_KEY_PROJECT,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
// For standard keys, update last accessed time
@@ -253,7 +259,14 @@ Http::init()
}
}
$queueForAudits->setUser($user);
$userClone = clone $user;
$userClone->setAttribute('type', match ($apiKey->getType()) {
API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT,
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
default => ACTIVITY_TYPE_KEY_PROJECT,
});
$auditContext->user = $userClone;
}
// Apply permission
@@ -414,7 +427,7 @@ Http::init()
}
if (! empty($method)) {
$namespace = $method->getNamespace();
$namespace = \strtolower($method->getNamespace());
if (
array_key_exists($namespace, $project->getAttribute('services', []))
@@ -425,6 +438,15 @@ Http::init()
}
}
// Step 8b: Check REST protocol status
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
// Step 9: Validate scope permissions
$allowed = (array) $route->getLabel('scope', 'none');
if (empty(\array_intersect($allowed, $scopes))) {
@@ -466,7 +488,7 @@ Http::init()
->inject('user')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForAudits')
->inject('auditContext')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
@@ -483,12 +505,16 @@ Http::init()
->inject('telemetry')
->inject('platform')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, 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) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, 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) {
$response->setUser($user);
$request->setUser($user);
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$path = $route->getMatchedPath();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
@@ -496,14 +522,6 @@ Http::init()
default => '',
};
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
/*
* Abuse Check
*/
@@ -580,20 +598,21 @@ Http::init()
->setProject($project)
->setUser($user);
$queueForAudits
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($request->getIP())
->setHostname($request->getHostname())
->setEvent($route->getLabel('audits.event', ''))
->setProject($project);
$auditContext->mode = $mode;
$auditContext->userAgent = $request->getUserAgent('');
$auditContext->ip = $request->getIP();
$auditContext->hostname = $request->getHostname();
$auditContext->event = $route->getLabel('audits.event', '');
$auditContext->project = $project;
/* If a session exists, use the user associated with the session */
if (! $user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$auditContext->user = $userClone;
}
/* Auto-set projects */
@@ -617,6 +636,7 @@ Http::init()
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
$key = $request->cacheIdentifier();
Span::add('storage.cache.key', $key);
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
@@ -665,6 +685,8 @@ Http::init()
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
Span::add('storage.bucket.id', $bucketId);
Span::add('storage.file.id', $fileId);
// Do not update transformedAt if it's a console user
if (! $user->isPrivileged($authorization->getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
@@ -677,16 +699,27 @@ Http::init()
}
}
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
'accessedAt' => DateTime::now(),
])));
// Refresh the filesystem file's mtime so TTL-based expiry in cache->load() stays valid
$cache->save($key, $data);
}
$response
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'));
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
if (! $isImageTransformation || ! $isDisabled) {
Span::add('storage.cache.hit', true);
$response->send($data);
}
} else {
$storageCacheOperationsCounter->add(1, ['result' => 'miss']);
Span::add('storage.cache.hit', false);
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Pragma', 'no-cache')
@@ -758,7 +791,8 @@ Http::shutdown()
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForAudits')
->inject('auditContext')
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForDeletes')
@@ -774,7 +808,8 @@ Http::shutdown()
->inject('eventProcessor')
->inject('bus')
->inject('apiKey')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey) use ($parseLabel) {
->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, Build $queueForBuilds, Messaging $queueForMessaging, 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();
@@ -869,16 +904,18 @@ Http::shutdown()
if (! empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
if (! empty($resource) && $resource !== $pattern) {
$queueForAudits->setResource($resource);
$auditContext->resource = $resource;
}
}
if (! $user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$auditContext->user = $userClone;
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
/**
* User in the request is empty, and no user was set for auditing previously.
* This indicates:
@@ -896,24 +933,21 @@ Http::shutdown()
'name' => 'Guest',
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
$auditUser = $auditContext->user;
if (! empty($auditContext->resource) && ! \is_null($auditUser) && ! $auditUser->isEmpty()) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route->getLabel('audits.payload', true);
if (! empty($pattern)) {
$queueForAudits->setPayload($responsePayload);
$auditContext->payload = $responsePayload;
}
foreach ($queueForEvents->getParams() as $key => $value) {
$queueForAudits->setParam($key, $value);
}
$queueForAudits->trigger();
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDeletes->getType())) {
+56 -54
View File
@@ -1,16 +1,13 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
global $register;
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Constant;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Http\Server;
use Swoole\Process;
use Swoole\Table;
use Swoole\Timer;
@@ -29,6 +26,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
use Utopia\Logger\Log;
@@ -49,18 +47,33 @@ $certifiedDomains = new Table(100_000);
$certifiedDomains->column('value', Table::TYPE_INT, 1);
$certifiedDomains->create();
Http::setResource('riskyDomains', fn () => $riskyDomains);
Http::setResource('certifiedDomains', fn () => $certifiedDomains);
$http = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
mode: SWOOLE_PROCESS,
);
global $container;
$container->set('riskyDomains', fn () => $riskyDomains);
$container->set('certifiedDomains', fn () => $certifiedDomains);
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['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(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
Constant::OPTION_WORKER_NUM => $totalWorkers,
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
Constant::OPTION_HTTP_COMPRESSION => false,
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
container: $container,
);
$http = $swooleAdapter->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
*
@@ -69,16 +82,16 @@ $totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intva
* riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary.
* doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func
*
* @param Server $server Swoole server instance.
* @param \Swoole\Http\Server $server Swoole server instance.
* @param int $fd client ID
* @param int $type the type of data and its current state
* @param string|null $data Request content for categorization.
* @global int $totalThreads Total number of workers.
* @return int Chosen worker ID for the request.
*/
function dispatch(Server $server, int $fd, int $type, $data = null): int
function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null): int
{
$resolveWorkerId = function (Server $server, $data = null) {
$resolveWorkerId = function (\Swoole\Http\Server $server, $data = null) {
global $totalWorkers, $riskyDomains;
// If data is not set we can send request to any worker
@@ -161,18 +174,6 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
return $workerId;
}
$http
->set([
Constant::OPTION_WORKER_NUM => $totalWorkers,
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
Constant::OPTION_HTTP_COMPRESSION => false,
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
]);
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
});
@@ -189,16 +190,14 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
Http::setResource('bus', function ($register, $utopia) {
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
}, ['register', 'utopia']);
$container->set('bus', function ($register) use ($swooleAdapter) {
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
}, ['register']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
$max = 15;
$sleep = 2;
$attempts = 0;
@@ -289,13 +288,13 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
Span::current()?->finish();
}
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register) {
$app = new Http('UTC');
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
$app = new Http($swooleAdapter, 'UTC');
go(function () use ($register, $app) {
$pools = $register->get('pools');
/** @var \Utopia\Pools\Group $pools */
Http::setResource('pools', fn () => $pools);
/** @var \Utopia\Pools\Group $pools */
$pools = $app->getResource('pools');
go(function () use ($app, $pools) {
/** @var array $collections */
$collections = Config::getParam('collections', []);
@@ -511,14 +510,11 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
});
});
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register, $files) {
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
Span::init('http.request');
Http::setResource('swooleRequest', fn () => $swooleRequest);
Http::setResource('swooleResponse', fn () => $swooleResponse);
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);
$request = new Request($utopiaRequest->getSwooleRequest());
$response = new Response($utopiaResponse->getSwooleResponse());
Span::add('http.method', $request->getMethod());
@@ -534,13 +530,19 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
return;
}
$app = new Http('UTC');
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
$app = new Http($swooleAdapter, 'UTC');
$requestContainer->set('utopia', fn () => $app);
$registerRequestResources($requestContainer);
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
$pools = $register->get('pools');
Http::setResource('pools', fn () => $pools);
try {
$authorization = $app->getResource('authorization');
@@ -624,6 +626,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
}
}
$swooleResponse = $utopiaResponse->getSwooleResponse();
$swooleResponse->setStatusCode(500);
$output = ((Http::isDevelopment())) ? [
@@ -647,11 +650,10 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on(Constant::EVENT_TASK, function () use ($register) {
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
$lastSyncUpdate = null;
$pools = $register->get('pools');
Http::setResource('pools', fn () => $pools);
$app = new Http('UTC');
$app = new Http($swooleAdapter, 'UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
@@ -726,4 +728,4 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
});
});
$http->start();
$swooleAdapter->start();
+1 -1
View File
@@ -12,7 +12,7 @@ Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapt
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
Config::load('protocols', __DIR__ . '/../config/protocols.php', $configAdapter);
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
Config::load('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
+6 -3
View File
@@ -46,8 +46,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4321;
const APP_VERSION_STABLE = '1.9.0';
const APP_CACHE_BUSTER = 4322;
const APP_VERSION_STABLE = '1.9.1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -156,9 +156,12 @@ const SESSION_PROVIDER_SERVER = 'server';
/**
* Activity associated with user or the app.
*/
const ACTIVITY_TYPE_APP = 'app';
const ACTIVITY_TYPE_USER = 'user';
const ACTIVITY_TYPE_ADMIN = 'admin';
const ACTIVITY_TYPE_GUEST = 'guest';
const ACTIVITY_TYPE_KEY_PROJECT = 'keyProject';
const ACTIVITY_TYPE_KEY_ACCOUNT = 'keyAccount';
const ACTIVITY_TYPE_KEY_ORGANIZATION = 'keyOrganization';
/**
* MFA
+8 -1
View File
@@ -1,5 +1,6 @@
<?php
use Appwrite\Network\Platform;
use Appwrite\OpenSSL\OpenSSL;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -123,11 +124,17 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
$platforms = $database->getAuthorization()->skip(fn () => $database
->find('platforms', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
foreach ($platforms as $platform) {
$platform->setAttribute('type', Platform::mapDeprecatedType($platform->getAttribute('type')));
}
return $platforms;
}
);
+12 -3
View File
@@ -106,7 +106,12 @@ use Appwrite\Utopia\Response\Model\Mock;
use Appwrite\Utopia\Response\Model\MockNumber;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\PlatformAndroid;
use Appwrite\Utopia\Response\Model\PlatformApple;
use Appwrite\Utopia\Response\Model\PlatformLinux;
use Appwrite\Utopia\Response\Model\PlatformList;
use Appwrite\Utopia\Response\Model\PlatformWeb;
use Appwrite\Utopia\Response\Model\PlatformWindows;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
@@ -197,7 +202,6 @@ Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, '
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
Response::setModel(new BaseList('Countries List', Response::MODEL_COUNTRY_LIST, 'countries', Response::MODEL_COUNTRY));
Response::setModel(new BaseList('Continents List', Response::MODEL_CONTINENT_LIST, 'continents', Response::MODEL_CONTINENT));
Response::setModel(new BaseList('Languages List', Response::MODEL_LANGUAGE_LIST, 'languages', Response::MODEL_LANGUAGE));
@@ -333,7 +337,12 @@ Response::setModel(new Key());
Response::setModel(new DevKey());
Response::setModel(new MockNumber());
Response::setModel(new AuthProvider());
Response::setModel(new Platform());
Response::setModel(new PlatformWeb());
Response::setModel(new PlatformApple());
Response::setModel(new PlatformAndroid());
Response::setModel(new PlatformWindows());
Response::setModel(new PlatformLinux());
Response::setModel(new PlatformList());
Response::setModel(new Variable());
Response::setModel(new Country());
Response::setModel(new Continent());
+366
View File
@@ -0,0 +1,366 @@
<?php
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Database\DateTime as DatabaseDateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\System\System;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
/**
* Register the minimal per-connection resources required by realtime.
*/
return function (Container $container): void {
$getProjectId = static function (Request $request): string {
$projectId = $request->getHeader('x-appwrite-project', '');
if (!empty($projectId)) {
return $projectId;
}
$projectId = $request->getParam('project', '');
return \is_string($projectId) ? $projectId : '';
};
$getMode = static function (Request $request, Document $project) use ($getProjectId): string {
$mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
$projectId = $getProjectId($request);
if (!empty($projectId) && $project->getId() !== $projectId) {
$mode = APP_MODE_ADMIN;
}
return $mode;
};
$getDbForPlatform = static function (Authorization $authorization) {
$database = getConsoleDB();
$database->setAuthorization($authorization);
return $database;
};
$getDbForProject = static function (Document $project, Authorization $authorization) use ($getDbForPlatform) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $getDbForPlatform($authorization);
}
$database = getProjectDB($project);
$database->setAuthorization($authorization);
return $database;
};
$findRule = static function (Request $request, Document $project, Authorization $authorization) use ($getDbForPlatform): Document {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
$domain = \parse_url($request->getReferer(), PHP_URL_HOST);
}
if (empty($domain)) {
return new Document();
}
$dbForPlatform = $getDbForPlatform($authorization);
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) {
if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($domain));
}
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]) ?? new Document();
});
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) {
$trustedProjects = [];
foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) {
if (empty($trustedProject)) {
continue;
}
$trustedProjects[] = $trustedProject;
}
if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects, true)) {
$permitsCurrentProject = true;
}
}
if (!$permitsCurrentProject) {
return new Document();
}
return $rule;
};
$findDevKey = static function (Request $request, Document $project, array $servers, Authorization $authorization) use ($getDbForPlatform): Document {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
$key = $project->find('secret', $devKey, 'devKeys');
if (!$key) {
return new Document([]);
}
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
return new Document([]);
}
$dbForPlatform = $getDbForPlatform($authorization);
$accessedAt = $key->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'accessedAt' => $key->getAttribute('accessedAt'),
])));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
$sdkValidator = new WhiteList($servers, true);
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (!\in_array($sdk, $sdks, true)) {
$sdks[] = $sdk;
$key->setAttribute('sdks', $sdks);
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'sdks' => $key->getAttribute('sdks'),
'accessedAt' => $key->getAttribute('accessedAt'),
])));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
return $key;
};
$container->set('authorization', function () {
return new Authorization();
}, []);
$container->set('project', function (Request $request, Document $console, Authorization $authorization) use ($getProjectId, $getDbForPlatform) {
$projectId = $getProjectId($request);
if (empty($projectId) || $projectId === 'console') {
return $console;
}
$dbForPlatform = $getDbForPlatform($authorization);
return $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
}, ['request', 'console', 'authorization']);
$container->set('originValidator', function (array $platform, Request $request, Document $project, array $servers, Authorization $authorization) use ($findDevKey, $findRule) {
$devKey = $findDevKey($request, $project, $servers, $authorization);
if (!$devKey->isEmpty()) {
return new URL();
}
$allowedHostnames = [...($platform['hostnames'] ?? [])];
if (!$project->isEmpty() && $project->getId() !== 'console') {
$allowedHostnames = [...$allowedHostnames, ...Platform::getHostnames($project->getAttribute('platforms', []))];
}
$rule = $findRule($request, $project, $authorization);
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
$allowedHostnames[] = $rule->getAttribute('domain', '');
}
$originHostname = \parse_url($request->getOrigin(), PHP_URL_HOST);
$refererHostname = \parse_url($request->getReferer(), PHP_URL_HOST);
$hostname = $originHostname ?: $refererHostname;
if ($request->getMethod() === 'OPTIONS' && !empty($hostname)) {
$allowedHostnames[] = $hostname;
}
$allowedSchemes = [...($platform['schemas'] ?? [])];
if (!$project->isEmpty() && $project->getId() !== 'console') {
$allowedSchemes[] = 'exp';
$allowedSchemes[] = 'appwrite-callback-' . $project->getId();
$allowedSchemes = [...$allowedSchemes, ...Platform::getSchemes($project->getAttribute('platforms', []))];
}
return new Origin(\array_unique($allowedHostnames), \array_unique($allowedSchemes));
}, ['platform', 'request', 'project', 'servers', 'authorization']);
$container->set('user', function (Request $request, Document $project, Document $console, Authorization $authorization) use ($getMode, $getDbForPlatform, $getDbForProject) {
$mode = $getMode($request, $project);
$store = new Store();
$proofForToken = new Token();
$proofForToken->setHash(new Sha());
$authorization->setDefaultStatus(true);
$dbForPlatform = $getDbForPlatform($authorization);
$dbForProject = $getDbForProject($project, $authorization);
$store->setKey('a_session_' . $project->getId());
if ($mode === APP_MODE_ADMIN) {
$store->setKey('a_session_' . $console->getId());
}
$store->decode(
$request->getCookie(
$store->getKey(),
$request->getCookie($store->getKey() . '_legacy', '')
)
);
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (!empty($sessionHeader)) {
$store->decode($sessionHeader);
}
}
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$fallback = \json_decode($request->getHeader('x-fallback-cookies', ''), true);
$store->decode((\is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '');
}
$user = null;
if ($mode === APP_MODE_ADMIN) {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
if ($project->isEmpty()) {
$user = new User([]);
} elseif (!empty($store->getProperty('id', ''))) {
if ($project->getId() === 'console') {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
/** @var User $user */
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
}
}
}
if (
!$user
|| $user->isEmpty()
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
) {
$user = new User([]);
}
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (!empty($authJWT) && !$project->isEmpty()) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
}
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
$user = $dbForPlatform->getDocument('users', $jwtUserId);
} else {
$user = $dbForProject->getDocument('users', $jwtUserId);
}
}
$jwtSessionId = $payload['sessionId'] ?? '';
if (!empty($jwtSessionId) && empty($user->find('$id', $jwtSessionId, 'sessions'))) {
$user = new User([]);
}
}
$accountKey = $request->getHeader('x-appwrite-key', '');
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
if (!empty($accountKeyUserId) && !empty($accountKey)) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
$accountKeyUser = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
if (!$accountKeyUser->isEmpty()) {
$key = $accountKeyUser->find(
key: 'secret',
find: $accountKey,
subject: 'keys'
);
if (!empty($key)) {
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
}
$user = $accountKeyUser;
}
}
}
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
$userDb = ($mode === APP_MODE_ADMIN || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
$targetUser = null;
if (!empty($impersonateUserId)) {
$targetUser = $authorization->skip(fn () => $userDb->getDocument('users', $impersonateUserId));
} elseif (!empty($impersonateEmail)) {
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
Query::equal('email', [\strtolower($impersonateEmail)]),
]));
} elseif (!empty($impersonatePhone)) {
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
Query::equal('phone', [$impersonatePhone]),
]));
}
if ($targetUser !== null && !$targetUser->isEmpty()) {
$impersonator = clone $user;
$user = clone $targetUser;
$user->setAttribute('impersonatorUserId', $impersonator->getId());
$user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence());
$user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', ''));
$user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', ''));
$user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0));
}
}
$dbForPlatform->setMetadata('user', $user->getId());
$dbForProject->setMetadata('user', $user->getId());
return $user;
}, ['request', 'project', 'console', 'authorization']);
};
+2 -13
View File
@@ -245,19 +245,8 @@ $register->set('pools', function () {
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
if ($multiprocessing) {
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
} else {
$workerCount = 1;
}
if ($workerCount > $instanceConnections) {
throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500);
}
$poolSize = (int)($instanceConnections / $workerCount);
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$poolSize = max(1, (int)($instanceConnections / $workerCount));
foreach ($connections as $key => $connection) {
$type = $connection['type'] ?? '';
+68 -1372
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+436
View File
@@ -0,0 +1,436 @@
<?php
use Appwrite\Event\Build;
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;
use Appwrite\Utopia\Database\Documents\User;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Cache;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
/**
* Register per-job resources on the given container.
* These resources depend on the queue message or keep mutable state and
* must be fresh for each worker job.
*/
return function (Container $container): void {
$container->set('log', fn () => new Log(), []);
$container->set('usage', fn () => new Context(), []);
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
$container->set('dbForPlatform', function (Cache $cache, Group $pools, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class);
return $dbForPlatform;
}, ['cache', 'pools', 'authorization']);
$container->set('project', function ($message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
if ($project->isEmpty() || $project->getId() === 'console') {
return $project;
}
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
$container->set('dbForProject', function (Cache $cache, Group $pools, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'pools', 'project', 'dbForPlatform', 'authorization']);
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
return $database;
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$container->set('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
$projectDocument ??= $project;
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
// Backwards-compatibility: older or seeded legacy databases may not have a DSN stored
// in the "database" attribute. In that case, fall back to the project's database DSN.
if ($databaseDSN === '') {
$databaseDSN = $projectDocument->getAttribute('database', '');
}
try {
$databaseDSN = new DSN($databaseDSN);
} catch (\InvalidArgumentException) {
$databaseDSN = new DSN('mysql://' . $databaseDSN);
}
try {
$dsn = new DSN($projectDocument->getAttribute('database'));
} catch (\InvalidArgumentException) {
// Temporary fallback until all projects use shared tables
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
}
$pools = $register->get('pools');
$databaseHost = $databaseDSN->getHost();
$pool = $pools->get($databaseHost);
$adapter = new DatabasePool($pool);
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization);
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
if ($databaseHost !== $dsn->getHost()) {
$dbTypeSharedTables = match ($databaseType) {
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
default => [],
};
if (\in_array($databaseHost, $dbTypeSharedTables)) {
$database
->setSharedTables(true)
->setTenant($projectDocument->getSequence())
->setNamespace($databaseDSN->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant($projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['cache', 'register', 'project', 'authorization']);
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
$adapter = new DatabasePool($pools->get('logs'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
};
}, ['pools', 'cache', 'authorization']);
$container->set('abuseRetention', function () {
return \time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
}, []);
$container->set('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
$container->set('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
}, []);
$container->set('queueForDatabase', function (Publisher $publisher) {
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('queueForBuilds', function (Publisher $publisher) {
return new Build($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('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFiles', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForCache', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', \get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
foreach (($extras ?? []) as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
}
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
}
};
}, ['register', 'project']);
$container->set('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
};
+2
View File
@@ -1,9 +1,11 @@
<?php
use Appwrite\Bus\Listeners\Log;
use Appwrite\Bus\Listeners\Mails;
use Appwrite\Bus\Listeners\Usage;
return [
new Log(),
new Mails(),
new Usage(),
];
+162 -58
View File
@@ -33,8 +33,8 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
@@ -43,11 +43,13 @@ use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\WebSocket\Adapter;
use Utopia\WebSocket\Server;
/**
* @var Registry $register
*/
require_once __DIR__ . '/init.php';
/** @var Registry $register */
$register = $GLOBALS['register'] ?? throw new \RuntimeException('Registry not initialized');
$registerConnectionResources ??= require __DIR__ . '/init/realtime/connection.php';
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
// Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump
@@ -240,6 +242,11 @@ if (!function_exists('triggerStats')) {
}
}
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
$realtime = getRealtime();
/**
@@ -383,22 +390,6 @@ $server->onStart(function () use ($stats, $containerId, &$statsDocument) {
}
});
function cloudRealtimeLogConnectionHostnames(Http $app, Document $project, Request $request): void
{
try {
/** @var array<int, string> $allowed */
$allowed = $app->getResource('allowedHostnames');
Console::info(sprintf(
'[Realtime] project=%s origin=%s allowedHostnames=%s',
$project->getId(),
$request->getOrigin(),
json_encode(array_values($allowed))
));
} catch (Throwable $e) {
Console::error('[Realtime] allowedHostnames log failed: ' . $e->getMessage());
}
}
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
Console::success('Worker ' . $workerId . ' started successfully');
@@ -457,7 +448,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
]
];
$server->send($realtime->getSubscribers($event), json_encode([
$server->send(array_keys($realtime->getSubscribers($event)), json_encode([
'type' => 'event',
'data' => $event['data']
]));
@@ -564,7 +555,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$receivers = $realtime->getSubscribers($event);
if (Http::isDevelopment() && !empty($receivers)) {
if (System::getEnv('_APP_ENV', 'production') === 'development' && !empty($receivers)) {
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
Console::log("[Debug][Worker {$workerId}] Connection IDs: " . json_encode(array_keys($receivers)));
Console::log("[Debug][Worker {$workerId}] Matched: " . json_encode(array_values($receivers)));
@@ -630,16 +621,17 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Failed to restart pub/sub...');
});
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
$app = new Http('UTC');
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerConnectionResources) {
global $container;
$request = new Request($request);
$response = new Response(new SwooleResponse());
Console::info("Connection open (user: {$connection})");
Http::setResource('pools', fn () => $register->get('pools'));
Http::setResource('request', fn () => $request);
Http::setResource('response', fn () => $response);
$connectionContainer = new Container($container);
$connectionContainer->set('request', fn () => $request);
$registerConnectionResources($connectionContainer);
$project = null;
$logUser = null;
@@ -647,8 +639,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
try {
/** @var Document $project */
$project = $app->getResource('project');
$authorization = $app->getResource('authorization');
$project = $connectionContainer->get('project');
$authorization = $connectionContainer->get('authorization');
/*
* Project Check
@@ -657,13 +649,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
}
$timelimit = $app->getResource('timelimit');
$user = $app->getResource('user'); /** @var User $user */
$timelimit = $connectionContainer->get('timelimit');
$user = $connectionContainer->get('user'); /** @var User $user */
$logUser = $user;
$apis = $project->getAttribute('apis', []);
// Websocket is what to check, but realtime is checked too for backwards compatibility
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
!$websocketEnabled
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
@@ -703,10 +697,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
* Skip this check for non-web platforms which are not required to send an origin header.
*/
$origin = $request->getOrigin();
$originValidator = $app->getResource('originValidator');
cloudRealtimeLogConnectionHostnames($app, $project, $request);
$originValidator = $connectionContainer->get('originValidator');
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
@@ -716,11 +707,43 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
$updateStats = static function (string $projectId, ?string $teamId, string $payloadJson) use ($register, $stats): void {
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($projectId, [
'projectId' => $projectId,
'teamId' => $teamId
]);
$stats->incr($projectId, 'connections');
$stats->incr($projectId, 'connectionsTotal');
triggerStats([
METRIC_REALTIME_CONNECTIONS => 1,
METRIC_REALTIME_OUTBOUND => \strlen($payloadJson),
], $projectId);
};
/**
* Channels Check
*/
if (empty($channels)) {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
// in case of message based 'subscribe' channels will be empty at first and only projectId and roles will be available
$sanitizedUser = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$connectedPayloadJson = json_encode([
'type' => 'connected',
'data' => [
'channels' => [],
'subscriptions' => [],
'user' => $sanitizedUser
]
]);
$realtime->subscribe($project->getId(), $connection, '', $roles, [], [], $user->getId());
$realtime->connections[$connection]['authorization'] = $authorization;
$server->send([$connection], $connectedPayloadJson);
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
return;
}
$names = array_keys($channels);
@@ -744,7 +767,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$subscriptionId,
$roles,
$subscription['channels'],
$subscription['queries']
$subscription['queries'],
$user->getId()
);
$mapping[$index] = $subscriptionId;
@@ -764,20 +788,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]);
$server->send([$connection], $connectedPayloadJson);
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($project->getId(), [
'projectId' => $project->getId(),
'teamId' => $project->getAttribute('teamId')
]);
$stats->incr($project->getId(), 'connections');
$stats->incr($project->getId(), 'connectionsTotal');
$connectedOutboundBytes = \strlen($connectedPayloadJson);
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
} catch (Throwable $th) {
@@ -793,7 +804,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
// sanitize 0 && 5xx errors
$realtimeViolation = $th instanceof AppwriteException && $th->getType() === AppwriteException::REALTIME_POLICY_VIOLATION;
if (($code === 0 || $code >= 500) && !$realtimeViolation && !Http::isDevelopment()) {
if (($code === 0 || $code >= 500) && !$realtimeViolation && System::getEnv('_APP_ENV', 'production') !== 'development') {
$message = 'Error: Server Error';
}
@@ -808,7 +819,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$server->send([$connection], json_encode($response));
$server->close($connection, $code);
if (Http::isDevelopment()) {
if (System::getEnv('_APP_ENV', 'production') === 'development') {
Console::error('[Error] Connection Error');
Console::error('[Error] Code: ' . $response['data']['code']);
Console::error('[Error] Message: ' . $response['data']['message']);
@@ -819,7 +830,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
$project = null;
$authorization = null;
try {
$rawSize = \strlen($message);
$response = new Response(new SwooleResponse());
@@ -945,7 +955,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$subscriptionId,
$roles,
$subscription['channels'] ?? [],
$queries
$queries,
$user->getId()
);
}
}
@@ -979,6 +990,99 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
break;
case 'subscribe':
/**
* Message based upsertion of a subscription
* If subscriptionId is given then it will match subId of the connection and update the subscription with channels and queries
* If non-existing subid is given or not given a new subid will be generated
* Similar to what we have now -> two subscribe() block with same channels and queries still two different subscriptions
*
* structure of the payload -> array of maps
* 'data' : [subscriptionId:"" , channels:[] , queries:[]]
*/
if (!is_array($message['data']) || !array_is_list($message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$roles = $realtime->connections[$connection]['roles'] ?? [Role::guests()->toString()];
$userId = $realtime->connections[$connection]['userId'] ?? '';
// bulk validation + parsing before subscribing
$parsedPayloads = [];
foreach ($message['data'] as $payload) {
if (!\is_array($payload)) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each subscribe payload must be an object.');
}
if (!array_key_exists('channels', $payload)) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not present in payload.');
}
if (!is_array($payload['channels']) || !array_is_list($payload['channels'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not a valid array.');
}
// registering the queries if not present and check in the same payload later on
if (!array_key_exists('queries', $payload)) {
$payload['queries'] = [];
}
if (!is_array($payload['queries']) || !array_is_list($payload['queries'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'queries is not a valid array.');
}
$subscriptionId = \array_key_exists('subscriptionId', $payload)
? $payload['subscriptionId']
: ID::unique();
try {
$convertedQueries = Realtime::convertQueries($payload['queries']);
} catch (QueryException $e) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Invalid query: ' . $e->getMessage());
}
$parsedPayloads[] = [
'subscriptionId' => $subscriptionId,
'channels' => $payload['channels'],
'queries' => $convertedQueries,
];
}
foreach ($parsedPayloads as $parsedPayload) {
$subscriptionId = $parsedPayload['subscriptionId'];
$channels = \array_keys(Realtime::convertChannels($parsedPayload['channels'], $userId));
$queries = $parsedPayload['queries'];
$realtime->subscribe($projectId, $connection, $subscriptionId, $roles, $channels, $queries);
}
// subscribe() overwrites the connection entry; restore auth so later onMessage uses the same context.
$realtime->connections[$connection]['authorization'] = $authorization;
$responsePayload = json_encode([
'type' => 'response',
'data' => [
'to' => 'subscribe',
'success' => true,
'subscriptions' => \array_map(function (array $parsedPayload) {
return [
'subscriptionId' => $parsedPayload['subscriptionId'],
'channels' => $parsedPayload['channels'],
'queries' => \array_map(fn ($q) => $q->toString(), $parsedPayload['queries']),
];
}, $parsedPayloads),
]
]);
$server->send([$connection], $responsePayload);
if ($project !== null && !$project->isEmpty()) {
$subscribeOutboundBytes = \strlen($responsePayload);
if ($subscribeOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $subscribeOutboundBytes,
], $project->getId());
}
}
break;
default:
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
@@ -992,7 +1096,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$message = $th->getMessage();
// sanitize 0 && 5xx errors
if (($code === 0 || $code >= 500) && !Http::isDevelopment()) {
if (($code === 0 || $code >= 500) && System::getEnv('_APP_ENV', 'production') !== 'development') {
$message = 'Error: Server Error';
}
+30 -1
View File
@@ -532,6 +532,35 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
appwrite-worker-executions:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-executions
<<: *x-logging
container_name: appwrite-worker-executions
restart: unless-stopped
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_ENV
- _APP_DB_ADAPTER
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_CONFIG
appwrite-worker-functions:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-functions
@@ -964,7 +993,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
<<: *x-logging
restart: unless-stopped
stop_signal: SIGINT
image: openruntimes/executor:0.7.22
image: openruntimes/executor:0.11.4
networks:
- appwrite
- runtimes
+42 -530
View File
@@ -1,564 +1,69 @@
<?php
require_once __DIR__ . '/init.php';
$registerWorkerMessageResources = require __DIR__ . '/init/worker/message.php';
use Appwrite\Certificates\LetsEncrypt;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
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\Migration;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Adapter\Swoole;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Message;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Queue\Server;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
Runtime::enableCoroutine();
require_once __DIR__ . '/init/span.php';
global $register;
Server::setResource('register', fn () => $register);
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
Server::setResource('authorization', function () {
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
$pools = $register->get('pools');
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$container->set('project', fn () => new Document([]), []);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class);
$container->set('log', fn () => new Log(), []);
return $dbForPlatform;
}, ['cache', 'register', 'authorization']);
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
if ($project->getId() === 'console') {
return $project;
}
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
$pools = $register->get('pools');
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
return $database;
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
$adapter = new DatabasePool($pools->get('logs'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
};
}, ['pools', 'cache', 'authorization']);
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
$projectDocument ??= $project;
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
// Backwardscompatibility: older or seeded legacy databases may not have a DSN stored
// in the "database" attribute. In that case, fall back to the project's database DSN.
if ($databaseDSN === '') {
$databaseDSN = $projectDocument->getAttribute('database', '');
}
try {
$databaseDSN = new DSN($databaseDSN);
} catch (\InvalidArgumentException) {
$databaseDSN = new DSN('mysql://'.$databaseDSN);
}
try {
$dsn = new DSN($projectDocument->getAttribute('database'));
} catch (\InvalidArgumentException) {
// Temporary fallback until all projects use shared tables
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
}
$pools = $register->get('pools');
$pool = $pools->get($databaseDSN->getHost());
$adapter = new DatabasePool($pool);
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization);
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant((int) $projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['cache', 'register', 'project', 'authorization']);
Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
});
Server::setResource('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
Server::setResource('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
});
Server::setResource('cache', function (Registry $register) {
$pools = $register->get('pools');
$list = Config::getParam('pools-cache', []);
$adapters = [];
foreach ($list as $value) {
$adapters[] = new CachePool($pools->get($value));
}
return new Cache(new Sharding($adapters));
}, ['register']);
Server::setResource('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int) $port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});
Server::setResource('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
};
}, ['redis']);
Server::setResource('log', fn () => new Log());
Server::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
Server::setResource('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('consumer', function (Group $pools) {
$container->set('consumer', function (Group $pools) {
return new BrokerPool(consumer: $pools->get('consumer'));
}, ['pools']);
Server::setResource('consumerDatabases', function (BrokerPool $consumer) {
$container->set('consumerDatabases', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('consumerMigrations', function (BrokerPool $consumer) {
$container->set('consumerMigrations', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
$container->set('consumerStatsUsage', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('usage', function () {
return new Context();
}, []);
Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
Server::setResource('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
Server::setResource('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
Server::setResource('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
Server::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
Server::setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
Server::setResource('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
Server::setResource('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
Server::setResource('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
Server::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Server::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
Server::setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
Server::setResource('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
Server::setResource('logger', function (Registry $register) {
return $register->get('logger');
}, ['register']);
Server::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
Server::setResource('telemetry', fn () => new NoTelemetry());
Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
Server::setResource('plan', function (array $plan = []) {
return [];
});
Server::setResource('certificates', function () {
$container->set('certificates', function () {
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue a LetsEncrypt SSL certificate.');
}
return new LetsEncrypt($email);
});
}, []);
Server::setResource('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
foreach (($extras ?? []) as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
}
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
}
};
}, ['register', 'project']);
Server::setResource('executor', fn () => new Executor());
Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');
$args = $_SERVER['argv'] ?? [];
if (! isset($args[1])) {
Console::error('Missing worker name');
@@ -574,38 +79,45 @@ if (\str_starts_with($workerName, 'databases')) {
$queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName));
}
/** @var \Utopia\Pools\Group $pools */
$pools = $container->get('pools');
$adapter = new Swoole(
$pools->get('consumer')->pop()->getResource(),
System::getEnv('_APP_WORKERS_NUM', 1),
$queueName
);
$worker = new Server($adapter, $container);
try {
/**
* Any worker can be configured with the following env vars:
* - _APP_WORKERS_NUM The total number of worker processes
* - _APP_WORKER_PER_CORE The number of worker processes per core (ignored if _APP_WORKERS_NUM is set)
* - _APP_QUEUE_NAME The name of the queue to read for database events
*/
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
$registerWorkerMessageResources($worker->getContainer());
});
$container->set('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(
fn (string $name) => $worker->getContainer()->get($name)
);
}, ['register']);
$platform->setWorker($worker);
$platform->init(Service::TYPE_WORKER, [
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
'connection' => $pools->get('consumer')->pop()->getResource(),
'workerName' => strtolower($workerName) ?? null,
'queueName' => $queueName,
'workerName' => strtolower($workerName),
]);
} catch (\Throwable $e) {
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
Console::exit(1);
}
$worker = $platform->getWorker();
Server::setResource('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
}, ['register']);
$worker
->error()
->inject('error')
->inject('logger')
->inject('log')
->inject('pools')
->inject('project')
->inject('authorization')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) {
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if ($logger) {
+8 -9
View File
@@ -52,34 +52,34 @@
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.2.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "2.2.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "1.0.*",
"utopia-php/cli": "0.22.*",
"utopia-php/cli": "0.23.*",
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
"utopia-php/agents": "1.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "1.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.6.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/http": "0.34.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/platform": "0.12.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.15.*",
"utopia-php/servers": "0.2.5",
"utopia-php/queue": "0.17.*",
"utopia-php/servers": "0.3.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "1.0.*",
"utopia-php/system": "0.10.*",
@@ -92,10 +92,9 @@
"chillerlan/php-qrcode": "4.3.*",
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"webonyx/graphql-php": "15.31.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*",
"utopia-php/di": "0.1.0"
"enshrined/svg-sanitize": "0.22.*"
},
"require-dev": {
"ext-fileinfo": "*",
Generated
+223 -216
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1288,6 +1288,7 @@ services:
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
restart: on-failure:3
networks:
- appwrite
volumes:
+320
View File
@@ -0,0 +1,320 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Appwrite\OpenSSL\OpenSSL;
use Utopia\System\System;
// Reference Material
// https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code
// https://docs.x.com/x-api/users/get-me
class X extends OAuth2
{
private const PKCE_STATE_KEY = '_pkce';
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [
'tweet.read',
'users.read',
'users.email',
'offline.access',
];
/**
* @var string
*/
private string $pkceVerifier = '';
/**
* @return string
*/
public function getName(): string
{
return 'x';
}
public function getLoginURL(): string
{
$state = $this->state;
$state[self::PKCE_STATE_KEY] = $this->encryptPKCEVerifier($this->getPKCEVerifier());
return 'https://x.com/i/oauth2/authorize?' . \http_build_query([
'response_type' => 'code',
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => $this->base64UrlEncode(\json_encode($state, JSON_THROW_ON_ERROR)),
'code_challenge' => $this->getPKCEChallenge(),
'code_challenge_method' => 'S256',
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$this->tokens = $this->decodeJsonObject($this->request(
'POST',
'https://api.x.com/2/oauth2/token',
$this->tokenEndpointHeaders(),
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'grant_type' => 'authorization_code',
'redirect_uri' => $this->callback,
'code_verifier' => $this->getPKCEVerifier(),
])
));
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$this->tokens = $this->decodeJsonObject($this->request(
'POST',
'https://api.x.com/2/oauth2/token',
$this->tokenEndpointHeaders(),
\http_build_query([
'client_id' => $this->appID,
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token',
])
));
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['data']['id'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['data']['confirmed_email'] ?? '';
}
/**
* Check if the OAuth email is verified.
*
* X returns a confirmed email only when the app has email access enabled
* and the authenticated user has a confirmed email address.
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
return !empty($this->getUserEmail($accessToken));
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['data']['name'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$this->user = $this->decodeJsonObject($this->request(
'GET',
'https://api.x.com/2/users/me?user.fields=confirmed_email',
['Authorization: Bearer ' . $accessToken]
));
}
return $this->user;
}
/**
* @return array<string, mixed>|null
*/
public function parseState(string $state): ?array
{
$decoded = $this->base64UrlDecode($state);
if ($decoded === false) {
return null;
}
$parsed = \json_decode($decoded, true);
if (!\is_array($parsed)) {
return null;
}
$pkce = $parsed[self::PKCE_STATE_KEY] ?? null;
if (\is_array($pkce)) {
$this->pkceVerifier = $this->decryptPKCEVerifier($pkce);
}
unset($parsed[self::PKCE_STATE_KEY]);
return $parsed;
}
/**
* @return list<string>
*/
private function tokenEndpointHeaders(): array
{
return [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
}
/**
* @return array<string, mixed>
*/
private function decodeJsonObject(string $json): array
{
$decoded = \json_decode($json, true);
return \is_array($decoded) ? $decoded : [];
}
private function getPKCEVerifier(): string
{
if ($this->pkceVerifier === '') {
$this->pkceVerifier = $this->base64UrlEncode(\random_bytes(64));
}
return $this->pkceVerifier;
}
private function getPKCEChallenge(): string
{
return $this->base64UrlEncode(\hash('sha256', $this->getPKCEVerifier(), true));
}
private function encryptPKCEVerifier(string $verifier): array
{
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$key = $this->getPKCEStateKey();
$tag = null;
$data = OpenSSL::encrypt($verifier, OpenSSL::CIPHER_AES_128_GCM, $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($data === false || $tag === null) {
throw new \Exception('Failed to encrypt PKCE verifier.');
}
return [
'data' => $this->base64UrlEncode($data),
'iv' => \bin2hex($iv),
'tag' => \bin2hex($tag),
];
}
private function decryptPKCEVerifier(array $payload): string
{
$data = $payload['data'] ?? '';
$iv = $payload['iv'] ?? '';
$tag = $payload['tag'] ?? '';
if ($data === '' || $iv === '' || $tag === '') {
return '';
}
$decodedData = $this->base64UrlDecode($data);
$decodedIv = \hex2bin($iv);
$decodedTag = \hex2bin($tag);
if ($decodedData === false || $decodedIv === false || $decodedTag === false) {
return '';
}
return OpenSSL::decrypt(
$decodedData,
OpenSSL::CIPHER_AES_128_GCM,
$this->getPKCEStateKey(),
OPENSSL_RAW_DATA,
$decodedIv,
$decodedTag
) ?: '';
}
private function getPKCEStateKey(): string
{
$key = System::getEnv('_APP_OPENSSL_KEY_V1', '');
if ($key === '') {
throw new \Exception('X OAuth2 requires _APP_OPENSSL_KEY_V1 to encrypt PKCE state.');
}
return $key;
}
private function base64UrlEncode(string $value): string
{
return \rtrim(\strtr(\base64_encode($value), '+/', '-_'), '=');
}
private function base64UrlDecode(string $value): string|false
{
$padding = \strlen($value) % 4;
if ($padding > 0) {
$value .= \str_repeat('=', 4 - $padding);
}
return \base64_decode(\strtr($value, '-_', '+/'), true);
}
}
@@ -0,0 +1,21 @@
<?php
namespace Appwrite\Bus\Events;
use Utopia\Bus\Event;
class SessionCreated implements Event
{
/**
* @param array<string, mixed> $user
* @param array<string, mixed> $project
* @param array<string, mixed> $session
*/
public function __construct(
public readonly array $user,
public readonly array $project,
public readonly array $session,
public readonly string $locale,
) {
}
}
+28 -9
View File
@@ -3,10 +3,12 @@
namespace Appwrite\Bus\Listeners;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Event\Execution;
use Appwrite\Event\Message\Execution as ExecutionMessage;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\Span\Span;
use Utopia\System\System;
class Log extends Listener
{
@@ -24,16 +26,33 @@ class Log extends Listener
{
$this
->desc('Persists execution logs to database via queue')
->inject('publisher')
->inject('publisherForExecutions')
->callback($this->handle(...));
}
public function handle(ExecutionCompleted $event, Publisher $publisher): void
public function handle(ExecutionCompleted $event, ExecutionPublisher $publisherForExecutions): void
{
$queueForExecutions = new Execution($publisher);
$queueForExecutions
->setExecution(new Document($event->execution))
->setProject(new Document($event->project))
->trigger();
$project = new Document($event->project);
$execution = new Document($event->execution);
if ($execution->getAttribute('resourceType', '') === 'functions') {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.v1_executions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
}
$publisherForExecutions->enqueue(new ExecutionMessage(
project: $project,
execution: $execution,
));
}
}
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace Appwrite\Bus\Listeners;
use Appwrite\Auth\MFA\Type;
use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Event\Mail;
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;
class Mails extends Listener
{
public static function getName(): string
{
return 'mails';
}
public static function getEvents(): array
{
return [SessionCreated::class];
}
public function __construct()
{
$this
->desc('Sends session alert emails')
->inject('publisher')
->inject('locale')
->inject('platform')
->inject('dbForProject')
->callback($this->handle(...));
}
public function handle(SessionCreated $event, Publisher $publisher, Locale $locale, array $platform, Database $dbForProject): void
{
$project = new Document($event->project);
if (!($project->getAttribute('auths', [])['sessionAlerts'] ?? false)) {
return;
}
if (empty($event->user['email'])) {
return;
}
$provider = $event->session['provider'] ?? '';
$factors = $event->session['factors'] ?? [];
if (\in_array($provider, [SESSION_PROVIDER_MAGIC_URL, SESSION_PROVIDER_TOKEN]) && \in_array(Type::EMAIL, $factors)) {
return;
}
if ($dbForProject->count('sessions', [Query::equal('userId', [$event->user['$id']])]) === 1) {
return;
}
$locale->setDefault($event->locale);
$session = new Document($event->session);
$smtp = $project->getAttribute('smtp', []);
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
if (!(new FileName())->isValid($smtpBaseTemplate)) {
throw new \Exception('Invalid template path');
}
$customTemplate = $project->getAttribute('templates', [])["email.sessionAlert-$event->locale"] ?? [];
$isBranded = $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE;
$subject = $customTemplate['subject'] ?? $locale->getText('emails.sessionAlert.subject');
$preview = $locale->getText('emails.sessionAlert.preview');
$body = empty($customTemplate['message'])
? Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-session-alert.tpl')
->setParam('{{hello}}', $locale->getText('emails.sessionAlert.hello'))
->setParam('{{body}}', $locale->getText('emails.sessionAlert.body'))
->setParam('{{listDevice}}', $locale->getText('emails.sessionAlert.listDevice'))
->setParam('{{listIpAddress}}', $locale->getText('emails.sessionAlert.listIpAddress'))
->setParam('{{listCountry}}', $locale->getText('emails.sessionAlert.listCountry'))
->setParam('{{footer}}', $locale->getText('emails.sessionAlert.footer'))
->setParam('{{thanks}}', $locale->getText('emails.sessionAlert.thanks'))
->setParam('{{signature}}', $locale->getText('emails.sessionAlert.signature'))
->render()
: $customTemplate['message'];
$clientName = $session->getAttribute('clientName')
?: ($session->getAttribute('userAgent') ?: 'UNKNOWN');
$projectName = $project->getId() === 'console'
? $platform['platformName']
: $project->getAttribute('name');
$emailVariables = [
'direction' => $locale->getText('settings.direction'),
'date' => (new \DateTime())->format('F j'),
'year' => (new \DateTime())->format('YYYY'),
'time' => (new \DateTime())->format('H:i:s'),
'user' => $event->user['name'] ?? '',
'project' => $projectName,
'device' => $clientName,
'ipAddress' => $session->getAttribute('ip'),
'country' => $locale->getText('countries.' . $session->getAttribute('countryCode'), $locale->getText('locale.country.unknown')),
];
if ($isBranded) {
$emailVariables += [
'accentColor' => $platform['accentColor'],
'logoUrl' => $platform['logoUrl'],
'twitter' => $platform['twitterUrl'],
'discord' => $platform['discordUrl'],
'github' => $platform['githubUrl'],
'terms' => $platform['termsUrl'],
'privacy' => $platform['privacyUrl'],
'platform' => $platform['platformName'],
];
}
$queueForMails = new Mail($publisher);
if ($smtp['enabled'] ?? false) {
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '')
->setSmtpReplyTo($customTemplate['replyTo'] ?? $smtp['replyTo'] ?? '')
->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'));
}
$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();
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Event\Context;
use Utopia\Database\Document;
class Audit
{
public function __construct(
public ?Document $project = null,
public ?Document $user = null,
public string $mode = '',
public string $userAgent = '',
public string $ip = '',
public string $hostname = '',
public string $event = '',
public string $resource = '',
public array $payload = [],
) {
}
public function isEmpty(): bool
{
return $this->project === null
&& $this->user === null
&& $this->mode === ''
&& $this->userAgent === ''
&& $this->ip === ''
&& $this->hostname === ''
&& $this->event === ''
&& $this->resource === ''
&& $this->payload === [];
}
}
+40 -21
View File
@@ -637,9 +637,11 @@ class Event
*/
$eventValues = \array_values($events);
/**
* Return a combined list of table, collection events and if tablesdb present then include all for backward compatibility
*/
$databaseType = $database?->getAttribute('type', 'legacy');
if ($database !== null && !\in_array($databaseType, ['legacy', 'tablesdb'], true)) {
return $eventValues;
}
return Event::mirrorCollectionEvents($pattern, $eventValues[0], $eventValues);
}
@@ -662,21 +664,30 @@ class Event
}
/**
* Adds `table` events for `collection` events.
* Adds table/collection counterpart events for backward compatibility.
*
* Example:
*
* `databases.*.collections.*.documents.*.update` →\
* `[databases.*.collections.*.documents.*.update, databases.*.tables.*.rows.*.update]`
*
* `databases.*.tables.*.rows.*.update` →\
* `[databases.*.tables.*.rows.*.update, databases.*.collections.*.documents.*.update]`
*/
private static function mirrorCollectionEvents(string $pattern, string $firstEvent, array $events): array
{
$tableEventMap = [
$collectionsToTablesMap = [
'documents' => 'rows',
'collections' => 'tables',
'attributes' => 'columns',
];
$tablesToCollectionsMap = [
'rows' => 'documents',
'tables' => 'collections',
'columns' => 'attributes',
];
$databasesEventMap = [
'tablesdb' => 'databases',
'tables' => 'collections',
@@ -687,7 +698,10 @@ class Event
if (
(
str_contains($pattern, 'databases.') &&
str_contains($firstEvent, 'collections')
(
str_contains($firstEvent, 'collections') ||
str_contains($firstEvent, 'tables')
)
) ||
(
str_contains($firstEvent, 'tablesdb.')
@@ -698,25 +712,16 @@ class Event
$pairedEvents[] = $event;
// tablesdb needs databases event with tables and collections
if (str_contains($event, 'tablesdb')) {
$databasesSideEvent = str_replace(
array_keys($databasesEventMap),
array_values($databasesEventMap),
$event
);
$databasesSideEvent = self::replaceEventSegments($event, $databasesEventMap);
$pairedEvents[] = $databasesSideEvent;
$tableSideEvent = str_replace(
array_keys($tableEventMap),
array_values($tableEventMap),
$databasesSideEvent
);
$tableSideEvent = self::replaceEventSegments($databasesSideEvent, $collectionsToTablesMap);
$pairedEvents[] = $tableSideEvent;
} elseif (str_contains($event, 'collections')) {
$tableSideEvent = str_replace(
array_keys($tableEventMap),
array_values($tableEventMap),
$event
);
$tableSideEvent = self::replaceEventSegments($event, $collectionsToTablesMap);
$pairedEvents[] = $tableSideEvent;
} elseif (str_contains($event, 'tables')) {
$collectionSideEvent = self::replaceEventSegments($event, $tablesToCollectionsMap);
$pairedEvents[] = $collectionSideEvent;
}
}
@@ -728,6 +733,20 @@ class Event
return array_values(array_unique($events));
}
/**
* Replace only exact event path segments, never partial substrings.
*/
private static function replaceEventSegments(string $event, array $map): string
{
$parts = \explode('.', $event);
$parts = \array_map(
fn (string $part) => $map[$part] ?? $part,
$parts
);
return \implode('.', $parts);
}
/**
* Maps event terminology based on database type
*/
+19
View File
@@ -53,4 +53,23 @@ class Execution extends Event
'execution' => $this->execution,
];
}
/**
* Trim payload for the execution event.
* Only the project ID is needed the worker DI fetches the full project from the platform database.
*
* @return array
*/
protected function trimPayload(): array
{
$trimmed = [];
if ($this->project) {
$trimmed['project'] = new Document([
'$id' => $this->project->getId(),
]);
}
return $trimmed;
}
}
-1
View File
@@ -101,7 +101,6 @@ class Mail extends Event
/**
* Sets preview for the mail event.
*
* @param string $preview
* @return self
*/
public function setPreview(string $preview): self
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Event\Message;
use Appwrite\Event\Context\Audit as AuditContext;
use Utopia\Database\Document;
final class Audit extends Base
{
public function __construct(
public readonly string $event,
public readonly array $payload,
public readonly Document $project = new Document(),
public readonly Document $user = new Document(),
public readonly string $resource = '',
public readonly string $mode = '',
public readonly string $ip = '',
public readonly string $userAgent = '',
public readonly string $hostname = '',
) {
}
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'user' => $this->user->getArrayCopy(),
'payload' => $this->payload,
'resource' => $this->resource,
'mode' => $this->mode,
'ip' => $this->ip,
'userAgent' => $this->userAgent,
'event' => $this->event,
'hostname' => $this->hostname,
];
}
public static function fromArray(array $data): static
{
return new self(
event: $data['event'] ?? '',
payload: $data['payload'] ?? [],
project: new Document($data['project'] ?? []),
user: new Document($data['user'] ?? []),
resource: $data['resource'] ?? '',
mode: $data['mode'] ?? '',
ip: $data['ip'] ?? '',
userAgent: $data['userAgent'] ?? '',
hostname: $data['hostname'] ?? '',
);
}
public static function fromContext(AuditContext $context): static
{
return new self(
event: $context->event,
payload: $context->payload,
project: $context->project ?? new Document(),
user: $context->user ?? new Document(),
resource: $context->resource,
mode: $context->mode,
ip: $context->ip,
userAgent: $context->userAgent,
hostname: $context->hostname,
);
}
}
@@ -0,0 +1,43 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Certificate extends Base
{
public function __construct(
public readonly Document $project,
public readonly Document $domain,
public readonly bool $skipRenewCheck = false,
public readonly ?string $validationDomain = null,
public readonly string $action = \Appwrite\Event\Certificate::ACTION_GENERATION,
) {
}
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'domain' => $this->domain->getArrayCopy(),
'skipRenewCheck' => $this->skipRenewCheck,
'validationDomain' => $this->validationDomain,
'action' => $this->action,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
domain: new Document($data['domain'] ?? []),
skipRenewCheck: $data['skipRenewCheck'] ?? false,
validationDomain: $data['validationDomain'] ?? null,
action: $data['action'] ?? \Appwrite\Event\Certificate::ACTION_GENERATION,
);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Execution extends Base
{
public function __construct(
public readonly Document $project,
public readonly Document $execution,
) {
}
public function toArray(): array
{
return [
'project' => $this->project->getArrayCopy(),
'execution' => $this->execution->getArrayCopy(),
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
execution: new Document($data['execution'] ?? []),
);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Migration extends Base
{
public function __construct(
public readonly Document $project,
public readonly Document $migration,
public readonly array $platform = [],
) {
}
public function toArray(): array
{
return [
'project' => $this->project->getArrayCopy(),
'migration' => $this->migration->getArrayCopy(),
'platform' => $this->platform,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
migration: new Document($data['migration'] ?? []),
platform: $data['platform'] ?? [],
);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Screenshot extends Base
{
public function __construct(
public readonly Document $project,
public readonly string $deploymentId,
) {
}
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'deploymentId' => $this->deploymentId,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
deploymentId: $data['deploymentId'] ?? '',
);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class StatsResources extends Base
{
public function __construct(
public readonly Document $project,
) {
}
public function toArray(): array
{
return [
'project' => $this->project->getArrayCopy(),
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Audit as AuditMessage;
use Utopia\Console;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Audit extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(AuditMessage $message): string|bool
{
// Audit delivery is best-effort and should never fail the request lifecycle.
try {
return $this->publish($this->queue, $message);
} catch (\Throwable $th) {
Console::error('[Audit] Failed to publish audit message: ' . $th->getMessage());
return false;
}
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Certificate as CertificateMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Certificate extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(CertificateMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Execution as ExecutionMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Execution extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(ExecutionMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Migration as MigrationMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Migration extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(MigrationMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Screenshot as ScreenshotMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Screenshot extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(ScreenshotMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\StatsResources as StatsResourcesMessage;
use Utopia\Console;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class StatsResources extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(StatsResourcesMessage $message): string|bool
{
// Resource stats are best-effort; publishing failures should not interrupt the scheduler loop.
try {
return $this->publish($this->queue, $message);
} catch (\Throwable $th) {
Console::error('[StatsResources] Failed to publish stats resources message: ' . $th->getMessage());
return false;
}
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
+20
View File
@@ -61,6 +61,26 @@ class Realtime extends Event
return $this->subscribers;
}
/**
* Reset the event state for long-lived worker processes.
*
* `Event::reset()` clears params/sensitive/event/payload only. Realtime routing also
* depends on `context`, `subscribers`, and `project`/`user` fields, so we clear them too
* to prevent stale state from affecting subsequent triggers.
*/
public function reset(): self
{
parent::reset();
$this->subscribers = [];
$this->context = [];
$this->project = null;
$this->user = null;
$this->userId = null;
return $this;
}
/**
* Execute Event.
*
+2
View File
@@ -333,6 +333,8 @@ class Exception extends \Exception
/** Platform */
public const string PLATFORM_NOT_FOUND = 'platform_not_found';
public const string PLATFORM_METHOD_UNSUPPORTED = 'platform_method_unsupported';
public const string PLATFORM_ALREADY_EXISTS = 'platform_already_exists';
/** GraphqQL */
public const string GRAPHQL_NO_QUERY = 'graphql_no_query';
+2 -2
View File
@@ -81,8 +81,8 @@ abstract class Adapter implements PromiseAdapter
/**
* Create a new promise that resolves when all passed in promises resolve.
*
* @param array $promisesOrValues
* @param iterable $promisesOrValues
* @return GQLPromise
*/
abstract public function all(array $promisesOrValues): GQLPromise;
abstract public function all(iterable $promisesOrValues): GQLPromise;
}
@@ -35,8 +35,12 @@ class Swoole extends Adapter
return new GQLPromise($promise, $this);
}
public function all(array $promisesOrValues): GQLPromise
public function all(iterable $promisesOrValues): GQLPromise
{
if ($promisesOrValues instanceof \Traversable) {
$promisesOrValues = \iterator_to_array($promisesOrValues);
}
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\GraphQL;
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
final class ResolverLock
{
public Channel $channel;
public ?int $owner = null;
public int $depth = 0;
public function __construct()
{
$this->channel = new Channel(1);
}
/**
* Acquire the lock. Re-entering from the same coroutine only
* increments depth to avoid self-deadlock.
*/
public function acquire(): void
{
$cid = Coroutine::getCid();
if ($this->owner === $cid) {
$this->depth++;
return;
}
$this->channel->push(true);
$this->owner = $cid;
$this->depth = 1;
}
/**
* Release the lock.
*/
public function release(): void
{
if ($this->owner !== Coroutine::getCid()) {
return;
}
$this->depth--;
if ($this->depth > 0) {
return;
}
$this->owner = null;
$this->channel->pop();
}
}
+237 -107
View File
@@ -6,6 +6,7 @@ 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;
@@ -13,6 +14,75 @@ use Utopia\System\System;
class Resolvers
{
/**
* Request-scoped locks keyed by the per-request GraphQL Http instance.
*
* @var array<string, ResolverLock>
*/
private static array $locks = [];
/**
* Preserve response side effects that callers depend on, such as session
* cookies created by account auth routes.
*/
private static function mergeResponseSideEffects(Response $from, Response $to): void
{
foreach ($from->getCookies() as $cookie) {
$to->removeCookie($cookie['name']);
$to->addCookie(
$cookie['name'],
$cookie['value'],
$cookie['expire'],
$cookie['path'],
$cookie['domain'],
$cookie['secure'],
$cookie['httponly'],
$cookie['samesite']
);
}
$headers = $from->getHeaders();
$fallbackCookies = $headers['X-Fallback-Cookies'] ?? null;
if ($fallbackCookies === null) {
return;
}
$to->removeHeader('X-Fallback-Cookies');
foreach ((array) $fallbackCookies as $value) {
$to->addHeader('X-Fallback-Cookies', $value);
}
}
/**
* 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.
*/
private static function getLock(Http $utopia): ResolverLock
{
$key = \spl_object_hash($utopia);
if (!isset(self::$locks[$key])) {
self::$locks[$key] = new ResolverLock();
}
return self::$locks[$key];
}
/**
* Create a resolver for a given API {@see Route}.
*
@@ -24,34 +94,39 @@ class Resolvers
Http $utopia,
?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', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
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');
$path = $route->getPath();
foreach ($args as $key => $value) {
if (\str_contains($path, '/:' . $key)) {
$path = \str_replace(':' . $key, $value, $path);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($route, $args): void {
$path = $route->getPath();
foreach ($args as $key => $value) {
if (\str_contains($path, '/:' . $key)) {
$path = \str_replace(':' . $key, $value, $path);
}
}
$request->setMethod($route->getMethod());
$request->setURI($path);
switch ($route->getMethod()) {
case 'GET':
$request->setQueryString($args);
break;
default:
$request->setPayload($args);
break;
}
}
$request->setMethod($route->getMethod());
$request->setURI($path);
switch ($route->getMethod()) {
case 'GET':
$request->setQueryString($args);
break;
default:
$request->setPayload($args);
break;
}
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
);
});
}
/**
@@ -91,18 +166,23 @@ class Resolvers
string $collectionId,
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', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
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');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -122,23 +202,29 @@ class Resolvers
callable $url,
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', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
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');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setQueryString($params($databaseId, $collectionId, $args));
$beforeResolve = function ($payload) {
return $payload['documents'];
};
$beforeResolve = function ($payload) {
return $payload['documents'];
};
self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
beforeResolve: $beforeResolve,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setQueryString($params($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -158,19 +244,24 @@ class Resolvers
callable $url,
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', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
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');
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -190,19 +281,24 @@ class Resolvers
callable $url,
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', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
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');
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $params, $args): void {
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
$request->setPayload($params($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -220,18 +316,23 @@ class Resolvers
string $collectionId,
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', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
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');
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
self::resolve($utopia, $request, $response, $resolve, $reject);
}
);
self::resolve(
$utopia,
$request,
$response,
$resolve,
$reject,
prepareRequest: static function (Request $request) use ($databaseId, $collectionId, $url, $args): void {
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
}
);
});
}
/**
@@ -241,7 +342,7 @@ class Resolvers
* @param callable $resolve
* @param callable $reject
* @param callable|null $beforeResolve
* @param callable|null $beforeReject
* @param callable|null $prepareRequest
* @return void
* @throws Exception
*/
@@ -252,38 +353,67 @@ class Resolvers
callable $resolve,
callable $reject,
?callable $beforeResolve = null,
?callable $beforeReject = null,
?callable $prepareRequest = null,
): void {
// Drop json content type so post args are used directly
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
$request->removeHeader('content-type');
}
$lock = self::getLock($utopia);
$request = clone $request;
$utopia->setResource('request', static fn () => $request);
$response->setContentType(Response::CONTENT_TYPE_NULL);
$lock->acquire();
$original = $utopia->getRoute();
try {
$route = $utopia->match($request, fresh: true);
$request = clone $request;
$utopia->execute($route, $request, $response);
} catch (\Throwable $e) {
if ($beforeReject) {
$e = $beforeReject($e);
// Drop json content type so post args are used directly.
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
$request->removeHeader('content-type');
}
if ($prepareRequest) {
$prepareRequest($request);
}
/** @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->setContentType(Response::CONTENT_TYPE_NULL);
$resolverResponse->setSent(false);
$route = $utopia->match($request, fresh: true);
$request->setRoute($route);
$utopia->execute($route, $request, $resolverResponse);
self::mergeResponseSideEffects($resolverResponse, $response);
if ($resolverResponse->isSent()) {
$response
->setStatusCode($resolverResponse->getStatusCode())
->setSent(true);
$resolve(null);
return;
}
$payload = $resolverResponse->getPayload();
$statusCode = $resolverResponse->getStatusCode();
} catch (\Throwable $e) {
$reject($e);
return;
} finally {
if ($original !== null) {
$utopia->setRoute($original);
}
$lock->release();
unset(self::$locks[\spl_object_hash($utopia)]);
}
$payload = $response->getPayload();
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) {
if ($beforeReject) {
$payload = $beforeReject($payload);
}
if ($statusCode < 200 || $statusCode >= 400) {
$reject(new GQLException(
message: $payload['message'],
code: $response->getStatusCode()
code: $statusCode
));
return;
}
-4
View File
@@ -32,10 +32,6 @@ class Schema
array $urls,
array $params,
): GQLSchema {
Http::setResource('utopia:graphql', static function () use ($utopia) {
return $utopia;
});
if (!empty(self::$schema)) {
return self::$schema;
}
+8 -3
View File
@@ -3,12 +3,13 @@
namespace Appwrite\GraphQL\Types;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Assoc extends Json
{
public $name = 'Assoc';
public $description = 'The `Assoc` scalar type represents associative array values.';
public string $name = 'Assoc';
public ?string $description = 'The `Assoc` scalar type represents associative array values.';
public function serialize($value)
{
@@ -30,6 +31,10 @@ class Assoc extends Json
public function parseLiteral(Node $valueNode, ?array $variables = null)
{
return \json_decode($valueNode->value, true);
if ($valueNode instanceof StringValueNode) {
return \json_decode($valueNode->value, true);
}
return parent::parseLiteral($valueNode, $variables);
}
}
+2 -2
View File
@@ -8,8 +8,8 @@ use GraphQL\Type\Definition\ScalarType;
class InputFile extends ScalarType
{
public $name = 'InputFile';
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
public string $name = 'InputFile';
public ?string $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
public function serialize($value)
+2 -2
View File
@@ -14,8 +14,8 @@ use GraphQL\Type\Definition\ScalarType;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Json extends ScalarType
{
public $name = 'Json';
public $description = 'The `JSON` scalar type represents JSON values as specified by
public string $name = 'Json';
public ?string $description = 'The `JSON` scalar type represents JSON values as specified by
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
public function serialize($value)
+27 -14
View File
@@ -20,6 +20,7 @@ class Realtime extends MessagingAdapter
* [CONNECTION_ID] ->
* 'projectId' -> [PROJECT_ID]
* 'roles' -> [ROLE_x, ROLE_Y]
* 'userId' -> [USER_ID]
* 'channels' -> [CHANNEL_NAME_X, CHANNEL_NAME_Y, CHANNEL_NAME_Z]
*/
public array $connections = [];
@@ -67,25 +68,35 @@ class Realtime extends MessagingAdapter
* @param array $queryGroup Array of Query objects for this subscription (AND logic within subscription)
* @return void
*/
public function subscribe(string $projectId, mixed $identifier, string $subscriptionId, array $roles, array $channels, array $queryGroup = []): void
{
public function subscribe(
string $projectId,
mixed $identifier,
string $subscriptionId,
array $roles,
array $channels,
array $queryGroup = [],
?string $userId = null
): void {
if (!isset($this->subscriptions[$projectId])) { // Init Project
$this->subscriptions[$projectId] = [];
}
$strings = [];
if (empty($queryGroup)) {
$strings[] = Query::select(['*'])->toString();
} else {
foreach ($queryGroup as $query) {
$strings[] = $query->toString();
}
}
$data = [];
$data = [
'strings' => $strings,
'compiled' => RuntimeQuery::compile($queryGroup),
];
if (!empty($channels)) {
if (empty($queryGroup)) {
$strings[] = Query::select(['*'])->toString();
} else {
foreach ($queryGroup as $query) {
$strings[] = $query->toString();
}
}
$data = [
'strings' => $strings,
'compiled' => RuntimeQuery::compile($queryGroup),
];
}
foreach ($roles as $role) {
if (!isset($this->subscriptions[$projectId][$role])) {
@@ -103,10 +114,12 @@ class Realtime extends MessagingAdapter
}
}
// Update connection info
// Keep userId from onOpen/authentication when provided.
// Fallback to existing stored value for subsequent subscribe upserts.
$this->connections[$identifier] = [
'projectId' => $projectId,
'roles' => $roles,
'userId' => $userId ?? ($this->connections[$identifier]['userId'] ?? ''),
'channels' => $channels
];
}
+1
View File
@@ -93,6 +93,7 @@ abstract class Migration
'1.8.0' => 'V23',
'1.8.1' => 'V23',
'1.9.0' => 'V24',
'1.9.1' => 'V24',
];
/**
+68 -45
View File
@@ -6,20 +6,10 @@ class Platform
{
public const TYPE_UNKNOWN = 'unknown';
public const TYPE_WEB = 'web';
public const TYPE_FLUTTER_IOS = 'flutter-ios';
public const TYPE_FLUTTER_ANDROID = 'flutter-android';
public const TYPE_FLUTTER_MACOS = 'flutter-macos';
public const TYPE_FLUTTER_WINDOWS = 'flutter-windows';
public const TYPE_FLUTTER_LINUX = 'flutter-linux';
public const TYPE_FLUTTER_WEB = 'flutter-web';
public const TYPE_APPLE_IOS = 'apple-ios';
public const TYPE_APPLE_MACOS = 'apple-macos';
public const TYPE_APPLE_WATCHOS = 'apple-watchos';
public const TYPE_APPLE_TVOS = 'apple-tvos';
public const TYPE_APPLE = 'apple';
public const TYPE_ANDROID = 'android';
public const TYPE_UNITY = 'unity';
public const TYPE_REACT_NATIVE_IOS = 'react-native-ios';
public const TYPE_REACT_NATIVE_ANDROID = 'react-native-android';
public const TYPE_WINDOWS = 'windows';
public const TYPE_LINUX = 'linux';
public const TYPE_SCHEME = 'scheme';
public const SCHEME_HTTP = 'http';
@@ -57,6 +47,36 @@ class Platform
self::SCHEME_TAURI => 'Web (Tauri)',
];
/**
* Map deprecated platform types to their new consolidated types.
*
* The 1.9.x refactor consolidated ~15 platform types into 5 new ones.
* Existing platforms in the database may still have old type values.
*
* @param string $type
* @return string The mapped type, or the original if not deprecated.
*/
public static function mapDeprecatedType(string $type): string
{
$mapping = [
'flutter-web' => self::TYPE_WEB,
'unity' => self::TYPE_WEB,
'flutter-ios' => self::TYPE_APPLE,
'flutter-macos' => self::TYPE_APPLE,
'apple-ios' => self::TYPE_APPLE,
'apple-macos' => self::TYPE_APPLE,
'apple-watchos' => self::TYPE_APPLE,
'apple-tvos' => self::TYPE_APPLE,
'react-native-ios' => self::TYPE_APPLE,
'flutter-android' => self::TYPE_ANDROID,
'react-native-android' => self::TYPE_ANDROID,
'flutter-windows' => self::TYPE_WINDOWS,
'flutter-linux' => self::TYPE_LINUX,
];
return $mapping[$type] ?? $type;
}
/**
* Get user-friendly platform name from a scheme.
*
@@ -77,25 +97,28 @@ class Platform
$key = strtolower($platform['key'] ?? '');
switch ($type) {
case 'flutter-web':
case 'unity':
case self::TYPE_WEB:
case self::TYPE_FLUTTER_WEB:
if (!empty($hostname)) {
$hostnames[] = $hostname;
}
break;
case self::TYPE_FLUTTER_IOS:
case self::TYPE_FLUTTER_ANDROID:
case self::TYPE_FLUTTER_MACOS:
case self::TYPE_FLUTTER_WINDOWS:
case self::TYPE_FLUTTER_LINUX:
case 'flutter-android':
case 'react-native-android':
case self::TYPE_ANDROID:
case self::TYPE_APPLE_IOS:
case self::TYPE_APPLE_MACOS:
case self::TYPE_APPLE_WATCHOS:
case self::TYPE_APPLE_TVOS:
case self::TYPE_REACT_NATIVE_IOS:
case self::TYPE_REACT_NATIVE_ANDROID:
case self::TYPE_UNITY:
case 'flutter-windows':
case self::TYPE_WINDOWS:
case 'flutter-linux':
case self::TYPE_LINUX:
case 'flutter-ios':
case 'flutter-macos':
case 'apple-ios':
case 'apple-macos':
case 'apple-watchos':
case 'apple-tvos':
case 'react-native-ios':
case self::TYPE_APPLE:
if (!empty($key)) {
$hostnames[] = $key;
}
@@ -120,38 +143,38 @@ class Platform
$schemes[] = $scheme;
}
break;
case 'flutter-web':
case 'unity':
case self::TYPE_WEB:
case self::TYPE_FLUTTER_WEB:
$schemes[] = self::SCHEME_HTTP;
$schemes[] = self::SCHEME_HTTPS;
break;
case self::TYPE_FLUTTER_IOS:
case self::TYPE_APPLE_IOS:
case self::TYPE_REACT_NATIVE_IOS:
$schemes[] = self::SCHEME_IOS;
break;
case self::TYPE_FLUTTER_ANDROID:
case 'flutter-android':
case 'react-native-android':
case self::TYPE_ANDROID:
case self::TYPE_REACT_NATIVE_ANDROID:
$schemes[] = self::SCHEME_ANDROID;
break;
case self::TYPE_FLUTTER_MACOS:
case self::TYPE_APPLE_MACOS:
case 'flutter-ios':
case 'flutter-macos':
case 'apple-ios':
case 'apple-macos':
case 'apple-watchos':
case 'apple-tvos':
case 'react-native-ios':
case self::TYPE_APPLE:
$schemes[] = self::SCHEME_WATCHOS;
$schemes[] = self::SCHEME_MACOS;
$schemes[] = self::SCHEME_TVOS;
$schemes[] = self::SCHEME_IOS;
break;
case self::TYPE_FLUTTER_WINDOWS:
case self::TYPE_UNITY:
case 'flutter-windows':
case self::TYPE_WINDOWS:
$schemes[] = self::SCHEME_WINDOWS;
break;
case self::TYPE_FLUTTER_LINUX:
case 'flutter-linux':
case self::TYPE_LINUX:
$schemes[] = self::SCHEME_LINUX;
break;
case self::TYPE_APPLE_WATCHOS:
$schemes[] = self::SCHEME_WATCHOS;
break;
case self::TYPE_APPLE_TVOS:
$schemes[] = self::SCHEME_TVOS;
break;
default:
break;
}
+16 -16
View File
@@ -145,9 +145,20 @@ class Server
$paths = $this->paths;
$state = $this->state;
Http::setResource('installerState', fn () => $state);
Http::setResource('installerConfig', fn () => $config);
Http::setResource('installerPaths', fn () => $paths);
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
public function getNativeServer(): SwooleServer
{
return $this->server;
}
};
$nativeServer = $adapter->getNativeServer();
$container = $adapter->getContainer();
$container->set('installerState', fn () => $state);
$container->set('installerConfig', fn () => $config);
$container->set('installerPaths', fn () => $paths);
$container->set('swooleServer', fn () => $nativeServer);
// Register routes via Utopia Platform
$platform = new Installer();
@@ -160,17 +171,6 @@ class Server
->inject('response')
->action($errorHandler->action(...));
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
public function getNativeServer(): SwooleServer
{
return $this->server;
}
};
$nativeServer = $adapter->getNativeServer();
Http::setResource('swooleServer', fn () => $nativeServer);
$nativeServer->on('start', function () use ($nativeServer, $port, $readyFile) {
\Swoole\Process::signal(SIGTERM, fn () => $nativeServer->shutdown());
\Swoole\Process::signal(SIGINT, fn () => $nativeServer->shutdown());
@@ -180,7 +180,7 @@ class Server
}
});
$adapter->onRequest(function (Request $request, Response $response) use ($files) {
$adapter->onRequest(function (Request $request, Response $response) use ($adapter, $files) {
// Serve static files from memory
$uri = $request->getURI();
if ($files->isFileLoaded($uri)) {
@@ -190,7 +190,7 @@ class Server
return;
}
$app = new Http('UTC');
$app = new Http($adapter, 'UTC');
$app->run($request, $response);
});
@@ -7,9 +7,13 @@ use Appwrite\Platform\Action as AppwriteAction;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Operator;
use Utopia\Database\Query;
class Action extends AppwriteAction
{
public const LIST_CACHE_FIELD_DOCUMENTS = 'documents';
public const LIST_CACHE_FIELD_TOTAL = 'total';
private string $context = DATABASE_TYPE_LEGACY;
public function getDatabaseType(): string
@@ -101,4 +105,67 @@ class Action extends AppwriteAction
return $data;
}
/**
* Stable Redis key for a collection's cached list responses.
*
* All variations (schema × roles × queries) for a single collection live as
* fields inside this one Redis hash, so purging every cached entry for a
* collection is a single O(1) DEL regardless of how many variations have
* been cached.
*/
protected function getListCacheKey(Database $dbForProject, string $collectionId): string
{
return \sprintf(
'%s-cache:%s:%s:%s:collection:%s',
$dbForProject->getCacheName(),
$dbForProject->getAdapter()->getHostname(),
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$collectionId,
);
}
/**
* Hash field for a single variation of a cached list response.
*
* Scoped by the collection schema (attributes + indexes), the caller's
* authorization roles, the exact query set, and the field type so users
* with different permissions never share entries.
*
* @param Document $collection Collection document (for schema hash)
* @param array<mixed> $roles Caller authorization roles
* @param array<Query|string> $queries Queries for this list call
* @param string $type LIST_CACHE_FIELD_DOCUMENTS or LIST_CACHE_FIELD_TOTAL
*/
protected function getListCacheField(Document $collection, array $roles, array $queries, string $type): string
{
$schemaHash = \md5(
\json_encode($collection->getAttribute('attributes', []))
. \json_encode($collection->getAttribute('indexes', []))
);
$serialized = \array_map(
static fn ($query) => $query instanceof Query ? $query->toArray() : $query,
$queries,
);
return \sprintf(
'%s:%s:%s:%s',
$schemaHash,
\md5(\json_encode($roles)),
\md5(\json_encode($serialized)),
$type,
);
}
/**
* Purge every cached list response for a collection.
*
* One DEL on the collection's Redis hash, clearing all variations at once.
*/
protected function purgeListCache(Database $dbForProject, string $collectionId): bool
{
return $dbForProject->getCache()->purge($this->getListCacheKey($dbForProject, $collectionId));
}
}
@@ -3,34 +3,29 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
abstract class Action extends UtopiaAction
abstract class Action extends DatabasesAction
{
/**
* The current API context (either 'table' or 'collection').
*/
private ?string $context = COLLECTIONS;
private ?string $databaseType = LEGACY;
/**
* Get the response model used in the SDK and HTTP responses.
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): UtopiaAction
public function setHttpPath(string $path): self
{
if (\str_contains($path, '/tablesdb')) {
$this->context = TABLES;
$this->databaseType = TABLESDB;
} elseif (\str_contains($path, '/vectorsdb')) {
$this->databaseType = VECTORSDB;
}
return parent::setHttpPath($path);
parent::setHttpPath($path);
return $this;
}
/**
@@ -41,14 +36,6 @@ abstract class Action extends UtopiaAction
return $this->context;
}
/**
* Get the current API database type.
*/
protected function getDatabaseType(): string
{
return $this->databaseType;
}
/**
* Get the key used in event parameters (e.g., 'collectionId' or 'tableId').
*/
@@ -207,6 +207,8 @@ class Decrement extends Action
->addMetric($this->getDatabasesOperationWriteMetric(), 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
$response->dynamic($document, $this->getResponseModel());
$queueForEvents
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collectionId)
@@ -216,7 +218,5 @@ class Decrement extends Action
->setContext('database', $database)
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->getPayload(), sensitive: $relationships);
$response->dynamic($document, $this->getResponseModel());
}
}
@@ -207,6 +207,8 @@ class Increment extends Action
->addMetric($this->getDatabasesOperationWriteMetric(), 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), $this->getDatabasesIdOperationWriteMetric()), 1);
$response->dynamic($document, $this->getResponseModel());
$queueForEvents
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collectionId)
@@ -216,7 +218,5 @@ class Increment extends Action
->setContext('database', $database)
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->getPayload(), sensitive: $relationships);
$response->dynamic($document, $this->getResponseModel());
}
}
@@ -50,6 +50,11 @@ class Create extends Action
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return false;
}
public function __construct()
{
$this
@@ -139,30 +144,42 @@ class Create extends Action
->inject('eventProcessor')
->callback($this->action(...));
}
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
: $data;
$supportsEmptyDocument = $this->getSupportForEmptyDocument();
$hasData = !empty($data);
$hasDocuments = !empty($documents);
/**
* Determine which internal path to call, single or bulk
*/
if (empty($data) && empty($documents)) {
if (!$supportsEmptyDocument && !$hasData && !$hasDocuments) {
// No single or bulk documents provided
throw new Exception($this->getMissingDataException());
}
if (!empty($data) && !empty($documents)) {
// When empty documents are supported, an empty payload should still be treated as single create.
if ($supportsEmptyDocument && !$hasData && !$hasDocuments) {
$data = [];
$hasData = true;
}
if ($hasData && $hasDocuments) {
// Both single and bulk documents provided
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup());
}
if (!empty($data) && empty($documentId)) {
if ($hasData && empty($documentId)) {
// Single document provided without document ID
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
throw new Exception($this->getMissingDataException(), $message);
}
if (!empty($documents) && !empty($documentId)) {
if ($hasDocuments && !empty($documentId)) {
// Bulk documents provided with document ID
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
throw new Exception(
@@ -170,13 +187,13 @@ class Create extends Action
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.'
);
}
if (!empty($documents) && !empty($permissions)) {
if ($hasDocuments && !empty($permissions)) {
// Bulk documents provided with permissions
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead');
}
$isBulk = true;
if (!empty($data)) {
$isBulk = $hasDocuments;
if ($hasData) {
// Single document provided, convert to single item array
// But remember that it was single to respond with a single document
$isBulk = false;
@@ -131,6 +131,7 @@ class XList extends Action
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -72,7 +72,7 @@ class XList extends Action
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours).', true)
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for caching list responses. Responses are stored in an in-memory key-value cache, keyed per project, collection, schema version (attributes and indexes), caller authorization roles, and the exact query — so users with different permissions never share cached entries. Schema changes invalidate cached entries automatically; document writes do not, so choose a TTL you are comfortable serving as stale data. Set to 0 to disable caching. Must be between 0 and 86400 (24 hours).', true)
->inject('response')
->inject('dbForProject')
->inject('user')
@@ -127,84 +127,72 @@ class XList extends Action
}
try {
$selectQueries = Query::groupByType($queries)['selections'] ?? [];
$hasSelects = ! empty(Query::groupByType($queries)['selections'] ?? []);
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
// When there are no select queries, relationship loading is skipped on the
// underlying find() to avoid pulling related documents the caller did not ask for.
$find = $hasSelects
? fn () => $dbForDatabases->find($collectionTableId, $queries)
: fn () => $dbForDatabases->skipRelationships(fn () => $dbForDatabases->find($collectionTableId, $queries));
// Use transaction-aware document retrieval if transactionId is provided
if ($transactionId !== null) {
$documents = $transactionState->listDocuments($database, $collectionTableId, $transactionId, $queries);
$total = $includeTotal ? $transactionState->countDocuments($database, $collectionTableId, $transactionId, $queries) : 0;
} elseif (! empty($selectQueries)) {
} elseif ((int)$ttl > 0) {
$cacheKey = $this->getListCacheKey($dbForProject, $collectionId);
$roles = $dbForProject->getAuthorization()->getRoles();
$documentsField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_DOCUMENTS);
if ((int)$ttl > 0) {
$serializedQueries = [];
foreach ($queries as $query) {
$serializedQueries[] = $query instanceof Query ? $query->toArray() : $query;
}
$hostname = $dbForProject->getAdapter()->getHostname();
$roles = $dbForProject->getAuthorization()->getRoles();
$schemaHash = \md5(\json_encode($collection->getAttribute('attributes', [])) . \json_encode($collection->getAttribute('indexes', [])));
$cacheKeyBase = \sprintf(
'%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s',
$dbForProject->getCacheName(),
$hostname,
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$collectionId,
$schemaHash,
\md5(\json_encode($roles)),
\md5(\json_encode($serializedQueries))
);
$documentsCacheKey = $cacheKeyBase . ':documents';
$totalCacheKey = $cacheKeyBase . ':total';
$documentsCacheHit = $totalDocumentsCacheHit = false;
$cachedDocuments = $dbForProject->getCache()->load($documentsCacheKey, $ttl);
if ($cachedDocuments !== null &&
$cachedDocuments !== false &&
\is_array($cachedDocuments)) {
$documents = \array_map(function ($doc) {
return new Document($doc);
}, $cachedDocuments);
$documentsCacheHit = true;
} else {
$documents = $dbForDatabases->find($collectionTableId, $queries);
// Convert Document objects to arrays for caching
$documentsArray = \array_map(function ($doc) {
return $doc->getArrayCopy();
}, $documents);
$dbForProject->getCache()->save($documentsCacheKey, $documentsArray);
}
if ($includeTotal) {
$cachedTotal = $dbForProject->getCache()->load($totalCacheKey, $ttl);
if ($cachedTotal !== null && $cachedTotal !== false) {
$total = $cachedTotal;
$totalDocumentsCacheHit = true;
} else {
$total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT);
$dbForProject->getCache()->save($totalCacheKey, $total);
}
} else {
$total = 0;
}
$response->addHeader('X-Appwrite-Cache', $documentsCacheHit ? 'hit' : 'miss');
} else {
// has selects, allow relationship on documents
$documents = $dbForDatabases->find($collectionTableId, $queries);
$total = $includeTotal ? $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
$documentsCacheHit = false;
try {
$cachedDocuments = $dbForProject->getCache()->load($cacheKey, $ttl, $documentsField);
} catch (\Throwable) {
$cachedDocuments = null;
}
if ($cachedDocuments !== null &&
$cachedDocuments !== false &&
\is_array($cachedDocuments)) {
$documents = \array_map(function ($doc) {
return new Document($doc);
}, $cachedDocuments);
$documentsCacheHit = true;
} else {
$documents = $find();
$documentsArray = \array_map(function ($doc) {
return $doc->getArrayCopy();
}, $documents);
try {
$dbForProject->getCache()->save($cacheKey, $documentsArray, $documentsField);
} catch (\Throwable) {
}
}
if ($includeTotal) {
$totalField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_TOTAL);
try {
$cachedTotal = $dbForProject->getCache()->load($cacheKey, $ttl, $totalField);
} catch (\Throwable) {
$cachedTotal = null;
}
if ($cachedTotal !== null && $cachedTotal !== false) {
$total = $cachedTotal;
} else {
$total = $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT);
try {
$dbForProject->getCache()->save($cacheKey, $total, $totalField);
} catch (\Throwable) {
}
}
} else {
$total = 0;
}
$response->addHeader('X-Appwrite-Cache', $documentsCacheHit ? 'hit' : 'miss');
} else {
// has no selects, disable relationship loading on documents
/* @type Document[] $documents */
$documents = $dbForDatabases->skipRelationships(fn () => $dbForDatabases->find($collectionTableId, $queries));
$documents = $find();
$total = $includeTotal ? $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
}
} catch (OrderException $e) {
@@ -123,6 +123,7 @@ class XList extends Action
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'] ?? null,
'time' => $log['time'] ?? null,
'osCode' => $os['osCode'] ?? null,
@@ -68,6 +68,7 @@ class Update extends Action
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
->param('purge', false, new Boolean(true), 'When true, purge all cached list responses for this collection as part of the update. Use this to force readers to see fresh data immediately instead of waiting for the cache TTL to expire.', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
@@ -76,7 +77,7 @@ class Update extends Action
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, ?string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, ?string $name, ?array $permissions, bool $documentSecurity, bool $enabled, bool $purge, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Authorization $authorization): void
{
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
@@ -117,6 +118,10 @@ class Update extends Action
->setParam('databaseId', $databaseId)
->setParam($this->getEventsParamKey(), $collection->getId());
if ($purge) {
$this->purgeListCache($dbForProject, $collectionId);
}
$this->addRowBytesInfo($collection, $dbForProject);
$response->dynamic($collection, $this->getResponseModel());
@@ -61,16 +61,16 @@ class Create extends Action
$databaseKeys = System::getEnv('_APP_DATABASE_DOCUMENTSDB_KEYS', '');
$databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE');
$dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb');
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')));
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')));
break;
case VECTORSDB:
$databases = Config::getParam('pools-vectorsdb', []);
$databaseKeys = System::getEnv('_APP_DATABASE_VECTORSDB_KEYS', '');
$databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE');
$dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql');
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')));
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')));
break;
default:
// legacy/tablesdb
@@ -108,7 +108,7 @@ class Create extends Action
if ($index !== false) {
$selectedDsn = $databases[$index];
} else {
if (!empty($dsn)) {
if (!empty($dsn) && !empty($databaseSharedTables)) {
$beforeFilter = \array_values($databases);
if ($isSharedTablesV1) {
$databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1));
@@ -118,7 +118,10 @@ class Create extends Action
$databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables));
}
}
$selectedDsn = !empty($databases) ? $databases[array_rand($databases)] : '';
if (empty($databases)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "No {$databasetype} database pool available for the current shared-tables mode");
}
$selectedDsn = $databases[array_rand($databases)];
}
if (\in_array($selectedDsn, $databaseSharedTables)) {
@@ -113,6 +113,7 @@ class XList extends Action
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -182,19 +182,33 @@ class Update extends Action
$dbForDatabases = $getDatabasesDB($databaseDoc);
try {
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'committing',
])));
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
'transactions',
$transactionId,
new Document(['status' => 'committing'])
));
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
Query::equal('transactionInternalId', [$transaction->getSequence()]),
Query::orderAsc(),
Query::limit(PHP_INT_MAX),
]));
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
Query::equal('transactionInternalId', [$transaction->getSequence()]),
Query::orderAsc(),
Query::limit(PHP_INT_MAX),
]));
$collections = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
$collectionInternalId = $operation['collectionInternalId'];
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
if (!isset($collections[$collectionId])) {
$collections[$collectionId] = $authorization->skip(
fn () => $dbForProject->getCollection($collectionId)
);
}
}
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $transactionState, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $collections) {
$state = [];
$collections = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
@@ -210,11 +224,6 @@ class Update extends Action
$data = $data->getArrayCopy();
}
if (!isset($collections[$collectionId])) {
$collections[$collectionId] = $authorization->skip(
fn () => $dbForProject->getCollection($collectionId)
);
}
$collection = $collections[$collectionId];
if (\is_array($data) && !empty($data)) {
@@ -276,16 +285,17 @@ class Update extends Action
}
}
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
'transactions',
$transactionId,
new Document(['status' => 'committed'])
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
});
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
'transactions',
$transactionId,
new Document(['status' => 'committed'])
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
} catch (NotFoundException $e) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'failed',
@@ -34,6 +34,12 @@ class Create extends DocumentCreate
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return true;
}
public function __construct()
{
$this
@@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listDocumentsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/documentsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listDocumentsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/documentsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -58,6 +58,7 @@ class Update extends CollectionUpdate
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
->param('purge', false, new Boolean(true), 'When true, purge all cached list responses for this collection as part of the update. Use this to force readers to see fresh data immediately instead of waiting for the cache TTL to expire.', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
@@ -107,6 +107,7 @@ class XList extends Action
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -57,7 +57,7 @@ class XList extends DocumentXList
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours).', true)
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for caching list responses. Responses are stored in an in-memory key-value cache, keyed per project, table, schema version (columns and indexes), caller authorization roles, and the exact query — so users with different permissions never share cached entries. Schema changes invalidate cached entries automatically; row writes do not, so choose a TTL you are comfortable serving as stale data. Set to 0 to disable caching. Must be between 0 and 86400 (24 hours).', true)
->inject('response')
->inject('dbForProject')
->inject('user')
@@ -60,6 +60,7 @@ class Update extends CollectionUpdate
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('rowSecurity', false, new Boolean(true), 'Enables configuring permissions for individual rows. A user needs one of row or table-level permissions to access a row. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is table enabled? When set to \'disabled\', users cannot access the table but Server SDKs with and API key can still read and write to the table. No data is lost when this is toggled.', true)
->param('purge', false, new Boolean(true), 'When true, purge all cached list responses for this table as part of the update. Use this to force readers to see fresh data immediately instead of waiting for the cache TTL to expire.', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
@@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listVectorsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/vectorsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,57 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listVectorsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/vectorsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -12,7 +12,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\B
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Create as CreateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Delete as DeleteRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Get as GetRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs\XList as ListRowLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Update as UpdateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Upsert as UpsertRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\XList as ListRows;
@@ -21,7 +20,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Cre
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Delete as DeleteColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Get as GetColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\XList as ListColumnIndexes;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs\XList as ListTableLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Update as UpdateTable;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Usage\Get as GetTableUsage;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\XList as ListTables;
@@ -69,7 +67,6 @@ class DocumentsDB extends Base
$service->addAction(UpdateTable::getName(), new UpdateTable());
$service->addAction(DeleteTable::getName(), new DeleteTable());
$service->addAction(ListTables::getName(), new ListTables());
$service->addAction(ListTableLogs::getName(), new ListTableLogs());
$service->addAction(GetTableUsage::getName(), new GetTableUsage());
}
@@ -92,7 +89,6 @@ class DocumentsDB extends Base
$service->addAction(DeleteRow::getName(), new DeleteRow());
$service->addAction(DeleteRows::getName(), new DeleteRows());
$service->addAction(ListRows::getName(), new ListRows());
$service->addAction(ListRowLogs::getName(), new ListRowLogs());
$service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn());
$service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn());
}
@@ -10,7 +10,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Bul
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Create as CreateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Delete as DeleteDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Get as GetDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs\XList as ListDocumentLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Update as UpdateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Upsert as UpsertDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\XList as ListDocuments;
@@ -19,7 +18,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Creat
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Delete as DeleteIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Get as GetIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\XList as ListIndexes;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs\XList as ListCollectionLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Update as UpdateCollection;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Usage\Get as GetCollectionUsage;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\XList as ListCollections;
@@ -69,7 +67,6 @@ class VectorsDB extends Base
$service->addAction(UpdateCollection::getName(), new UpdateCollection());
$service->addAction(DeleteCollection::getName(), new DeleteCollection());
$service->addAction(ListCollections::getName(), new ListCollections());
$service->addAction(ListCollectionLogs::getName(), new ListCollectionLogs());
$service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage());
}
@@ -92,7 +89,6 @@ class VectorsDB extends Base
$service->addAction(UpdateDocuments::getName(), new UpdateDocuments());
$service->addAction(UpsertDocuments::getName(), new UpsertDocuments());
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
$service->addAction(ListDocumentLogs::getName(), new ListDocumentLogs());
}
private function registerTransactionActions(Service $service): void
@@ -650,26 +650,30 @@ class Databases extends Action
Document|null $attribute = null,
Document|null $index = null,
): void {
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('databaseId', $database->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId());
try {
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('databaseId', $database->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId());
if (! empty($attribute)) {
$queueForRealtime
->setParam('columnId', $attribute->getId())
->setParam('attributeId', $attribute->getId())
->setPayload($attribute->getArrayCopy());
}
if (! empty($index)) {
$queueForRealtime
->setParam('indexId', $index->getId())
->setPayload($index->getArrayCopy());
if (! empty($attribute)) {
$queueForRealtime
->setParam('columnId', $attribute->getId())
->setParam('attributeId', $attribute->getId())
->setPayload($attribute->getArrayCopy());
}
if (! empty($index)) {
$queueForRealtime
->setParam('indexId', $index->getId())
->setPayload($index->getArrayCopy());
}
$queueForRealtime->trigger();
} finally {
$queueForRealtime->reset();
}
$queueForRealtime->trigger();
}
}
@@ -305,9 +305,7 @@ class Create extends Base
if ($async) {
if (is_null($scheduledAt)) {
if ($project->getId() != '6862e6a6000cce69f9da') {
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
$queueForFunctions
->setType('http')
->setExecution($execution)
@@ -348,9 +346,7 @@ class Create extends Base
->setAttribute('scheduleInternalId', $schedule->getSequence())
->setAttribute('scheduledAt', $scheduledAt);
if ($project->getId() != '6862e6a6000cce69f9da') {
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
@@ -516,9 +512,7 @@ class Create extends Base
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
;
if ($project->getId() != '6862e6a6000cce69f9da') {
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$executionResponse['headers']['x-appwrite-execution-id'] = $execution->getId();
@@ -6,9 +6,9 @@ use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Usage\Context;
@@ -58,7 +58,7 @@ class Builds extends Action
->inject('project')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('queueForScreenshots')
->inject('publisherForScreenshots')
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
@@ -84,7 +84,7 @@ class Builds extends Action
Document $project,
Database $dbForPlatform,
Event $queueForEvents,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
@@ -126,7 +126,7 @@ class Builds extends Action
$deviceForFunctions,
$deviceForSites,
$deviceForFiles,
$queueForScreenshots,
$publisherForScreenshots,
$queueForWebhooks,
$queueForFunctions,
$queueForRealtime,
@@ -161,7 +161,7 @@ class Builds extends Action
Device $deviceForFunctions,
Device $deviceForSites,
Device $deviceForFiles,
Screenshot $queueForScreenshots,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
@@ -1120,10 +1120,10 @@ class Builds extends Action
/** Screenshot site */
if ($resource->getCollection() === 'sites') {
$queueForScreenshots
->setDeploymentId($deployment->getId())
->setProject($project)
->trigger();
$publisherForScreenshots->enqueue(new \Appwrite\Event\Message\Screenshot(
project: $project,
deploymentId: $deployment->getId(),
));
Console::log('Site screenshot queued');
}
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Functions\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Message\Screenshot;
use Appwrite\Event\Realtime;
use Appwrite\Permission;
use Appwrite\Role;
@@ -62,9 +63,11 @@ class Screenshots extends Action
throw new \Exception('Missing payload');
}
$screenshotMessage = Screenshot::fromArray($payload);
Console::log('Site screenshot started');
$deploymentId = $payload['deploymentId'] ?? null;
$deploymentId = $screenshotMessage->deploymentId;
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {

Some files were not shown because too many files have changed in this diff Show More