mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
310
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f79e846cb2 | ||
|
|
9ed02eb194 | ||
|
|
a39627e555 | ||
|
|
5138ebec7b | ||
|
|
ca30269486 | ||
|
|
6ae070c3f1 | ||
|
|
233c4761d6 | ||
|
|
d7174d0de3 | ||
|
|
66c83162d3 | ||
|
|
8b297f5a89 | ||
|
|
4604c77939 | ||
|
|
ef46367fa3 | ||
|
|
3078e6bff5 | ||
|
|
80643faacc | ||
|
|
cd75f17815 | ||
|
|
7b9970af9c | ||
|
|
2544baa669 | ||
|
|
4d6a3d36f7 | ||
|
|
ab936a7ce4 | ||
|
|
b671bfb1c7 | ||
|
|
a70d67810e | ||
|
|
762be367e1 | ||
|
|
f3b2562add | ||
|
|
0d26a2a2dd | ||
|
|
124ee0df8f | ||
|
|
802379366a | ||
|
|
9b8dcfda07 | ||
|
|
e9698c96f0 | ||
|
|
ddbddafbf9 | ||
|
|
86a4bfa74e | ||
|
|
8124b07860 | ||
|
|
a7898b3d5b | ||
|
|
5d24b51421 | ||
|
|
0203323b4a | ||
|
|
94e29cff53 | ||
|
|
72def3b2fb | ||
|
|
d015a75e81 | ||
|
|
1306c85eb5 | ||
|
|
c3ea66b37a | ||
|
|
72ce068714 | ||
|
|
ea76213dd2 | ||
|
|
12b79363d1 | ||
|
|
0723101397 | ||
|
|
d56a3c1534 | ||
|
|
b86d7ac3fa | ||
|
|
b1fab79dc4 | ||
|
|
2f066a6ba8 | ||
|
|
e8ca0610ee | ||
|
|
15caa27977 | ||
|
|
5f22022527 | ||
|
|
b9c7c172ad | ||
|
|
c2bf1e8040 | ||
|
|
cda03f63ab | ||
|
|
f5a61fb4d6 | ||
|
|
429f73eb85 | ||
|
|
da871635d9 | ||
|
|
ccaea5d010 | ||
|
|
b5e9c1786a | ||
|
|
79e150b7b2 | ||
|
|
beee5e721e | ||
|
|
0ef4bf21cc | ||
|
|
b2486fcb6c | ||
|
|
c67b77bca0 | ||
|
|
ce91b1a03d | ||
|
|
fbf390c710 | ||
|
|
991f5ff9fd | ||
|
|
e8d8373922 | ||
|
|
0fee1f0ffd | ||
|
|
eb07e99225 | ||
|
|
325968c52a | ||
|
|
39e66711c8 | ||
|
|
42a1c8da38 | ||
|
|
206bd63620 | ||
|
|
75696a21bf | ||
|
|
48866acdb7 | ||
|
|
b1171c661e | ||
|
|
a0e4e89621 | ||
|
|
728ed57df0 | ||
|
|
629479d275 | ||
|
|
5c922f89fe | ||
|
|
6e292349b9 | ||
|
|
7b940e3a17 | ||
|
|
7ab3debb10 | ||
|
|
c083e1ce74 | ||
|
|
61e886eed4 | ||
|
|
71ac9c7264 | ||
|
|
f78d5523ce | ||
|
|
955e1bcbe9 | ||
|
|
09a337aa1b | ||
|
|
2cfb5ecfd9 | ||
|
|
38687bc24e | ||
|
|
77b2a57968 | ||
|
|
223bb7f08a | ||
|
|
b73abf69d0 | ||
|
|
e76ed98590 | ||
|
|
7b10fe1371 | ||
|
|
e4abc0e0da | ||
|
|
6478499f1c | ||
|
|
3bbfbdb063 | ||
|
|
bc6ecbd22c | ||
|
|
4d2f631393 | ||
|
|
479a583ff5 | ||
|
|
08cd610823 | ||
|
|
6cd19bf330 | ||
|
|
d6d8729983 | ||
|
|
6c866be9f3 | ||
|
|
0ec515779e | ||
|
|
91240e0ca7 | ||
|
|
e3ae7daab4 | ||
|
|
f482e6de68 | ||
|
|
c12cad80bb | ||
|
|
2a17429226 | ||
|
|
e2627d784b | ||
|
|
5c915ef92f | ||
|
|
5697d983d0 | ||
|
|
a5fb10c61d | ||
|
|
5469b78367 | ||
|
|
e887859cc0 | ||
|
|
942cfc4591 | ||
|
|
321fc8ee70 | ||
|
|
568ac54d3d | ||
|
|
432e86ec07 | ||
|
|
fef1b5b5e5 | ||
|
|
bfac0f92af | ||
|
|
ae6df78020 | ||
|
|
23d84cf29f | ||
|
|
c79019f960 | ||
|
|
dd78b6dbce | ||
|
|
81e64afe7a | ||
|
|
5ef675bcdd | ||
|
|
a4734a5de7 | ||
|
|
03812cf7bf | ||
|
|
515a34c180 | ||
|
|
8e688bad4d | ||
|
|
4905f7cc48 | ||
|
|
3d27b76051 | ||
|
|
2d6348bf5d | ||
|
|
2131a0cea1 | ||
|
|
9379e4d6c8 | ||
|
|
15922fb88c | ||
|
|
ba9526cce0 | ||
|
|
6dbed494e3 | ||
|
|
4eb1e7ae45 | ||
|
|
bdd53212a0 | ||
|
|
1a69d9d236 | ||
|
|
a860c33969 | ||
|
|
ffd73dc70f | ||
|
|
362a7bef0e | ||
|
|
d08a6f572c | ||
|
|
893c1aa669 | ||
|
|
c34873a8b0 | ||
|
|
17f60b611b | ||
|
|
445d7495a3 | ||
|
|
aebbe91c34 | ||
|
|
2a9f9f6851 | ||
|
|
eec023aab3 | ||
|
|
71f389e1c0 | ||
|
|
d71b289025 | ||
|
|
f4da9b54e7 | ||
|
|
df84b94df0 | ||
|
|
42bf515c8a | ||
|
|
a7d39b5ef7 | ||
|
|
4c1417944d | ||
|
|
27aee1fb88 | ||
|
|
4319c16584 | ||
|
|
d68fc9a22e | ||
|
|
e3d6fc123f | ||
|
|
e6505b9cf9 | ||
|
|
c1ab9da740 | ||
|
|
b567cd5341 | ||
|
|
f2ac01bccf | ||
|
|
eecfba2a72 | ||
|
|
2cfaa2223e | ||
|
|
1a87bde88e | ||
|
|
4b92e3781f | ||
|
|
65bb8946f6 | ||
|
|
db4dcd164e | ||
|
|
384c47f436 | ||
|
|
dcc926402d | ||
|
|
6969e446d5 | ||
|
|
c8bb939357 | ||
|
|
02488c853d | ||
|
|
92f26c1b5a | ||
|
|
ded06d7435 | ||
|
|
6c59cef8f4 | ||
|
|
46072bb95e | ||
|
|
904b7312d6 | ||
|
|
68fc6b0600 | ||
|
|
47ea8699d1 | ||
|
|
9ba3f6dfe8 | ||
|
|
3483daef0d | ||
|
|
9943305640 | ||
|
|
8bb6f01e22 | ||
|
|
2cc7bbc0a4 | ||
|
|
e9366ac2e6 | ||
|
|
22f8a3eab9 | ||
|
|
896e5a517a | ||
|
|
281dcfc64a | ||
|
|
457cd53475 | ||
|
|
93e7dc772d | ||
|
|
228d095ee5 | ||
|
|
43189f8940 | ||
|
|
b615e4b901 | ||
|
|
ebd6457361 | ||
|
|
0d3bcc9b3a | ||
|
|
61e98a501a | ||
|
|
502012ddf7 | ||
|
|
a6eb479c93 | ||
|
|
e0d7b418dc | ||
|
|
ea97072479 | ||
|
|
7573ee75a2 | ||
|
|
0dc00eb2c5 | ||
|
|
b8ccc3b846 | ||
|
|
6f1cac7d76 | ||
|
|
5f3384b821 | ||
|
|
71cb462f49 | ||
|
|
27e859030d | ||
|
|
e7a82e4d31 | ||
|
|
6dc836307e | ||
|
|
0582cdf394 | ||
|
|
573d8423a3 | ||
|
|
3fa3318346 | ||
|
|
23dfb23a3b | ||
|
|
fc5ea06821 | ||
|
|
5642983f91 | ||
|
|
b208e5c066 | ||
|
|
aa135d5cd4 | ||
|
|
6756ee31b6 | ||
|
|
89c988d73c | ||
|
|
6d85d1567e | ||
|
|
7567639996 | ||
|
|
057bff2140 | ||
|
|
d7bb234072 | ||
|
|
c0f8dee4d4 | ||
|
|
eb10996517 | ||
|
|
31803f0eb9 | ||
|
|
ac9214f3c4 | ||
|
|
14347d86a8 | ||
|
|
1b855d2d41 | ||
|
|
3b5b15d1a6 | ||
|
|
2c5bc32f7e | ||
|
|
59b41c4b52 | ||
|
|
b6e8b55994 | ||
|
|
3a4fb5dd14 | ||
|
|
2a679ead32 | ||
|
|
7da9d480d0 | ||
|
|
15de315cae | ||
|
|
a37aa2dd68 | ||
|
|
b29f70a0af | ||
|
|
2fc5e56a61 | ||
|
|
b1373c02bc | ||
|
|
1db12e78ef | ||
|
|
a25c8004e4 | ||
|
|
547d59d95e | ||
|
|
4e0477af32 | ||
|
|
1659bc3ffa | ||
|
|
84e9c8243c | ||
|
|
020d7bb801 | ||
|
|
b6aeaffe8b | ||
|
|
65883122e3 | ||
|
|
cd651dbdb8 | ||
|
|
2dfb386b12 | ||
|
|
e9dac6710f | ||
|
|
d4660a70c3 | ||
|
|
16f9c35850 | ||
|
|
49511ebdd9 | ||
|
|
63511c56ae | ||
|
|
445ada0226 | ||
|
|
a5d4f69c6c | ||
|
|
f788fc8f8a | ||
|
|
67ef2ab552 | ||
|
|
68b4a49e9d | ||
|
|
28a09d7aaa | ||
|
|
ee3b2d75ae | ||
|
|
ffe1fd89a7 | ||
|
|
06c4ba81e9 | ||
|
|
f2772cea6b | ||
|
|
b76a4bdd2f | ||
|
|
1c94381830 | ||
|
|
297fae8f81 | ||
|
|
3e403194e4 | ||
|
|
0305790e69 | ||
|
|
25435aaa11 | ||
|
|
19895e54e3 | ||
|
|
4b3fe0b6fe | ||
|
|
4ef906b836 | ||
|
|
28aa4e8a8d | ||
|
|
9c6a6c265a | ||
|
|
dc0eb5f7a7 | ||
|
|
8d1acef95d | ||
|
|
03ccca2c35 | ||
|
|
6c1f967509 | ||
|
|
3b4196735a | ||
|
|
7e315f79cc | ||
|
|
336bd44826 | ||
|
|
881d96a653 | ||
|
|
874e5f61f0 | ||
|
|
39cf207df9 | ||
|
|
b7e2606b9f | ||
|
|
bd288ce650 | ||
|
|
9f2105b294 | ||
|
|
b3944c678c | ||
|
|
31c8c09060 | ||
|
|
4283671d49 | ||
|
|
c3a3717bde | ||
|
|
5cdb59142f | ||
|
|
bf6f784826 | ||
|
|
33f7fd259b | ||
|
|
bc99e04b57 | ||
|
|
10b8f97e96 |
@@ -102,12 +102,15 @@ _APP_STATS_RESOURCES_INTERVAL=30
|
||||
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
|
||||
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
|
||||
_APP_INTERVAL_DOMAIN_VERIFICATION=60
|
||||
_APP_INTERVAL_CLEANUP_STALE_EXECUTIONS=300
|
||||
_APP_USAGE_STATS=enabled
|
||||
_APP_LOGGING_CONFIG=
|
||||
_APP_LOGGING_CONFIG_REALTIME=
|
||||
_APP_GRAPHQL_INTROSPECTION=enabled
|
||||
_APP_GRAPHQL_MAX_BATCH_SIZE=10
|
||||
_APP_GRAPHQL_MAX_COMPLEXITY=250
|
||||
_APP_GRAPHQL_MAX_DEPTH=4
|
||||
_APP_GRAPHQL_SCHEMA_CACHE_MB=50
|
||||
_APP_DOCKER_HUB_USERNAME=
|
||||
_APP_DOCKER_HUB_PASSWORD=
|
||||
_APP_VCS_GITHUB_APP_NAME=
|
||||
@@ -126,4 +129,4 @@ _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
|
||||
_APP_PROJECT_REGIONS=default
|
||||
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
|
||||
_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
|
||||
_APP_TRUSTED_HEADERS=x-forwarded-for
|
||||
_APP_TRUSTED_HEADERS=x-forwarded-for
|
||||
|
||||
+11
-11
@@ -223,7 +223,7 @@ jobs:
|
||||
-e _APP_DATABASE_SHARED_TABLES \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1 \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
|
||||
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -312,7 +312,7 @@ jobs:
|
||||
-e _APP_DATABASE_SHARED_TABLES \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1 \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
|
||||
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -322,8 +322,8 @@ jobs:
|
||||
echo "=== OpenRuntimes Executor Logs ==="
|
||||
docker compose logs openruntimes-executor
|
||||
|
||||
e2e_dev_keys:
|
||||
name: E2E Service Test (Dev Keys)
|
||||
e2e_abuse_enabled:
|
||||
name: E2E Service Test (Abuse enabled)
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
steps:
|
||||
@@ -344,7 +344,7 @@ jobs:
|
||||
docker compose up -d
|
||||
sleep 30
|
||||
|
||||
- name: Run Projects tests with dev keys in dedicated table mode
|
||||
- name: Run Projects tests in dedicated table mode
|
||||
run: |
|
||||
echo "Using project tables"
|
||||
export _APP_DATABASE_SHARED_TABLES=
|
||||
@@ -354,7 +354,7 @@ jobs:
|
||||
-e _APP_DATABASE_SHARED_TABLES \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1 \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -364,8 +364,8 @@ jobs:
|
||||
echo "=== OpenRuntimes Executor Logs ==="
|
||||
docker compose logs openruntimes-executor
|
||||
|
||||
e2e_dev_keys_shared_mode:
|
||||
name: E2E Shared Mode Service Test (Dev Keys)
|
||||
e2e_abuse_enabled_shared_mode:
|
||||
name: E2E Shared Mode Service Test (Abuse enabled)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ setup, check_database_changes ]
|
||||
if: needs.check_database_changes.outputs.database_changed == 'true'
|
||||
@@ -394,7 +394,7 @@ jobs:
|
||||
docker compose up -d
|
||||
sleep 30
|
||||
|
||||
- name: Run Projects tests with dev keys in ${{ matrix.tables-mode }} table mode
|
||||
- name: Run Projects tests in ${{ matrix.tables-mode }} table mode
|
||||
run: |
|
||||
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
|
||||
echo "Using shared tables V1"
|
||||
@@ -410,7 +410,7 @@ jobs:
|
||||
-e _APP_DATABASE_SHARED_TABLES \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1 \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -420,7 +420,7 @@ jobs:
|
||||
echo "=== OpenRuntimes Executor Logs ==="
|
||||
docker compose logs openruntimes-executor
|
||||
|
||||
e2e_screenshots_keys:
|
||||
e2e_screenshots:
|
||||
name: E2E Service Test (Site Screenshots)
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
|
||||
+98
@@ -1,3 +1,101 @@
|
||||
# Version 1.8.1
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Notable changes
|
||||
|
||||
* Add branch deployments support in [#10486](https://github.com/appwrite/appwrite/pull/10486)
|
||||
* Add TanStack Start sites support in [#10681](https://github.com/appwrite/appwrite/pull/10681)
|
||||
* Add Next.js standalone support in [#10747](https://github.com/appwrite/appwrite/pull/10747)
|
||||
* Add Resend integration in [#10690](https://github.com/appwrite/appwrite/pull/10690)
|
||||
* Add option to enable/disable image transformations per-bucket in [#10722](https://github.com/appwrite/appwrite/pull/10722)
|
||||
* Add operators support in [#10735](https://github.com/appwrite/appwrite/pull/10735) and [#10800](https://github.com/appwrite/appwrite/pull/10800)
|
||||
* Add function and sites stats in [#10786](https://github.com/appwrite/appwrite/pull/10786)
|
||||
* Add disable count feature in [#10668](https://github.com/appwrite/appwrite/pull/10668)
|
||||
* Add ElevenLabs site template in [#10782](https://github.com/appwrite/appwrite/pull/10782)
|
||||
* Add suggested environment variables in [#10795](https://github.com/appwrite/appwrite/pull/10795)
|
||||
* Update GeoDB database in [#10890](https://github.com/appwrite/appwrite/pull/10890)
|
||||
* Update Flutter default build runtime in [#10807](https://github.com/appwrite/appwrite/pull/10807)
|
||||
* Upgrade runtimes in [#10804](https://github.com/appwrite/appwrite/pull/10804)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix duplicate document error while creating file in [#10891](https://github.com/appwrite/appwrite/pull/10891)
|
||||
* Fix "Update external deployment (authorize)" throwing 500 error due to invalid query in [#10888](https://github.com/appwrite/appwrite/pull/10888)
|
||||
* Fix error setting user password in [#10889](https://github.com/appwrite/appwrite/pull/10889)
|
||||
* Fix error generating email MFA challenges in [#10884](https://github.com/appwrite/appwrite/pull/10884)
|
||||
* Fix file token expiry in [#10877](https://github.com/appwrite/appwrite/pull/10877)
|
||||
* Fix TanStack Nitro default in [#10860](https://github.com/appwrite/appwrite/pull/10860)
|
||||
* Fix TanStack builds in [#10767](https://github.com/appwrite/appwrite/pull/10767)
|
||||
* Fix nullable validation in [#10819](https://github.com/appwrite/appwrite/pull/10819) and [#10778](https://github.com/appwrite/appwrite/pull/10778)
|
||||
* Fix WebP library in [#10738](https://github.com/appwrite/appwrite/pull/10738)
|
||||
* Fix batch writes in [#10812](https://github.com/appwrite/appwrite/pull/10812)
|
||||
* Fix error handler error in [#10719](https://github.com/appwrite/appwrite/pull/10719)
|
||||
* Fix Next 16 compatibility in [#10713](https://github.com/appwrite/appwrite/pull/10713)
|
||||
* Fix stats usage memory leak in [#10683](https://github.com/appwrite/appwrite/pull/10683)
|
||||
* Fix author URL in template deployments in [#10535](https://github.com/appwrite/appwrite/pull/10535)
|
||||
* Fix VCS lock deletion in [#10691](https://github.com/appwrite/appwrite/pull/10691)
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* Add CSV export functionality in [#10546](https://github.com/appwrite/appwrite/pull/10546), [#10750](https://github.com/appwrite/appwrite/pull/10750), [#10813](https://github.com/appwrite/appwrite/pull/10813), and [#10847](https://github.com/appwrite/appwrite/pull/10847)
|
||||
* Add JWT disposition in [#10867](https://github.com/appwrite/appwrite/pull/10867)
|
||||
* Add screenshots endpoint in [#10675](https://github.com/appwrite/appwrite/pull/10675)
|
||||
* Add screenshot endpoint stats in [#10706](https://github.com/appwrite/appwrite/pull/10706)
|
||||
* Add users attributes in [#10688](https://github.com/appwrite/appwrite/pull/10688)
|
||||
* Add max build duration environment variable in [#10674](https://github.com/appwrite/appwrite/pull/10674)
|
||||
* Add custom realtime logger in [#10871](https://github.com/appwrite/appwrite/pull/10871)
|
||||
* Add logs in [#10869](https://github.com/appwrite/appwrite/pull/10869)
|
||||
* Improve MFA docs endpoint order in [#10793](https://github.com/appwrite/appwrite/pull/10793)
|
||||
* Auth refactor in [#10758](https://github.com/appwrite/appwrite/pull/10758), [#10837](https://github.com/appwrite/appwrite/pull/10837), [#10682](https://github.com/appwrite/appwrite/pull/10682), and [#10667](https://github.com/appwrite/appwrite/pull/10667)
|
||||
* Bump assistant to 0.8.4 in [#10887](https://github.com/appwrite/appwrite/pull/10887)
|
||||
* Bump database to 3.1.5 in [#10766](https://github.com/appwrite/appwrite/pull/10766)
|
||||
* Bump Utopia DNS in [#10761](https://github.com/appwrite/appwrite/pull/10761)
|
||||
* Update domains to 0.8.3 in [#10658](https://github.com/appwrite/appwrite/pull/10658)
|
||||
* Update domains to 0.9.1 in [#10678](https://github.com/appwrite/appwrite/pull/10678)
|
||||
* Update Apple Swift to 13.3.0 in [#10679](https://github.com/appwrite/appwrite/pull/10679)
|
||||
* Update Apple Swift in [#10663](https://github.com/appwrite/appwrite/pull/10663)
|
||||
* Update CLI to 10.2.2 in [#10672](https://github.com/appwrite/appwrite/pull/10672)
|
||||
* Update to CLI 12.0.0 in [#10853](https://github.com/appwrite/appwrite/pull/10853)
|
||||
* Update docs examples to use Permission class in [#10707](https://github.com/appwrite/appwrite/pull/10707)
|
||||
* Update SDK examples docs in [#10855](https://github.com/appwrite/appwrite/pull/10855)
|
||||
* Release Python SDK in [#10762](https://github.com/appwrite/appwrite/pull/10762)
|
||||
* Release Flutter 20.3.2 in [#10838](https://github.com/appwrite/appwrite/pull/10838)
|
||||
* Release Flutter/Dart add screenshot examples in [#10811](https://github.com/appwrite/appwrite/pull/10811)
|
||||
* Release PHP CLI in [#10791](https://github.com/appwrite/appwrite/pull/10791)
|
||||
* Release SDKs in [#10817](https://github.com/appwrite/appwrite/pull/10817)
|
||||
* Update SDKs in [#10694](https://github.com/appwrite/appwrite/pull/10694), [#10729](https://github.com/appwrite/appwrite/pull/10729), and [#10744](https://github.com/appwrite/appwrite/pull/10744)
|
||||
* Update SDK generator in [#10743](https://github.com/appwrite/appwrite/pull/10743)
|
||||
* Update database in [#10664](https://github.com/appwrite/appwrite/pull/10664)
|
||||
* Update README file in [#10763](https://github.com/appwrite/appwrite/pull/10763)
|
||||
* SDK release documentation in [#10745](https://github.com/appwrite/appwrite/pull/10745)
|
||||
* SDK release runtime config in [#10765](https://github.com/appwrite/appwrite/pull/10765)
|
||||
* Sync specs in [#10789](https://github.com/appwrite/appwrite/pull/10789)
|
||||
* Sync 1.8.0 in [#10677](https://github.com/appwrite/appwrite/pull/10677)
|
||||
* Add workflow for issue triage in [#10718](https://github.com/appwrite/appwrite/pull/10718)
|
||||
* Add issue auto-labeler in [#10700](https://github.com/appwrite/appwrite/pull/10700)
|
||||
* Add AI moderator repo in [#10717](https://github.com/appwrite/appwrite/pull/10717)
|
||||
* Browser bump in [#10850](https://github.com/appwrite/appwrite/pull/10850)
|
||||
* Template type enum override in [#10848](https://github.com/appwrite/appwrite/pull/10848)
|
||||
* VCS reference type in [#10852](https://github.com/appwrite/appwrite/pull/10852)
|
||||
* Index scope description in [#10851](https://github.com/appwrite/appwrite/pull/10851)
|
||||
* Config for environment in [#10833](https://github.com/appwrite/appwrite/pull/10833)
|
||||
* Format instance in [#10830](https://github.com/appwrite/appwrite/pull/10830)
|
||||
* Replace sleep in webhooks service in [#10656](https://github.com/appwrite/appwrite/pull/10656)
|
||||
* Update email composer in [#10720](https://github.com/appwrite/appwrite/pull/10720)
|
||||
* Update facts on GitHub sites and functions in [#10593](https://github.com/appwrite/appwrite/pull/10593) and [#10771](https://github.com/appwrite/appwrite/pull/10771)
|
||||
* Fix wrong user type in [#10875](https://github.com/appwrite/appwrite/pull/10875)
|
||||
* Fix limit and offset computation in [#10880](https://github.com/appwrite/appwrite/pull/10880)
|
||||
* Fix enum examples in [#10828](https://github.com/appwrite/appwrite/pull/10828)
|
||||
* Fix response models multi-methods in [#10815](https://github.com/appwrite/appwrite/pull/10815)
|
||||
* Fix undefined variable in [#10654](https://github.com/appwrite/appwrite/pull/10654)
|
||||
* Fix undefined sequence in [#10652](https://github.com/appwrite/appwrite/pull/10652)
|
||||
* Fix description in [#10702](https://github.com/appwrite/appwrite/pull/10702)
|
||||
* Fix warning in builds worker in [#10705](https://github.com/appwrite/appwrite/pull/10705)
|
||||
* Fix sites create deployment docs in [#10566](https://github.com/appwrite/appwrite/pull/10566)
|
||||
* Fix test dependencies projects in [#10655](https://github.com/appwrite/appwrite/pull/10655)
|
||||
* Fix list sites test in [#10726](https://github.com/appwrite/appwrite/pull/10726)
|
||||
|
||||
# Version 1.8.0
|
||||
|
||||
## What's Changed
|
||||
|
||||
@@ -77,6 +77,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/queue-count-success && \
|
||||
chmod +x /usr/local/bin/worker-audits && \
|
||||
chmod +x /usr/local/bin/worker-builds && \
|
||||
chmod +x /usr/local/bin/worker-screenshots && \
|
||||
chmod +x /usr/local/bin/worker-certificates && \
|
||||
chmod +x /usr/local/bin/worker-databases && \
|
||||
chmod +x /usr/local/bin/worker-deletes && \
|
||||
|
||||
+3
-3
@@ -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.8.0
|
||||
appwrite/appwrite:1.8.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.8.0
|
||||
appwrite/appwrite:1.8.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.8.0
|
||||
appwrite/appwrite:1.8.1
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -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.8.0
|
||||
appwrite/appwrite:1.8.1
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -87,7 +87,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.8.0
|
||||
appwrite/appwrite:1.8.1
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -97,7 +97,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.8.0
|
||||
appwrite/appwrite:1.8.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.
|
||||
|
||||
+21
-10
@@ -41,8 +41,6 @@ Config::setParam('runtimes', (new Runtimes('v5'))->getAll(supported: false));
|
||||
// require controllers after overwriting runtimes
|
||||
require_once __DIR__ . '/controllers/general.php';
|
||||
|
||||
Authorization::disable();
|
||||
|
||||
CLI::setResource('register', fn () => $register);
|
||||
|
||||
CLI::setResource('cache', function ($pools) {
|
||||
@@ -60,7 +58,13 @@ CLI::setResource('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
CLI::setResource('dbForPlatform', function ($pools, $cache) {
|
||||
CLI::setResource('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
CLI::setResource('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
$sleep = 3;
|
||||
$maxAttempts = 5;
|
||||
$attempts = 0;
|
||||
@@ -74,6 +78,8 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
|
||||
$dbForPlatform = new Database($adapter, $cache);
|
||||
|
||||
$dbForPlatform
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', 'console');
|
||||
@@ -99,7 +105,7 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) {
|
||||
}
|
||||
|
||||
return $dbForPlatform;
|
||||
}, ['pools', 'cache']);
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
CLI::setResource('console', function () {
|
||||
return new Document(Config::getParam('console'));
|
||||
@@ -110,10 +116,10 @@ CLI::setResource(
|
||||
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
|
||||
);
|
||||
|
||||
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
|
||||
CLI::setResource('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, &$databases) {
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
@@ -146,6 +152,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
|
||||
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$databases[$dsn->getHost()] = $database;
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
@@ -162,17 +169,19 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId());
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache']);
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
CLI::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $database) {
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int)$project->getSequence());
|
||||
return $database;
|
||||
@@ -182,6 +191,8 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK)
|
||||
@@ -194,7 +205,7 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache']);
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
CLI::setResource('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
|
||||
@@ -1288,6 +1288,17 @@ return [
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('labels'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
|
||||
@@ -330,7 +330,18 @@ $platformCollections = [
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('labels'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => [],
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
@@ -1402,21 +1413,21 @@ $platformCollections = [
|
||||
'$id' => '_key_type',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['type'],
|
||||
'lengths' => [32],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_trigger',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['trigger'],
|
||||
'lengths' => [32],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_deploymentResourceType',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['deploymentResourceType'],
|
||||
'lengths' => [32],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
@@ -1458,23 +1469,23 @@ $platformCollections = [
|
||||
'$id' => ID::custom('_key_owner'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['owner'],
|
||||
'lengths' => [16],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_region'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['region'],
|
||||
'lengths' => [16],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_piid_riid_rt'),
|
||||
'$id' => ID::custom('_key_piid_diid_drt'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['projectInternalId', 'deploymentInternalId', 'deploymentResourceType'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_region_status_createdAt',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['region', 'status', '$createdAt'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -567,6 +567,17 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('deploymentRetention'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('deploymentInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
@@ -765,6 +776,17 @@ return [
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('startCommand'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 20000,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('specification'),
|
||||
@@ -776,6 +798,28 @@ return [
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('buildSpecification'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('runtimeSpecification'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('scopes'),
|
||||
'type' => Database::VAR_STRING,
|
||||
@@ -1035,6 +1079,17 @@ return [
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('startCommand'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 20000,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('fallbackFile'),
|
||||
'type' => Database::VAR_STRING,
|
||||
@@ -1046,6 +1101,17 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('deploymentRetention'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('deploymentInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
@@ -1200,6 +1266,28 @@ return [
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('buildSpecification'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('runtimeSpecification'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('buildRuntime'),
|
||||
'type' => Database::VAR_STRING,
|
||||
@@ -1357,6 +1445,17 @@ return [
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('startCommand'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 20000,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('buildOutput'),
|
||||
@@ -2098,6 +2197,13 @@ return [
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_resourceType'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['resourceType'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -1103,7 +1103,6 @@ return [
|
||||
'name' => Exception::RULE_VERIFICATION_FAILED,
|
||||
'description' => 'Domain verification failed. Please check if your DNS records are correct and try again.',
|
||||
'code' => 400,
|
||||
'publish' => true
|
||||
],
|
||||
Exception::PROJECT_SMTP_CONFIG_INVALID => [
|
||||
'name' => Exception::PROJECT_SMTP_CONFIG_INVALID,
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ return [
|
||||
[
|
||||
'key' => 'flutter',
|
||||
'name' => 'Flutter',
|
||||
'version' => '20.3.2',
|
||||
'version' => '20.3.3',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-flutter',
|
||||
'package' => 'https://pub.dev/packages/appwrite',
|
||||
'enabled' => true,
|
||||
@@ -227,7 +227,7 @@ return [
|
||||
[
|
||||
'key' => 'cli',
|
||||
'name' => 'Command Line',
|
||||
'version' => '12.0.1',
|
||||
'version' => '13.0.1',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-cli',
|
||||
'package' => 'https://www.npmjs.com/package/appwrite-cli',
|
||||
'enabled' => true,
|
||||
@@ -377,7 +377,7 @@ return [
|
||||
[
|
||||
'key' => 'dart',
|
||||
'name' => 'Dart',
|
||||
'version' => '20.1.0',
|
||||
'version' => '20.1.1',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-dart',
|
||||
'package' => 'https://pub.dev/packages/dart_appwrite',
|
||||
'enabled' => true,
|
||||
|
||||
@@ -48,7 +48,7 @@ return [
|
||||
'name' => 'Avatars',
|
||||
'subtitle' => 'The Avatars service aims to help you complete everyday tasks related to your app image, icons, and avatars.',
|
||||
'description' => '/docs/services/avatars.md',
|
||||
'controller' => 'api/avatars.php',
|
||||
'controller' => '', // Uses modules
|
||||
'sdk' => true,
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/client/avatars',
|
||||
@@ -104,7 +104,7 @@ return [
|
||||
'name' => 'Health',
|
||||
'subtitle' => 'The Health service allows you to both validate and monitor your Appwrite server\'s health.',
|
||||
'description' => '/docs/services/health.md',
|
||||
'controller' => 'api/health.php',
|
||||
'controller' => '', // Uses modules
|
||||
'sdk' => true,
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/server/health',
|
||||
@@ -146,7 +146,7 @@ return [
|
||||
'name' => 'Storage',
|
||||
'subtitle' => 'The Storage service allows you to manage your project files.',
|
||||
'description' => '/docs/services/storage.md',
|
||||
'controller' => '',
|
||||
'controller' => '', // Uses modules
|
||||
'sdk' => true,
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/client/storage',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,4 +3,6 @@
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\System\System;
|
||||
|
||||
Image::setResourceLimit('memory', intval(System::getEnv('_APP_IMAGES_RESOURCE_LIMIT_MEMORY', 1024*1024*64)));
|
||||
if (\class_exists('Imagick')) {
|
||||
Image::setResourceLimit('memory', intval(System::getEnv('_APP_IMAGES_RESOURCE_LIMIT_MEMORY', 1024*1024*64)));
|
||||
}
|
||||
|
||||
@@ -1285,6 +1285,15 @@ return [
|
||||
'category' => 'GraphQL',
|
||||
'description' => '',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => '_APP_GRAPHQL_INTROSPECTION',
|
||||
'description' => 'Enable or disable GraphQL introspection. Set to \'enabled\' to allow schema introspection, or \'disabled\' to block it. The default value is \'enabled\'.',
|
||||
'introduction' => '',
|
||||
'default' => 'enabled',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_GRAPHQL_MAX_BATCH_SIZE',
|
||||
'description' => 'Maximum number of batched queries per request. The default value is 10.',
|
||||
@@ -1312,6 +1321,15 @@ return [
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_GRAPHQL_SCHEMA_CACHE_MB',
|
||||
'description' => 'Maximum memory in megabytes for the GraphQL schema LRU cache. Each project with database collections generates its own schema. Schemas are evicted when the cache exceeds this limit. Default is 50 MB.',
|
||||
'introduction' => '1.8.0',
|
||||
'default' => '50',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => 'integer'
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
+112
-97
@@ -33,9 +33,9 @@ use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Identities;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use libphonenumber\NumberParseException;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use MaxMind\Db\Reader;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\App;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
@@ -207,10 +207,10 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
|
||||
}
|
||||
|
||||
|
||||
$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) {
|
||||
$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) {
|
||||
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */
|
||||
$userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
$userFromRequest = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
|
||||
if ($userFromRequest->isEmpty()) {
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
@@ -266,7 +266,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
$detector->getDevice()
|
||||
));
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$session = $dbForProject->createDocument('sessions', $session
|
||||
->setAttribute('$permissions', [
|
||||
@@ -275,7 +275,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
]));
|
||||
|
||||
Authorization::skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId()));
|
||||
$authorization->skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId()));
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
// Magic URL + Email OTP
|
||||
@@ -376,8 +376,9 @@ App::post('/v1/account')
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks) {
|
||||
|
||||
$email = \strtolower($email);
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -469,9 +470,9 @@ App::post('/v1/account')
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
try {
|
||||
$target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([
|
||||
$target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
@@ -497,9 +498,9 @@ App::post('/v1/account')
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
Authorization::unsetRole(Role::guests()->toString());
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
Authorization::setRole(Role::users()->toString());
|
||||
$authorization->removeRole(Role::guests()->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::users()->toString());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -959,6 +960,7 @@ App::post('/v1/account/sessions/email')
|
||||
))
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},email:{param-email}')
|
||||
->label('abuse-reset', [201])
|
||||
->param('email', '', new EmailValidator(), 'User email.')
|
||||
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
|
||||
->inject('request')
|
||||
@@ -975,7 +977,8 @@ App::post('/v1/account/sessions/email')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->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) {
|
||||
->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) {
|
||||
$email = \strtolower($email);
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
@@ -1020,7 +1023,7 @@ App::post('/v1/account/sessions/email')
|
||||
$detector->getDevice()
|
||||
));
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
// Re-hash if not using recommended algo
|
||||
if ($user->getAttribute('hash') !== $proofForPassword->getHash()->getName()) {
|
||||
@@ -1119,7 +1122,8 @@ App::post('/v1/account/sessions/anonymous')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
|
||||
->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) {
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -1164,7 +1168,7 @@ App::post('/v1/account/sessions/anonymous')
|
||||
'accessedAt' => DateTime::now(),
|
||||
]);
|
||||
$user->removeAttribute('$sequence');
|
||||
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
|
||||
// Create session token
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
@@ -1190,7 +1194,7 @@ App::post('/v1/account/sessions/anonymous')
|
||||
$detector->getDevice()
|
||||
));
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
@@ -1257,6 +1261,7 @@ App::post('/v1/account/sessions/token')
|
||||
))
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'ip:{ip},userId:{param-userId}')
|
||||
->label('abuse-reset', [201])
|
||||
->param('userId', '', new CustomId(), 'User 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.')
|
||||
->param('secret', '', new Text(256), 'Secret of a token generated by login methods. For example, the `createMagicURLToken` or `createPhoneToken` methods.')
|
||||
->inject('request')
|
||||
@@ -1272,6 +1277,7 @@ App::post('/v1/account/sessions/token')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('authorization')
|
||||
->action($createSession);
|
||||
|
||||
App::get('/v1/account/sessions/oauth2/:provider')
|
||||
@@ -1468,7 +1474,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->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, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) {
|
||||
->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, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
@@ -1724,7 +1731,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
$userDoc = Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
$userDoc = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
$dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
@@ -1742,8 +1749,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
}
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
Authorization::setRole(Role::users()->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::users()->toString());
|
||||
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
$failureRedirect(Exception::USER_BLOCKED); // User is in status blocked
|
||||
@@ -1814,7 +1821,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$state['success'] = URLParser::parse($state['success']);
|
||||
$query = URLParser::parseQuery($state['success']['query']);
|
||||
@@ -1838,7 +1845,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
'ip' => $request->getIP(),
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$token = $dbForProject->createDocument('tokens', $token
|
||||
->setAttribute('$permissions', [
|
||||
@@ -2075,7 +2082,8 @@ App::post('/v1/account/tokens/magic-url')
|
||||
->inject('queueForMails')
|
||||
->inject('proofForPassword')
|
||||
->inject('platform')
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform) {
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2148,7 +2156,7 @@ App::post('/v1/account/tokens/magic-url')
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
}
|
||||
|
||||
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
|
||||
@@ -2168,7 +2176,7 @@ App::post('/v1/account/tokens/magic-url')
|
||||
'ip' => $request->getIP(),
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$token = $dbForProject->createDocument('tokens', $token
|
||||
->setAttribute('$permissions', [
|
||||
@@ -2354,7 +2362,8 @@ App::post('/v1/account/tokens/email')
|
||||
->inject('queueForMails')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForCode')
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode) {
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2423,9 +2432,9 @@ App::post('/v1/account/tokens/email')
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
try {
|
||||
$target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([
|
||||
$target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
@@ -2463,7 +2472,7 @@ App::post('/v1/account/tokens/email')
|
||||
'ip' => $request->getIP(),
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$token = $dbForProject->createDocument('tokens', $token
|
||||
->setAttribute('$permissions', [
|
||||
@@ -2645,6 +2654,7 @@ App::put('/v1/account/sessions/magic-url')
|
||||
))
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'ip:{ip},userId:{param-userId}')
|
||||
->label('abuse-reset', [201])
|
||||
->param('userId', '', new CustomId(), 'User 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.')
|
||||
->param('secret', '', new Text(256), 'Valid verification token.')
|
||||
->inject('request')
|
||||
@@ -2659,10 +2669,11 @@ App::put('/v1/account/sessions/magic-url')
|
||||
->inject('queueForMails')
|
||||
->inject('store')
|
||||
->inject('proofForCode')
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode) use ($createSession) {
|
||||
->inject('authorization')
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $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);
|
||||
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization);
|
||||
});
|
||||
|
||||
App::put('/v1/account/sessions/phone')
|
||||
@@ -2708,6 +2719,7 @@ App::put('/v1/account/sessions/phone')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('authorization')
|
||||
->action($createSession);
|
||||
|
||||
App::post('/v1/account/tokens/phone')
|
||||
@@ -2751,7 +2763,8 @@ App::post('/v1/account/tokens/phone')
|
||||
->inject('plan')
|
||||
->inject('store')
|
||||
->inject('proofForCode')
|
||||
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode) {
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
|
||||
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
|
||||
}
|
||||
@@ -2801,9 +2814,9 @@ App::post('/v1/account/tokens/phone')
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
try {
|
||||
$target = Authorization::skip(fn () => $dbForProject->createDocument('targets', new Document([
|
||||
$target = $authorization->skip(fn () => $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
@@ -2849,7 +2862,7 @@ App::post('/v1/account/tokens/phone')
|
||||
'ip' => $request->getIP(),
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$token = $dbForProject->createDocument('tokens', $token
|
||||
->setAttribute('$permissions', [
|
||||
@@ -2895,26 +2908,21 @@ App::post('/v1/account/tokens/phone')
|
||||
->setRecipients([$phone])
|
||||
->setProviderType(MESSAGE_TYPE_SMS);
|
||||
|
||||
if (isset($plan['authPhone'])) {
|
||||
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
|
||||
$timelimit
|
||||
->setParam('{organizationId}', $project->getAttribute('teamId'));
|
||||
$helper = PhoneNumberUtil::getInstance();
|
||||
try {
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
$abuse = new Abuse($timelimit);
|
||||
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
|
||||
$helper = PhoneNumberUtil::getInstance();
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
} catch (NumberParseException $e) {
|
||||
// Ignore invalid phone number for country code stats
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
$token->setAttribute('secret', $secret);
|
||||
@@ -3240,7 +3248,8 @@ App::patch('/v1/account/email')
|
||||
->inject('project')
|
||||
->inject('hooks')
|
||||
->inject('proofForPassword')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword) {
|
||||
->inject('authorization')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword, Authorization $authorization) {
|
||||
// passwordUpdate will be empty if the user has never set a password
|
||||
$passwordUpdate = $user->getAttribute('passwordUpdate');
|
||||
|
||||
@@ -3292,7 +3301,7 @@ App::patch('/v1/account/email')
|
||||
->setAttribute('passwordUpdate', DateTime::now());
|
||||
}
|
||||
|
||||
$target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
|
||||
$target = $authorization->skip(fn () => $dbForProject->findOne('targets', [
|
||||
Query::equal('identifier', [$email]),
|
||||
]));
|
||||
|
||||
@@ -3308,7 +3317,7 @@ App::patch('/v1/account/email')
|
||||
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)));
|
||||
}
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
} catch (Duplicate) {
|
||||
@@ -3349,8 +3358,9 @@ App::patch('/v1/account/phone')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('hooks')
|
||||
->inject('proofForPassword')
|
||||
->action(function (string $phone, string $password, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword) {
|
||||
->inject('proofForPassword')
|
||||
->inject('authorization')
|
||||
->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword, Authorization $authorization) {
|
||||
// passwordUpdate will be empty if the user has never set a password
|
||||
$passwordUpdate = $user->getAttribute('passwordUpdate');
|
||||
|
||||
@@ -3365,7 +3375,7 @@ App::patch('/v1/account/phone')
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]);
|
||||
|
||||
$target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
|
||||
$target = $authorization->skip(fn () => $dbForProject->findOne('targets', [
|
||||
Query::equal('identifier', [$phone]),
|
||||
]));
|
||||
|
||||
@@ -3396,7 +3406,7 @@ App::patch('/v1/account/phone')
|
||||
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone)));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone)));
|
||||
}
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
} catch (Duplicate $th) {
|
||||
@@ -3532,7 +3542,9 @@ App::post('/v1/account/recovery')
|
||||
->inject('queueForMails')
|
||||
->inject('queueForEvents')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken) {
|
||||
->inject('authorization')
|
||||
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
|
||||
}
|
||||
@@ -3568,7 +3580,7 @@ App::post('/v1/account/recovery')
|
||||
'ip' => $request->getIP(),
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($profile->getId())->toString());
|
||||
$authorization->addRole(Role::user($profile->getId())->toString());
|
||||
|
||||
$recovery = $dbForProject->createDocument('tokens', $recovery
|
||||
->setAttribute('$permissions', [
|
||||
@@ -3724,7 +3736,8 @@ App::put('/v1/account/recovery')
|
||||
->inject('hooks')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $userId, string $secret, string $password, Response $response, User $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $secret, string $password, Response $response, User $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $profile */
|
||||
$profile = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
@@ -3738,7 +3751,7 @@ App::put('/v1/account/recovery')
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
Authorization::setRole(Role::user($profile->getId())->toString());
|
||||
$authorization->addRole(Role::user($profile->getId())->toString());
|
||||
|
||||
$newPassword = $proofForPassword->hash($password);
|
||||
|
||||
@@ -3841,7 +3854,8 @@ App::post('/v1/account/verifications/email')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken) {
|
||||
->inject('authorization')
|
||||
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
|
||||
@@ -3870,7 +3884,7 @@ App::post('/v1/account/verifications/email')
|
||||
'ip' => $request->getIP(),
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$verification = $dbForProject->createDocument('tokens', $verification
|
||||
->setAttribute('$permissions', [
|
||||
@@ -4069,9 +4083,10 @@ App::put('/v1/account/verifications/email')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsToken $proofForToken) {
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $profile */
|
||||
$profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
$profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
|
||||
if ($profile->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
@@ -4083,7 +4098,7 @@ App::put('/v1/account/verifications/email')
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
Authorization::setRole(Role::user($profile->getId())->toString());
|
||||
$authorization->addRole(Role::user($profile->getId())->toString());
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
|
||||
|
||||
@@ -4143,7 +4158,8 @@ App::post('/v1/account/verifications/phone')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('plan')
|
||||
->inject('proofForCode')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode) {
|
||||
->inject('authorization')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
|
||||
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
|
||||
}
|
||||
@@ -4182,7 +4198,7 @@ App::post('/v1/account/verifications/phone')
|
||||
'ip' => $request->getIP(),
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$verification = $dbForProject->createDocument('tokens', $verification
|
||||
->setAttribute('$permissions', [
|
||||
@@ -4223,26 +4239,21 @@ App::post('/v1/account/verifications/phone')
|
||||
->setRecipients([$user->getAttribute('phone')])
|
||||
->setProviderType(MESSAGE_TYPE_SMS);
|
||||
|
||||
if (isset($plan['authPhone'])) {
|
||||
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
|
||||
$timelimit
|
||||
->setParam('{organizationId}', $project->getAttribute('teamId'));
|
||||
$helper = PhoneNumberUtil::getInstance();
|
||||
try {
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
$abuse = new Abuse($timelimit);
|
||||
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
|
||||
$helper = PhoneNumberUtil::getInstance();
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
} catch (NumberParseException $e) {
|
||||
// Ignore invalid phone number for country code stats
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
$verification->setAttribute('secret', $secret);
|
||||
@@ -4288,9 +4299,10 @@ App::put('/v1/account/verifications/phone')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('proofForCode')
|
||||
->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsCode $proofForCode) {
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $secret, Response $response, User $user, Database $dbForProject, Event $queueForEvents, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $profile */
|
||||
$profile = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
$profile = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
|
||||
if ($profile->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
@@ -4302,7 +4314,7 @@ App::put('/v1/account/verifications/phone')
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
Authorization::setRole(Role::user($profile->getId())->toString());
|
||||
$authorization->addRole(Role::user($profile->getId())->toString());
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
|
||||
|
||||
@@ -4355,12 +4367,13 @@ App::post('/v1/account/targets/push')
|
||||
->inject('dbForProject')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, User $user, Request $request, Response $response, Database $dbForProject, Store $store, ProofsToken $proofForToken) {
|
||||
->inject('authorization')
|
||||
->action(function (string $targetId, string $identifier, string $providerId, Event $queueForEvents, User $user, Request $request, Response $response, Database $dbForProject, Store $store, ProofsToken $proofForToken, Authorization $authorization) {
|
||||
$targetId = $targetId == 'unique()' ? ID::unique() : $targetId;
|
||||
|
||||
$provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
|
||||
$provider = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId));
|
||||
|
||||
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
|
||||
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
|
||||
|
||||
if (!$target->isEmpty()) {
|
||||
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
|
||||
@@ -4435,9 +4448,10 @@ App::put('/v1/account/targets/:targetId/push')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) {
|
||||
->inject('authorization')
|
||||
->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
|
||||
|
||||
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
|
||||
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
|
||||
|
||||
if ($target->isEmpty()) {
|
||||
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
|
||||
@@ -4500,8 +4514,9 @@ App::delete('/v1/account/targets/:targetId/push')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject) {
|
||||
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
|
||||
->inject('authorization')
|
||||
->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
|
||||
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
|
||||
|
||||
if ($target->isEmpty()) {
|
||||
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\GraphQL\Cache as GraphQLCache;
|
||||
use Appwrite\GraphQL\Promises\Adapter;
|
||||
use Appwrite\GraphQL\Schema;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
@@ -17,7 +17,6 @@ use GraphQL\Type\Schema as GQLSchema;
|
||||
use GraphQL\Validator\Rules\DisableIntrospection;
|
||||
use GraphQL\Validator\Rules\QueryComplexity;
|
||||
use GraphQL\Validator\Rules\QueryDepth;
|
||||
use Swoole\Coroutine\WaitGroup;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
@@ -28,11 +27,12 @@ use Utopia\Validator\Text;
|
||||
App::init()
|
||||
->groups(['graphql'])
|
||||
->inject('project')
|
||||
->action(function (Document $project) {
|
||||
->inject('authorization')
|
||||
->action(function (Document $project, Authorization $authorization) {
|
||||
if (
|
||||
array_key_exists('graphql', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['graphql']
|
||||
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -223,8 +223,11 @@ function execute(
|
||||
$flags = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
|
||||
$validations = GraphQL::getStandardValidationRules();
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
|
||||
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
|
||||
$validations[] = new DisableIntrospection();
|
||||
}
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
|
||||
$validations[] = new QueryComplexity($maxComplexity);
|
||||
$validations[] = new QueryDepth($maxDepth);
|
||||
}
|
||||
@@ -244,21 +247,12 @@ function execute(
|
||||
);
|
||||
}
|
||||
|
||||
$output = [];
|
||||
$wg = new WaitGroup();
|
||||
$wg->add();
|
||||
$promiseAdapter->all($promises)->then(
|
||||
function (array $results) use (&$output, &$wg, $flags) {
|
||||
try {
|
||||
$output = processResult($results, $flags);
|
||||
} finally {
|
||||
$wg->done();
|
||||
}
|
||||
}
|
||||
);
|
||||
$wg->wait();
|
||||
$allPromise = $promiseAdapter->all($promises);
|
||||
|
||||
return $output;
|
||||
// Use the adapter's wait() to run the queue and resolve promises
|
||||
$results = $promiseAdapter->wait($allPromise);
|
||||
|
||||
return processResult($results, $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,6 +326,7 @@ function processResult($result, $debugFlags): array
|
||||
App::shutdown()
|
||||
->groups(['schema'])
|
||||
->inject('project')
|
||||
->action(function (Document $project) {
|
||||
Schema::setDirty($project->getId());
|
||||
->inject('graphqlCache')
|
||||
->action(function (Document $project, GraphQLCache $graphqlCache) {
|
||||
$graphqlCache->setDirty($project->getId());
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@ use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Authorization\Input;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
@@ -1073,8 +1074,9 @@ App::get('/v1/messaging/providers')
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('response')
|
||||
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
|
||||
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
@@ -1100,7 +1102,7 @@ App::get('/v1/messaging/providers')
|
||||
}
|
||||
|
||||
$providerId = $cursor->getValue();
|
||||
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('providers', $providerId));
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found.");
|
||||
@@ -2481,8 +2483,9 @@ App::get('/v1/messaging/topics')
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('response')
|
||||
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
|
||||
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
@@ -2508,7 +2511,7 @@ App::get('/v1/messaging/topics')
|
||||
}
|
||||
|
||||
$topicId = $cursor->getValue();
|
||||
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found.");
|
||||
@@ -2782,29 +2785,27 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
|
||||
->param('targetId', '', new UID(), 'Target ID. The target ID to link to the specified Topic ID.')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('response')
|
||||
->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Authorization $authorization, Response $response) {
|
||||
$subscriberId = $subscriberId == 'unique()' ? ID::unique() : $subscriberId;
|
||||
|
||||
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
|
||||
if ($topic->isEmpty()) {
|
||||
throw new Exception(Exception::TOPIC_NOT_FOUND);
|
||||
}
|
||||
|
||||
$validator = new Authorization('subscribe');
|
||||
|
||||
if (!$validator->isValid($topic->getAttribute('subscribe'))) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
|
||||
if (!$authorization->isValid(new Input('subscribe', $topic->getAttribute('subscribe')))) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, $authorization->getDescription());
|
||||
}
|
||||
|
||||
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
|
||||
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
|
||||
|
||||
if ($target->isEmpty()) {
|
||||
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
|
||||
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
|
||||
|
||||
$subscriber = new Document([
|
||||
'$id' => $subscriberId,
|
||||
@@ -2837,7 +2838,7 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
|
||||
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
|
||||
};
|
||||
|
||||
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute(
|
||||
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute(
|
||||
'topics',
|
||||
$topicId,
|
||||
$totalAttribute,
|
||||
@@ -2882,8 +2883,9 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('response')
|
||||
->action(function (string $topicId, array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
|
||||
->action(function (string $topicId, array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
@@ -2894,7 +2896,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
|
||||
if ($topic->isEmpty()) {
|
||||
throw new Exception(Exception::TOPIC_NOT_FOUND);
|
||||
@@ -2917,7 +2919,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
|
||||
}
|
||||
|
||||
$subscriberId = $cursor->getValue();
|
||||
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found.");
|
||||
@@ -2931,10 +2933,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
$subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) {
|
||||
return function () use ($subscriber, $dbForProject) {
|
||||
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
|
||||
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
|
||||
$subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject, $authorization) {
|
||||
return function () use ($subscriber, $dbForProject, $authorization) {
|
||||
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
|
||||
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
|
||||
|
||||
return $subscriber
|
||||
->setAttribute('target', $target)
|
||||
@@ -3067,9 +3069,10 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
|
||||
->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
|
||||
->param('subscriberId', '', new UID(), 'Subscriber ID.')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('response')
|
||||
->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) {
|
||||
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
->action(function (string $topicId, string $subscriberId, Database $dbForProject, Authorization $authorization, Response $response) {
|
||||
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
|
||||
if ($topic->isEmpty()) {
|
||||
throw new Exception(Exception::TOPIC_NOT_FOUND);
|
||||
@@ -3081,8 +3084,8 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
|
||||
throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
|
||||
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
|
||||
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
|
||||
$user = $authorization->skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
|
||||
|
||||
$subscriber
|
||||
->setAttribute('target', $target)
|
||||
@@ -3118,9 +3121,10 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
|
||||
->param('subscriberId', '', new UID(), 'Subscriber ID.')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('response')
|
||||
->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
$topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Authorization $authorization, Response $response) {
|
||||
$topic = $authorization->skip(fn () => $dbForProject->getDocument('topics', $topicId));
|
||||
|
||||
if ($topic->isEmpty()) {
|
||||
throw new Exception(Exception::TOPIC_NOT_FOUND);
|
||||
@@ -3143,7 +3147,7 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
|
||||
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
|
||||
};
|
||||
|
||||
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute(
|
||||
$authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute(
|
||||
'topics',
|
||||
$topicId,
|
||||
$totalAttribute,
|
||||
@@ -3702,8 +3706,9 @@ App::get('/v1/messaging/messages')
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('response')
|
||||
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Response $response) {
|
||||
->action(function (array $queries, string $search, bool $includeTotal, Database $dbForProject, Authorization $authorization, Response $response) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
@@ -3729,7 +3734,7 @@ App::get('/v1/messaging/messages')
|
||||
}
|
||||
|
||||
$messageId = $cursor->getValue();
|
||||
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('messages', $messageId));
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForProject->getDocument('messages', $messageId));
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found.");
|
||||
|
||||
@@ -342,6 +342,7 @@ App::post('/v1/migrations/csv/imports')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('deviceForFiles')
|
||||
@@ -356,6 +357,7 @@ App::post('/v1/migrations/csv/imports')
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
array $platform,
|
||||
Device $deviceForFiles,
|
||||
@@ -363,7 +365,7 @@ App::post('/v1/migrations/csv/imports')
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
) {
|
||||
$bucket = Authorization::skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
return $dbForPlatform->getDocument('buckets', 'default');
|
||||
}
|
||||
@@ -374,7 +376,7 @@ App::post('/v1/migrations/csv/imports')
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
$file = Authorization::skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
|
||||
$file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
|
||||
if ($file->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
@@ -491,6 +493,7 @@ App::post('/v1/migrations/csv/exports')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
@@ -509,6 +512,7 @@ App::post('/v1/migrations/csv/exports')
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
@@ -520,7 +524,7 @@ App::post('/v1/migrations/csv/exports')
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
|
||||
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
|
||||
if ($bucket->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
@@ -533,12 +537,12 @@ App::post('/v1/migrations/csv/exports')
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -45,9 +45,10 @@ App::get('/v1/project/usage')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('getLogsDB')
|
||||
->inject('smsRates')
|
||||
->action(function (string $startDate, string $endDate, string $period, Response $response, Document $project, Database $dbForProject, callable $getLogsDB, array $smsRates) {
|
||||
->action(function (string $startDate, string $endDate, string $period, Response $response, Document $project, Database $dbForProject, Authorization $authorization, callable $getLogsDB, array $smsRates) {
|
||||
$stats = $total = $usage = [];
|
||||
$format = 'Y-m-d 00:00:00';
|
||||
$firstDay = (new DateTime($startDate))->format($format);
|
||||
@@ -102,7 +103,7 @@ App::get('/v1/project/usage')
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $dbForLogs, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
|
||||
$authorization->skip(function () use ($dbForProject, $dbForLogs, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
|
||||
foreach ($metrics['total'] as $metric) {
|
||||
$db = ($metric === METRIC_FILES_IMAGES_TRANSFORMED) ? $dbForLogs : $dbForProject;
|
||||
|
||||
@@ -286,7 +287,7 @@ App::get('/v1/project/usage')
|
||||
}, $dbForProject->find('functions'));
|
||||
|
||||
// This total is includes free and paid SMS usage
|
||||
$authPhoneTotal = Authorization::skip(fn () => $dbForProject->sum('stats', 'value', [
|
||||
$authPhoneTotal = $authorization->skip(fn () => $dbForProject->sum('stats', 'value', [
|
||||
Query::equal('metric', [METRIC_AUTH_METHOD_PHONE]),
|
||||
Query::equal('period', ['1d']),
|
||||
Query::greaterThanEqual('time', $firstDay),
|
||||
@@ -294,7 +295,7 @@ App::get('/v1/project/usage')
|
||||
]));
|
||||
|
||||
// This estimate is only for paid SMS usage
|
||||
$authPhoneMetrics = Authorization::skip(fn () => $dbForProject->find('stats', [
|
||||
$authPhoneMetrics = $authorization->skip(fn () => $dbForProject->find('stats', [
|
||||
Query::startsWith('metric', METRIC_AUTH_METHOD_PHONE . '.'),
|
||||
Query::equal('period', ['1d']),
|
||||
Query::greaterThanEqual('time', $firstDay),
|
||||
|
||||
@@ -204,6 +204,7 @@ App::post('/v1/projects')
|
||||
'accessedAt' => DateTime::now(),
|
||||
'search' => implode(' ', [$projectId, $name]),
|
||||
'database' => $dsn,
|
||||
'labels' => [],
|
||||
]));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::PROJECT_ALREADY_EXISTS);
|
||||
@@ -226,6 +227,7 @@ App::post('/v1/projects')
|
||||
if (!$sharedTablesV2) {
|
||||
$adapter = new DatabasePool($pools->get($dsn->getHost()));
|
||||
$dbForProject = new Database($adapter, $cache);
|
||||
$dbForProject->setDatabase(APP_DATABASE);
|
||||
|
||||
if ($sharedTables) {
|
||||
$dbForProject
|
||||
@@ -1500,6 +1502,7 @@ App::post('/v1/projects/:projectId/keys')
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
// TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column.
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'resourceInternalId' => $project->getSequence(),
|
||||
@@ -1552,13 +1555,8 @@ App::get('/v1/projects/:projectId/keys')
|
||||
}
|
||||
|
||||
$keys = $dbForPlatform->find('keys', [
|
||||
Query::or([
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::and([
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
])
|
||||
]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
Query::limit(5000),
|
||||
]);
|
||||
|
||||
@@ -1599,13 +1597,8 @@ App::get('/v1/projects/:projectId/keys/:keyId')
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::or([
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::and([
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
])
|
||||
])
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
@@ -1649,13 +1642,8 @@ App::put('/v1/projects/:projectId/keys/:keyId')
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::or([
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::and([
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
])
|
||||
])
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
@@ -1706,13 +1694,8 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::or([
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::and([
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
])
|
||||
])
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
|
||||
@@ -23,9 +23,9 @@ use Appwrite\Utopia\Database\Validator\Queries\Memberships;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Teams;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use libphonenumber\NumberParseException;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use MaxMind\Db\Reader;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\App;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
@@ -86,22 +86,24 @@ App::post('/v1/teams')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents) {
|
||||
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
$isAppUser = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
|
||||
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
|
||||
|
||||
try {
|
||||
$team = Authorization::skip(fn () => $dbForProject->createDocument('teams', new Document([
|
||||
$team = $authorization->skip(fn () => $dbForProject->createDocument('teams', new Document([
|
||||
'$id' => $teamId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team($teamId)),
|
||||
Permission::update(Role::team($teamId, 'owner')),
|
||||
Permission::delete(Role::team($teamId, 'owner')),
|
||||
],
|
||||
'labels' => [],
|
||||
'name' => $name,
|
||||
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
|
||||
'prefs' => new \stdClass(),
|
||||
@@ -491,6 +493,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('locale')
|
||||
->inject('queueForMails')
|
||||
->inject('queueForMessaging')
|
||||
@@ -500,9 +503,9 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
->inject('plan')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
|
||||
$isAppUser = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
|
||||
$url = htmlentities($url);
|
||||
if (empty($url)) {
|
||||
@@ -619,13 +622,13 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
]);
|
||||
|
||||
try {
|
||||
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', $userDocument));
|
||||
$invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', $userDocument));
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
|
||||
$isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner');
|
||||
|
||||
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team');
|
||||
@@ -661,11 +664,11 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
]);
|
||||
|
||||
$membership = ($isPrivilegedUser || $isAppUser) ?
|
||||
Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) :
|
||||
$authorization->skip(fn () => $dbForProject->createDocument('memberships', $membership)) :
|
||||
$dbForProject->createDocument('memberships', $membership);
|
||||
|
||||
if ($isPrivilegedUser || $isAppUser) {
|
||||
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
}
|
||||
} elseif ($membership->getAttribute('confirm') === false) {
|
||||
$membership->setAttribute('secret', $proofForToken->hash($secret));
|
||||
@@ -677,7 +680,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
}
|
||||
|
||||
$membership = ($isPrivilegedUser || $isAppUser) ?
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
|
||||
$dbForProject->updateDocument('memberships', $membership->getId(), $membership);
|
||||
} else {
|
||||
throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED);
|
||||
@@ -799,26 +802,21 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
->setRecipients([$phone])
|
||||
->setProviderType('SMS');
|
||||
|
||||
if (isset($plan['authPhone'])) {
|
||||
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
|
||||
$timelimit
|
||||
->setParam('{organizationId}', $project->getAttribute('teamId'));
|
||||
$helper = PhoneNumberUtil::getInstance();
|
||||
try {
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
$abuse = new Abuse($timelimit);
|
||||
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
|
||||
$helper = PhoneNumberUtil::getInstance();
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
} catch (NumberParseException $e) {
|
||||
// Ignore invalid phone number for country code stats
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,7 +861,8 @@ App::get('/v1/teams/:teamId/memberships')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject) {
|
||||
->inject('authorization')
|
||||
->action(function (string $teamId, array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Authorization $authorization) {
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
|
||||
if ($team->isEmpty()) {
|
||||
@@ -933,7 +932,7 @@ App::get('/v1/teams/:teamId/memberships')
|
||||
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
|
||||
];
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
|
||||
@@ -1004,7 +1003,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject) {
|
||||
->inject('authorization')
|
||||
->action(function (string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject, Authorization $authorization) {
|
||||
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
|
||||
@@ -1024,7 +1024,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
|
||||
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
|
||||
];
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
|
||||
@@ -1103,8 +1103,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Event $queueForEvents) {
|
||||
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
if ($team->isEmpty()) {
|
||||
@@ -1121,9 +1122,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
$isAppUser = User::isApp(Authorization::getRoles());
|
||||
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner');
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
// Quick check: fetch up to 2 owners to determine if only one exists
|
||||
@@ -1204,12 +1205,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
|
||||
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
$membership = $dbForProject->getDocument('memberships', $membershipId);
|
||||
@@ -1218,7 +1220,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
throw new Exception(Exception::MEMBERSHIP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$team = Authorization::skip(fn () => $dbForProject->getDocument('teams', $teamId));
|
||||
$team = $authorization->skip(fn () => $dbForProject->getDocument('teams', $teamId));
|
||||
|
||||
if ($team->isEmpty()) {
|
||||
throw new Exception(Exception::TEAM_NOT_FOUND);
|
||||
@@ -1254,11 +1256,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
->setAttribute('confirm', true)
|
||||
;
|
||||
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
|
||||
|
||||
// Create session for the user if not logged in
|
||||
if (!$hasSession) {
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
$authorization->addRole(Role::user($user->getId())->toString());
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
@@ -1286,7 +1288,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
|
||||
$session = $dbForProject->createDocument('sessions', $session);
|
||||
|
||||
Authorization::setRole(Role::user($userId)->toString());
|
||||
$authorization->addRole(Role::user($userId)->toString());
|
||||
|
||||
$encoded = $store
|
||||
->setProperty('id', $user->getId())
|
||||
@@ -1324,7 +1326,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -1368,8 +1370,9 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $teamId, string $membershipId, Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $teamId, string $membershipId, Document $user, Document $project, Response $response, Database $dbForProject, Authorization $authorization, Event $queueForEvents) {
|
||||
|
||||
$membership = $dbForProject->getDocument('memberships', $membershipId);
|
||||
|
||||
@@ -1427,7 +1430,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
|
||||
$dbForProject->purgeCachedDocument('users', $profile->getId());
|
||||
|
||||
if ($membership->getAttribute('confirm')) { // Count only confirmed members
|
||||
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
|
||||
$authorization->skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
|
||||
@@ -2678,8 +2678,8 @@ App::get('/v1/users/usage')
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('register')
|
||||
->action(function (string $range, Response $response, Database $dbForProject) {
|
||||
->inject('authorization')
|
||||
->action(function (string $range, Response $response, Database $dbForProject, Authorization $authorization) {
|
||||
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
@@ -2689,7 +2689,7 @@ App::get('/v1/users/usage')
|
||||
METRIC_SESSIONS,
|
||||
];
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
$authorization->skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $count => $metric) {
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
|
||||
+32
-29
@@ -76,7 +76,7 @@ use Utopia\VCS\Exception\RepositoryNotFound;
|
||||
|
||||
use function Swoole\Coroutine\batch;
|
||||
|
||||
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, array $platform) {
|
||||
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Authorization $authorization, Build $queueForBuilds, callable $getProjectDB, Request $request, array $platform) {
|
||||
$errors = [];
|
||||
foreach ($repositories as $repository) {
|
||||
try {
|
||||
@@ -87,12 +87,12 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
}
|
||||
|
||||
$projectId = $repository->getAttribute('projectId');
|
||||
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
|
||||
$resourceId = $repository->getAttribute('resourceId');
|
||||
$resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
|
||||
$resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
|
||||
$resourceInternalId = $resource->getSequence();
|
||||
|
||||
$deploymentId = ID::unique();
|
||||
@@ -141,7 +141,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
$latestCommentId = '';
|
||||
|
||||
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) {
|
||||
$latestComment = Authorization::skip(fn () => $dbForPlatform->findOne('vcsComments', [
|
||||
$latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::equal('providerPullRequestId', [$providerPullRequestId]),
|
||||
Query::orderDesc('$createdAt'),
|
||||
@@ -180,7 +180,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
|
||||
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
|
||||
} finally {
|
||||
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -191,7 +191,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
if (!empty($latestCommentId)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$latestComment = Authorization::skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
|
||||
$latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
@@ -212,7 +212,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
}
|
||||
}
|
||||
} elseif (!empty($providerBranch)) {
|
||||
$latestComments = Authorization::skip(fn () => $dbForPlatform->find('vcsComments', [
|
||||
$latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::equal('providerBranch', [$providerBranch]),
|
||||
Query::orderDesc('$createdAt'),
|
||||
@@ -251,7 +251,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
|
||||
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
|
||||
} finally {
|
||||
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
$commands[] = $resource->getAttribute('commands', '');
|
||||
}
|
||||
|
||||
$deployment = Authorization::skip(fn () => $dbForProject->createDocument('deployments', new Document([
|
||||
$deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
@@ -306,6 +306,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
'resourceType' => $resourceCollection,
|
||||
'entrypoint' => $resource->getAttribute('entrypoint', ''),
|
||||
'buildCommands' => \implode(' && ', $commands),
|
||||
'startCommand' => $resource->getAttribute('startCommand', ''),
|
||||
'buildOutput' => $resource->getAttribute('outputDirectory', ''),
|
||||
'adapter' => $resource->getAttribute('adapter', ''),
|
||||
'fallbackFile' => $resource->getAttribute('fallbackFile', ''),
|
||||
@@ -334,7 +335,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource));
|
||||
|
||||
if ($resource->getCollection() === 'sites') {
|
||||
$projectId = $project->getId();
|
||||
@@ -344,7 +345,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
$domain = ID::unique() . "." . $sitesDomain;
|
||||
$ruleId = md5($domain);
|
||||
$previewRuleId = $ruleId;
|
||||
Authorization::skip(
|
||||
$authorization->skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
@@ -377,7 +378,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
$domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}";
|
||||
$ruleId = md5($domain);
|
||||
try {
|
||||
Authorization::skip(
|
||||
$authorization->skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
@@ -408,7 +409,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
$domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}";
|
||||
$ruleId = md5($domain);
|
||||
try {
|
||||
Authorization::skip(
|
||||
$authorization->skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
@@ -460,7 +461,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
if ($lockAcquired) {
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
|
||||
@@ -472,7 +473,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
|
||||
}
|
||||
} finally {
|
||||
Authorization::skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1476,11 +1477,12 @@ App::post('/v1/vcs/github/events')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForBuilds')
|
||||
->inject('platform')
|
||||
->action(
|
||||
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
|
||||
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
|
||||
$payload = $request->getRawPayload();
|
||||
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
|
||||
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
|
||||
@@ -1516,14 +1518,14 @@ App::post('/v1/vcs/github/events')
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
|
||||
//find resourceId from relevant resources table
|
||||
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
|
||||
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::limit(100),
|
||||
]));
|
||||
|
||||
// create new deployment only on push (not committed by us) and not when branch is created or deleted
|
||||
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
|
||||
}
|
||||
} elseif ($event == $github::EVENT_INSTALLATION) {
|
||||
if ($parsedPayload["action"] == "deleted") {
|
||||
@@ -1536,16 +1538,16 @@ App::post('/v1/vcs/github/events')
|
||||
]);
|
||||
|
||||
foreach ($installations as $installation) {
|
||||
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
|
||||
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
|
||||
Query::equal('installationInternalId', [$installation->getSequence()]),
|
||||
Query::limit(1000)
|
||||
]));
|
||||
|
||||
foreach ($repositories as $repository) {
|
||||
Authorization::skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
|
||||
}
|
||||
|
||||
Authorization::skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
|
||||
}
|
||||
}
|
||||
} elseif ($event == $github::EVENT_PULL_REQUEST) {
|
||||
@@ -1574,12 +1576,12 @@ App::post('/v1/vcs/github/events')
|
||||
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
|
||||
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
|
||||
|
||||
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
|
||||
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::orderDesc('$createdAt')
|
||||
]));
|
||||
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
|
||||
} elseif ($parsedPayload["action"] == "closed") {
|
||||
// Allowed external contributions cleanup
|
||||
|
||||
@@ -1588,7 +1590,7 @@ App::post('/v1/vcs/github/events')
|
||||
$external = $parsedPayload["external"] ?? true;
|
||||
|
||||
if ($external) {
|
||||
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
|
||||
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::orderDesc('$createdAt')
|
||||
]));
|
||||
@@ -1599,7 +1601,7 @@ App::post('/v1/vcs/github/events')
|
||||
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
|
||||
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
|
||||
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
|
||||
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
|
||||
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1786,17 +1788,18 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForBuilds')
|
||||
->inject('platform')
|
||||
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
|
||||
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
|
||||
$installation = $dbForPlatform->getDocument('installations', $installationId);
|
||||
|
||||
if ($installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$repository = Authorization::skip(fn () => $dbForPlatform->findOne('repositories', [
|
||||
$repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [
|
||||
Query::equal('$id', [$repositoryId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()])
|
||||
]));
|
||||
@@ -1814,7 +1817,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
|
||||
|
||||
// TODO: Delete from array when PR is closed
|
||||
|
||||
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
|
||||
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
|
||||
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
@@ -1846,7 +1849,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
|
||||
$providerCommitMessage = $pullRequestResponse['title'] ?? '';
|
||||
$providerCommitUrl = $pullRequestResponse['html_url'] ?? '';
|
||||
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, '', '', '', '', $providerCommitHash, '', '', '', '', $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
+201
-93
@@ -6,6 +6,7 @@ use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
@@ -59,7 +60,7 @@ Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey)
|
||||
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
{
|
||||
$host = $request->getHostname() ?? '';
|
||||
if (!empty($previewHostname)) {
|
||||
@@ -67,16 +68,16 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
}
|
||||
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
$rule = Authorization::skip(function () use ($dbForPlatform, $host, $isMd5) {
|
||||
if ($isMd5) {
|
||||
return $dbForPlatform->getDocument('rules', md5($host));
|
||||
}
|
||||
|
||||
return $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$host]),
|
||||
]) ?? new Document();
|
||||
});
|
||||
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
|
||||
} else {
|
||||
$rule = $authorization->skip(
|
||||
fn () => $dbForPlatform->find('rules', [
|
||||
Query::equal('domain', [$host]),
|
||||
Query::limit(1)
|
||||
])
|
||||
)[0] ?? new Document();
|
||||
}
|
||||
|
||||
$errorView = __DIR__ . '/../views/general/error.phtml';
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
@@ -111,7 +112,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
}
|
||||
|
||||
$projectId = $rule->getAttribute('projectId');
|
||||
$project = Authorization::skip(
|
||||
$project = $authorization->skip(
|
||||
fn () => $dbForPlatform->getDocument('projects', $projectId)
|
||||
);
|
||||
|
||||
@@ -119,7 +120,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
$project->setAttribute('accessedAt', DateTime::now());
|
||||
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +159,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
|
||||
/** @var Document $deployment */
|
||||
if (!empty($rule->getAttribute('deploymentId', ''))) {
|
||||
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
|
||||
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
|
||||
} else {
|
||||
// 1.6.x DB schema compatibility
|
||||
// TODO: Make sure deploymentId is never empty, and remove this code
|
||||
@@ -172,15 +173,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
|
||||
// Document of site or function
|
||||
$resource = $resourceType === 'function' ?
|
||||
Authorization::skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
|
||||
Authorization::skip(fn () => $dbForProject->getDocument('sites', $resourceId));
|
||||
$authorization->skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
|
||||
$authorization->skip(fn () => $dbForProject->getDocument('sites', $resourceId));
|
||||
|
||||
// ID of active deployments
|
||||
// Attempts to use attribute from both schemas (1.6 and 1.7)
|
||||
$activeDeploymentId = $resource->getAttribute('deploymentId', $resource->getAttribute('deployment', ''));
|
||||
|
||||
// Get deployment document, as intended originally
|
||||
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
|
||||
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
|
||||
}
|
||||
|
||||
if ($deployment->getAttribute('resourceType', '') === 'functions') {
|
||||
@@ -199,8 +200,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
}
|
||||
|
||||
$resource = $type === 'function' ?
|
||||
Authorization::skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
|
||||
Authorization::skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
|
||||
$authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
|
||||
$authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
|
||||
|
||||
$isPreview = $type === 'function' ? false : ($rule->getAttribute('trigger', '') !== 'manual');
|
||||
|
||||
@@ -242,7 +243,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
$userExists = false;
|
||||
$userId = $payload['userId'] ?? '';
|
||||
if (!empty($userId)) {
|
||||
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
|
||||
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
|
||||
if (!$user->isEmpty() && $user->getAttribute('status', false)) {
|
||||
$userExists = true;
|
||||
}
|
||||
@@ -255,7 +256,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
}
|
||||
|
||||
$membershipExists = false;
|
||||
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
if (!$project->isEmpty() && isset($user)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
$membership = $user->find('teamId', $teamId, 'memberships');
|
||||
@@ -802,6 +803,20 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
|
||||
/* cleanup */
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
$resourceType = $type === 'function'
|
||||
? RESOURCE_TYPE_FUNCTIONS
|
||||
: RESOURCE_TYPE_SITES;
|
||||
|
||||
$queueForDeletes
|
||||
->setProject($project)
|
||||
->setResourceType($resourceType)
|
||||
->setResource($resource->getSequence())
|
||||
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
return true;
|
||||
} elseif ($type === 'api') {
|
||||
return false;
|
||||
@@ -812,8 +827,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
} else {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type, view: $errorView);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
App::init()
|
||||
@@ -862,15 +875,18 @@ App::init()
|
||||
->inject('devKey')
|
||||
->inject('apiKey')
|
||||
->inject('cors')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors) {
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$hostname = $request->getHostname() ?? '';
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
|
||||
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1033,7 +1049,8 @@ App::init()
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForCertificates')
|
||||
->inject('platform')
|
||||
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform) {
|
||||
->inject('authorization')
|
||||
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization) {
|
||||
$hostname = $request->getHostname();
|
||||
$cache = Config::getParam('hostnames', []);
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
@@ -1061,64 +1078,64 @@ App::init()
|
||||
}
|
||||
|
||||
// 4. Check/create rule (requires DB access)
|
||||
Authorization::disable();
|
||||
try {
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
$document = $isMd5
|
||||
? $dbForPlatform->getDocument('rules', md5($domain->get()))
|
||||
: $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain->get()]),
|
||||
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, &$cache) {
|
||||
try {
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
$document = $isMd5
|
||||
? $dbForPlatform->getDocument('rules', md5($domain->get()))
|
||||
: $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain->get()]),
|
||||
]);
|
||||
|
||||
if (!$document->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Create new rule
|
||||
$owner = '';
|
||||
$fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
|
||||
$funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
|
||||
if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) {
|
||||
$funcDomain = $fallback;
|
||||
}
|
||||
|
||||
if (
|
||||
(!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) ||
|
||||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
|
||||
) {
|
||||
$owner = 'Appwrite';
|
||||
}
|
||||
|
||||
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
|
||||
$document = new Document([
|
||||
'$id' => $ruleId,
|
||||
'domain' => $domain->get(),
|
||||
'type' => 'api',
|
||||
'status' => 'verifying',
|
||||
'projectId' => $console->getId(),
|
||||
'projectInternalId' => $console->getSequence(),
|
||||
'search' => implode(' ', [$ruleId, $domain->get()]),
|
||||
'owner' => $owner,
|
||||
'region' => $console->getAttribute('region')
|
||||
]);
|
||||
|
||||
if (!$document->isEmpty()) {
|
||||
return;
|
||||
$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();
|
||||
} catch (Duplicate $e) {
|
||||
Console::info('Certificate already exists');
|
||||
} finally {
|
||||
$cache[$domain->get()] = true;
|
||||
Config::setParam('hostnames', $cache);
|
||||
}
|
||||
|
||||
// 5. Create new rule
|
||||
$owner = '';
|
||||
$fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
|
||||
$funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
|
||||
if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) {
|
||||
$funcDomain = $fallback;
|
||||
}
|
||||
|
||||
if (
|
||||
(!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) ||
|
||||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
|
||||
) {
|
||||
$owner = 'Appwrite';
|
||||
}
|
||||
|
||||
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
|
||||
$document = new Document([
|
||||
'$id' => $ruleId,
|
||||
'domain' => $domain->get(),
|
||||
'type' => 'api',
|
||||
'status' => 'verifying',
|
||||
'projectId' => $console->getId(),
|
||||
'projectInternalId' => $console->getSequence(),
|
||||
'search' => implode(' ', [$ruleId, $domain->get()]),
|
||||
'owner' => $owner,
|
||||
'region' => $console->getAttribute('region')
|
||||
]);
|
||||
|
||||
$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();
|
||||
} catch (Duplicate $e) {
|
||||
Console::info('Certificate already exists');
|
||||
} finally {
|
||||
$cache[$domain->get()] = true;
|
||||
Config::setParam('hostnames', $cache);
|
||||
Authorization::reset();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
App::options()
|
||||
@@ -1141,14 +1158,17 @@ App::options()
|
||||
->inject('devKey')
|
||||
->inject('apiKey')
|
||||
->inject('cors')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors) {
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1182,7 +1202,8 @@ App::error()
|
||||
->inject('log')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('devKey')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage) {
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, Document $devKey, Authorization $authorization) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$class = \get_class($error);
|
||||
@@ -1264,7 +1285,7 @@ App::error()
|
||||
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
|
||||
*/
|
||||
if (!$publish && $project->getId() !== 'console') {
|
||||
if (!DBUser::isPrivileged(Authorization::getRoles())) {
|
||||
if (!DBUser::isPrivileged($authorization->getRoles())) {
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
@@ -1326,7 +1347,87 @@ App::error()
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('roles', Authorization::getRoles());
|
||||
$log->addExtra('roles', $authorization->getRoles());
|
||||
|
||||
try {
|
||||
/* add queries to log */
|
||||
$queries = $request->getParam('queries', []);
|
||||
if (!empty($queries) && is_array($queries)) {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
|
||||
// format query by removing sensitive values
|
||||
$formatQuery = function (array $queryArray) use (&$formatQuery): ?array {
|
||||
$method = $queryArray['method'] ?? '';
|
||||
$values = $queryArray['values'] ?? [];
|
||||
$attribute = $queryArray['attribute'] ?? '';
|
||||
|
||||
if (!is_string($method) || $method === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// logical queries - recursively format nested queries
|
||||
if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) {
|
||||
$nested = [];
|
||||
foreach ($values as $nestedArray) {
|
||||
if (is_array($nestedArray)) {
|
||||
$formatted = $formatQuery($nestedArray);
|
||||
if ($formatted !== null) {
|
||||
$nested[] = $formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
return empty($nested) ? null : [$method => $nested];
|
||||
}
|
||||
|
||||
// select - show selected attributes
|
||||
if ($method === Query::TYPE_SELECT) {
|
||||
$attributes = array_values(array_filter($values, 'is_string'));
|
||||
return [$method => $attributes];
|
||||
}
|
||||
|
||||
// pagination
|
||||
if (in_array($method, [
|
||||
Query::TYPE_LIMIT,
|
||||
Query::TYPE_OFFSET,
|
||||
Query::TYPE_CURSOR_AFTER,
|
||||
Query::TYPE_CURSOR_BEFORE
|
||||
], true)) {
|
||||
return [$method => []];
|
||||
}
|
||||
|
||||
// orders
|
||||
if (in_array($method, [
|
||||
Query::TYPE_ORDER_DESC,
|
||||
Query::TYPE_ORDER_ASC,
|
||||
Query::TYPE_ORDER_RANDOM
|
||||
], true)) {
|
||||
return [$method => !empty($attribute) ? [$attribute] : []];
|
||||
}
|
||||
|
||||
// filter
|
||||
if (!empty($attribute)) {
|
||||
return [$method => [$attribute]];
|
||||
}
|
||||
|
||||
// fallback
|
||||
return [$method => []];
|
||||
};
|
||||
|
||||
$formattedQueries = [];
|
||||
foreach ($parsedQueries as $query) {
|
||||
$formatted = $formatQuery($query->toArray());
|
||||
if ($formatted !== null) {
|
||||
$formattedQueries[] = $formatted;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($formattedQueries)) {
|
||||
$log->addExtra('queries', $formattedQueries);
|
||||
}
|
||||
}
|
||||
} catch (Throwable $_) {
|
||||
// don't fail the error handler
|
||||
}
|
||||
|
||||
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
|
||||
if (!empty($sdk)) {
|
||||
@@ -1450,13 +1551,16 @@ App::get('/robots.txt')
|
||||
->inject('platform')
|
||||
->inject('previewHostname')
|
||||
->inject('apiKey')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) {
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/robots.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1482,13 +1586,16 @@ App::get('/humans.txt')
|
||||
->inject('platform')
|
||||
->inject('previewHostname')
|
||||
->inject('apiKey')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey) {
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/humans.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1572,7 +1679,8 @@ App::get('/v1/ping')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForEvents')
|
||||
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) {
|
||||
->inject('authorization')
|
||||
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents, Authorization $authorization) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
|
||||
}
|
||||
@@ -1584,7 +1692,7 @@ App::get('/v1/ping')
|
||||
->setAttribute('pingCount', $pingCount)
|
||||
->setAttribute('pingedAt', $pingedAt);
|
||||
|
||||
Authorization::skip(function () use ($dbForPlatform, $project) {
|
||||
$authorization->skip(function () use ($dbForPlatform, $project) {
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
|
||||
});
|
||||
|
||||
|
||||
@@ -200,8 +200,12 @@ App::post('/v1/mock/api-key-unprefixed')
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
// TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column.
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'resourceInternalId' => $project->getSequence(),
|
||||
'resourceId' => $project->getId(),
|
||||
'resourceType' => 'projects',
|
||||
'name' => 'Outdated key',
|
||||
'scopes' => $scopes,
|
||||
'expire' => null,
|
||||
|
||||
+97
-210
@@ -10,12 +10,12 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
@@ -30,7 +30,7 @@ use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Database\Validator\Authorization\Input;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -74,151 +74,6 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
|
||||
return $label;
|
||||
};
|
||||
|
||||
/**
|
||||
* This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**.
|
||||
*
|
||||
* Accounts can be created in many ways beyond `createAccount`
|
||||
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
|
||||
*/
|
||||
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
|
||||
// Only trigger events for user creation with the database listener.
|
||||
if ($document->getCollection() !== 'users') {
|
||||
return;
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
->setEvent('users.[userId].create')
|
||||
->setParam('userId', $document->getId())
|
||||
->setPayload($response->output($document, Response::MODEL_USER));
|
||||
|
||||
// Trigger functions, webhooks, and realtime events
|
||||
$queueForFunctions
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
|
||||
|
||||
/** Trigger webhooks events only if a project has them enabled */
|
||||
if (!empty($project->getAttribute('webhooks'))) {
|
||||
$queueForWebhooks
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
/** Trigger realtime events only for non console events */
|
||||
if ($queueForEvents->getProject()->getId() !== 'console') {
|
||||
$queueForRealtime
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
|
||||
$value = 1;
|
||||
|
||||
switch ($event) {
|
||||
case Database::EVENT_DOCUMENT_DELETE:
|
||||
$value = -1;
|
||||
break;
|
||||
case Database::EVENT_DOCUMENTS_DELETE:
|
||||
$value = -1 * $document->getAttribute('modified', 0);
|
||||
break;
|
||||
case Database::EVENT_DOCUMENTS_CREATE:
|
||||
$value = $document->getAttribute('modified', 0);
|
||||
break;
|
||||
case Database::EVENT_DOCUMENTS_UPSERT:
|
||||
$value = $document->getAttribute('created', 0);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case $document->getCollection() === 'teams':
|
||||
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
|
||||
break;
|
||||
case $document->getCollection() === 'users':
|
||||
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'sessions': // sessions
|
||||
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
|
||||
break;
|
||||
case $document->getCollection() === 'databases': // databases
|
||||
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_COLLECTIONS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DOCUMENTS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
|
||||
break;
|
||||
case $document->getCollection() === 'buckets': //buckets
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_BUCKETS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'bucket_'): // files
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$bucketInternalId = $parts[1];
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_FILES, $value) // per project
|
||||
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
|
||||
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
|
||||
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
|
||||
break;
|
||||
case $document->getCollection() === 'functions':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_FUNCTIONS, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'sites':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_SITES, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'deployments':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
|
||||
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
|
||||
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function
|
||||
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value)
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
App::init()
|
||||
->groups(['api'])
|
||||
->inject('utopia')
|
||||
@@ -233,7 +88,8 @@ App::init()
|
||||
->inject('mode')
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
|
||||
->inject('authorization')
|
||||
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
/**
|
||||
@@ -318,7 +174,7 @@ App::init()
|
||||
// Handle special app role case
|
||||
if ($apiKey->getRole() === User::ROLE_APPS) {
|
||||
// Disable authorization checks for API keys
|
||||
Authorization::setDefaultStatus(false);
|
||||
$authorization->setDefaultStatus(false);
|
||||
|
||||
$user = new User([
|
||||
'$id' => '',
|
||||
@@ -392,14 +248,14 @@ App::init()
|
||||
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
|
||||
}
|
||||
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
|
||||
$authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
|
||||
}
|
||||
|
||||
$scopes = \array_unique($scopes);
|
||||
|
||||
Authorization::setRole($role);
|
||||
foreach ($user->getRoles() as $authRole) {
|
||||
Authorization::setRole($authRole);
|
||||
$authorization->addRole($role);
|
||||
foreach ($user->getRoles($authorization) as $authRole) {
|
||||
$authorization->addRole($authRole);
|
||||
}
|
||||
|
||||
// Step 6: Update project and user last activity
|
||||
@@ -407,7 +263,7 @@ App::init()
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
$project->setAttribute('accessedAt', DateTime::now());
|
||||
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +298,7 @@ App::init()
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
&& !$project->getAttribute('services', [])[$namespace]
|
||||
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
@@ -487,9 +343,6 @@ App::init()
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('publisher')
|
||||
->inject('publisherFunctions')
|
||||
->inject('publisherWebhooks')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMessaging')
|
||||
->inject('queueForAudits')
|
||||
@@ -499,7 +352,6 @@ App::init()
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForMails')
|
||||
->inject('queueForMigrations')
|
||||
->inject('dbForProject')
|
||||
->inject('timelimit')
|
||||
->inject('resourceToken')
|
||||
@@ -509,14 +361,15 @@ App::init()
|
||||
->inject('devKey')
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Migration $queueForMigrations, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform) use ($usageDatabaseListener, $eventDatabaseListener) {
|
||||
->inject('authorization')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['rest']
|
||||
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -546,7 +399,7 @@ App::init()
|
||||
|
||||
$closestLimit = null;
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
|
||||
@@ -629,38 +482,16 @@ App::init()
|
||||
$queueForBuilds->setPlatform($platform);
|
||||
$queueForMails->setPlatform($platform);
|
||||
|
||||
// Clone the queues, to prevent events triggered by the database listener
|
||||
// from overwriting the events that are supposed to be triggered in the shutdown hook.
|
||||
$queueForEventsClone = new Event($publisher);
|
||||
$queueForFunctions = new Func($publisherFunctions);
|
||||
$queueForWebhooks = new Webhook($publisherWebhooks);
|
||||
$queueForRealtime = new Realtime();
|
||||
|
||||
$dbForProject
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
|
||||
$project,
|
||||
$document,
|
||||
$response,
|
||||
$queueForEventsClone->from($queueForEvents),
|
||||
$queueForFunctions->from($queueForEvents),
|
||||
$queueForWebhooks->from($queueForEvents),
|
||||
$queueForRealtime->from($queueForEvents)
|
||||
));
|
||||
|
||||
$useCache = $route->getLabel('cache', false);
|
||||
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
|
||||
if ($useCache) {
|
||||
$route = $utopia->match($request);
|
||||
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged(Authorization::getRoles());
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged($authorization->getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
|
||||
);
|
||||
@@ -677,10 +508,10 @@ App::init()
|
||||
|
||||
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
|
||||
$bucketId = $parts[1] ?? null;
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -691,8 +522,7 @@ App::init()
|
||||
}
|
||||
|
||||
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
|
||||
$validator = new Authorization(Database::PERMISSION_READ);
|
||||
$valid = $validator->isValid($bucket->getRead());
|
||||
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
|
||||
if (!$fileSecurity && !$valid && !$isToken) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
@@ -703,7 +533,7 @@ App::init()
|
||||
if ($fileSecurity && !$valid && !$isToken) {
|
||||
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
|
||||
} else {
|
||||
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
|
||||
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
|
||||
}
|
||||
|
||||
if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) {
|
||||
@@ -714,11 +544,11 @@ App::init()
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
//Do not update transformedAt if it's a console user
|
||||
if (!User::isPrivileged(Authorization::getRoles())) {
|
||||
if (!User::isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -814,7 +644,10 @@ App::shutdown()
|
||||
->inject('queueForWebhooks')
|
||||
->inject('queueForRealtime')
|
||||
->inject('dbForProject')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
|
||||
->inject('authorization')
|
||||
->inject('timelimit')
|
||||
->inject('eventProcessor')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -823,9 +656,15 @@ App::shutdown()
|
||||
$queueForEvents->setPayload($responsePayload);
|
||||
}
|
||||
|
||||
$queueForFunctions
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
// Get project and function/webhook events (cached)
|
||||
$functionsEvents = $eventProcessor->getFunctionsEvents($project, $dbForProject);
|
||||
$webhooksEvents = $eventProcessor->getWebhooksEvents($project);
|
||||
|
||||
// Generate events for this operation
|
||||
$generatedEvents = Event::generateEvents(
|
||||
$queueForEvents->getEvent(),
|
||||
$queueForEvents->getParams()
|
||||
);
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
$queueForRealtime
|
||||
@@ -833,21 +672,69 @@ App::shutdown()
|
||||
->trigger();
|
||||
}
|
||||
|
||||
/** Trigger webhooks events only if a project has them enabled
|
||||
* A future optimisation is to only trigger webhooks if the webhook is "enabled"
|
||||
* But it might have performance implications on the API due to the number of webhooks etc.
|
||||
* Some profiling is needed to see if this is a problem.
|
||||
*/
|
||||
if (!empty($project->getAttribute('webhooks'))) {
|
||||
$queueForWebhooks
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
// Only trigger functions if there are matching function events
|
||||
if (!empty($functionsEvents)) {
|
||||
foreach ($generatedEvents as $event) {
|
||||
if (isset($functionsEvents[$event])) {
|
||||
$queueForFunctions
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only trigger webhooks if there are matching webhook events
|
||||
if (!empty($webhooksEvents)) {
|
||||
foreach ($generatedEvents as $event) {
|
||||
if (isset($webhooksEvents[$event])) {
|
||||
$queueForWebhooks
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
$requestParams = $route->getParamsValues();
|
||||
|
||||
/**
|
||||
* Abuse labels
|
||||
*/
|
||||
$abuseEnabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
|
||||
$abuseResetCode = $route->getLabel('abuse-reset', []);
|
||||
$abuseResetCode = \is_array($abuseResetCode) ? $abuseResetCode : [$abuseResetCode];
|
||||
|
||||
if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) {
|
||||
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
|
||||
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
|
||||
|
||||
foreach ($abuseKeyLabel as $abuseKey) {
|
||||
$start = $request->getContentRangeStart();
|
||||
$end = $request->getContentRangeEnd();
|
||||
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
|
||||
$timeLimit
|
||||
->setParam('{projectId}', $project->getId())
|
||||
->setParam('{userId}', $user->getId())
|
||||
->setParam('{userAgent}', $request->getUserAgent(''))
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getHostname() . $route->getPath())
|
||||
->setParam('{method}', $request->getMethod())
|
||||
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
|
||||
|
||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||
if (!empty($value)) {
|
||||
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
|
||||
}
|
||||
}
|
||||
|
||||
$abuse = new Abuse($timeLimit);
|
||||
$abuse->reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit labels
|
||||
*/
|
||||
@@ -940,11 +827,11 @@ App::shutdown()
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
$signature = md5($data['payload']);
|
||||
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$accessedAt = $cacheLog->getAttribute('accessedAt', 0);
|
||||
$now = DateTime::now();
|
||||
if ($cacheLog->isEmpty()) {
|
||||
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
|
||||
$authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
|
||||
'$id' => $key,
|
||||
'resource' => $resource,
|
||||
'resourceType' => $resourceType,
|
||||
@@ -954,7 +841,7 @@ App::shutdown()
|
||||
])));
|
||||
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
|
||||
$cacheLog->setAttribute('accessedAt', $now);
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
|
||||
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
|
||||
$cache->save($key, $data['payload']);
|
||||
}
|
||||
@@ -966,7 +853,7 @@ App::shutdown()
|
||||
}
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
if (!User::isPrivileged(Authorization::getRoles())) {
|
||||
if (!User::isPrivileged($authorization->getRoles())) {
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
|
||||
@@ -36,7 +36,8 @@ App::init()
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->action(function (App $utopia, Request $request, Document $project, Reader $geodb) {
|
||||
->inject('authorization')
|
||||
->action(function (App $utopia, Request $request, Document $project, Reader $geodb, Authorization $authorization) {
|
||||
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
|
||||
if (!empty($denylist && $project->getId() === 'console')) {
|
||||
$countries = explode(',', $denylist);
|
||||
@@ -49,8 +50,8 @@ App::init()
|
||||
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = User::isPrivileged(Authorization::getRoles());
|
||||
$isAppUser = User::isApp(Authorization::getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
|
||||
+17
-11
@@ -27,7 +27,6 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Log\User;
|
||||
use Utopia\Pools\Group;
|
||||
@@ -261,7 +260,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
|
||||
createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
|
||||
|
||||
// create appwrite database, `dbForPlatform` is a direct access call.
|
||||
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections) {
|
||||
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) {
|
||||
$authorization = $app->getResource('authorization');
|
||||
|
||||
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
|
||||
$adapter = new AdapterDatabase($dbForPlatform);
|
||||
$audit = new Audit($adapter);
|
||||
@@ -321,9 +322,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
|
||||
$dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes);
|
||||
}
|
||||
|
||||
if (Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) {
|
||||
if ($authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) {
|
||||
Console::info(" └── Creating screenshots bucket...");
|
||||
Authorization::skip(fn () => $dbForPlatform->createDocument('buckets', new Document([
|
||||
$authorization->skip(fn () => $dbForPlatform->createDocument('buckets', new Document([
|
||||
'$id' => ID::custom('screenshots'),
|
||||
'$collection' => ID::custom('buckets'),
|
||||
'name' => 'Screenshots',
|
||||
@@ -338,7 +339,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
|
||||
'search' => 'buckets Screenshots',
|
||||
])));
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
|
||||
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
|
||||
|
||||
Console::info(" └── Creating files collection for screenshots bucket...");
|
||||
$files = $collections['buckets']['files'] ?? [];
|
||||
@@ -366,7 +367,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
|
||||
'orders' => $index['orders'],
|
||||
]), $files['indexes']);
|
||||
|
||||
Authorization::skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes));
|
||||
$authorization->skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getSequence(), $attributes, $indexes));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -458,8 +459,12 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
App::setResource('pools', fn () => $pools);
|
||||
|
||||
try {
|
||||
Authorization::cleanRoles();
|
||||
Authorization::setRole(Role::any()->toString());
|
||||
$authorization = $app->getResource('authorization');
|
||||
|
||||
$request->setAuthorization($authorization);
|
||||
$response->setAuthorization($authorization);
|
||||
$authorization->cleanRoles();
|
||||
$authorization->addRole(Role::any()->toString());
|
||||
|
||||
$app->run($request, $response);
|
||||
} catch (\Throwable $th) {
|
||||
@@ -501,7 +506,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
$log->addExtra('file', $th->getFile());
|
||||
$log->addExtra('line', $th->getLine());
|
||||
$log->addExtra('trace', $th->getTraceAsString());
|
||||
$log->addExtra('roles', Authorization::getRoles());
|
||||
$log->addExtra('roles', isset($authorization) ? $authorization->getRoles() : []);
|
||||
|
||||
$sdk = $route->getLabel("sdk", false);
|
||||
|
||||
@@ -560,7 +565,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
$dbForPlatform = $app->getResource('dbForPlatform');
|
||||
|
||||
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $domains, &$lastSyncUpdate) {
|
||||
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $domains, &$lastSyncUpdate, $app) {
|
||||
try {
|
||||
$time = DateTime::now();
|
||||
$limit = 1000;
|
||||
@@ -577,7 +582,8 @@ $http->on(Constant::EVENT_TASK, function () use ($register, $domains) {
|
||||
}
|
||||
$results = [];
|
||||
try {
|
||||
$results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries));
|
||||
$authorization = $app->getResource('authorization');
|
||||
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
|
||||
} catch (Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ require_once __DIR__ . '/init/database/filters.php';
|
||||
require_once __DIR__ . '/init/database/formats.php';
|
||||
require_once __DIR__ . '/init/locales.php';
|
||||
require_once __DIR__ . '/init/registers.php';
|
||||
require_once __DIR__ . '/init/models.php';
|
||||
require_once __DIR__ . '/init/resources.php';
|
||||
|
||||
\stream_context_set_default([ // Set global user agent and http settings
|
||||
|
||||
@@ -44,7 +44,7 @@ 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.8.0';
|
||||
const APP_VERSION_STABLE = '1.8.1';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
@@ -161,6 +161,9 @@ const ACTIVITY_TYPE_GUEST = 'guest';
|
||||
const MFA_RECENT_DURATION = 1800; // 30 mins
|
||||
|
||||
|
||||
// Database name
|
||||
const APP_DATABASE = 'appwrite';
|
||||
|
||||
// Database Reconnect
|
||||
const DATABASE_RECONNECT_SLEEP = 2;
|
||||
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
|
||||
@@ -178,8 +181,10 @@ const BUILD_TYPE_DEPLOYMENT = 'deployment';
|
||||
const BUILD_TYPE_RETRY = 'retry';
|
||||
|
||||
// Deletion Types
|
||||
const DELETE_TYPE_DATABASES = 'databases';
|
||||
|
||||
const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false;
|
||||
|
||||
const DELETE_TYPE_DATABASES = 'databases';
|
||||
const DELETE_TYPE_DOCUMENT = 'document';
|
||||
const DELETE_TYPE_COLLECTIONS = 'collections';
|
||||
const DELETE_TYPE_TRANSACTION = 'transaction';
|
||||
@@ -191,6 +196,7 @@ const DELETE_TYPE_DEPLOYMENTS = 'deployments';
|
||||
const DELETE_TYPE_USERS = 'users';
|
||||
const DELETE_TYPE_TEAM_PROJECTS = 'teams_projects';
|
||||
const DELETE_TYPE_EXECUTIONS = 'executions';
|
||||
const DELETE_TYPE_EXECUTIONS_LIMIT = 'executionsLimit';
|
||||
const DELETE_TYPE_AUDIT = 'audit';
|
||||
const DELETE_TYPE_ABUSE = 'abuse';
|
||||
const DELETE_TYPE_USAGE = 'usage';
|
||||
|
||||
@@ -4,7 +4,6 @@ use Appwrite\OpenSSL\OpenSSL;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\System\System;
|
||||
|
||||
Database::addFilter(
|
||||
@@ -70,11 +69,11 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
$attributes = $database->find('attributes', [
|
||||
$attributes = $database->getAuthorization()->skip(fn () => $database->find('attributes', [
|
||||
Query::equal('collectionInternalId', [$document->getSequence()]),
|
||||
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
|
||||
Query::limit($database->getLimitForAttributes()),
|
||||
]);
|
||||
]));
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$attributeType = $attribute->getAttribute('type');
|
||||
@@ -105,12 +104,12 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('indexes', [
|
||||
Query::equal('collectionInternalId', [$document->getSequence()]),
|
||||
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
|
||||
Query::limit($database->getLimitForIndexes()),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -120,11 +119,11 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('platforms', [
|
||||
Query::equal('projectInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -134,17 +133,12 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('keys', [
|
||||
Query::or([
|
||||
Query::equal('projectInternalId', [$document->getSequence()]),
|
||||
Query::and([
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$document->getSequence()]),
|
||||
])
|
||||
]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -154,11 +148,11 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('devKeys', [
|
||||
Query::equal('projectInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -168,11 +162,11 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('webhooks', [
|
||||
Query::equal('projectInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -182,7 +176,7 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return Authorization::skip(fn () => $database->find('sessions', [
|
||||
return $database->getAuthorization()->skip(fn () => $database->find('sessions', [
|
||||
Query::equal('userInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]));
|
||||
@@ -195,7 +189,7 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return Authorization::skip(fn () => $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('tokens', [
|
||||
Query::equal('userInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
@@ -209,7 +203,7 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return Authorization::skip(fn () => $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('challenges', [
|
||||
Query::equal('userInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
@@ -223,7 +217,7 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return Authorization::skip(fn () => $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('authenticators', [
|
||||
Query::equal('userInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
@@ -237,7 +231,7 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return Authorization::skip(fn () => $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('memberships', [
|
||||
Query::equal('userInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
@@ -257,14 +251,14 @@ Database::addFilter(
|
||||
default => ['function', 'site']
|
||||
};
|
||||
|
||||
return $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('variables', [
|
||||
Query::equal('resourceInternalId', [$document->getSequence()]),
|
||||
Query::equal('resourceType', $resourceType),
|
||||
Query::orderAsc('resourceType'),
|
||||
Query::orderAsc(),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -300,11 +294,11 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('variables', [
|
||||
Query::equal('resourceType', ['project']),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -337,7 +331,7 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return Authorization::skip(fn () => $database
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('targets', [
|
||||
Query::equal('userInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
@@ -351,7 +345,7 @@ Database::addFilter(
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
$targetIds = Authorization::skip(fn () => \array_map(
|
||||
$targetIds = $database->getAuthorization()->skip(fn () => \array_map(
|
||||
fn ($document) => $document->getAttribute('targetInternalId'),
|
||||
$database->find('subscribers', [
|
||||
Query::equal('topicInternalId', [$document->getSequence()]),
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Account;
|
||||
use Appwrite\Utopia\Response\Model\AlgoArgon2;
|
||||
use Appwrite\Utopia\Response\Model\AlgoBcrypt;
|
||||
use Appwrite\Utopia\Response\Model\AlgoMd5;
|
||||
use Appwrite\Utopia\Response\Model\AlgoPhpass;
|
||||
use Appwrite\Utopia\Response\Model\AlgoScrypt;
|
||||
use Appwrite\Utopia\Response\Model\AlgoScryptModified;
|
||||
use Appwrite\Utopia\Response\Model\AlgoSha;
|
||||
use Appwrite\Utopia\Response\Model\Any;
|
||||
use Appwrite\Utopia\Response\Model\Attribute;
|
||||
use Appwrite\Utopia\Response\Model\AttributeBoolean;
|
||||
use Appwrite\Utopia\Response\Model\AttributeDatetime;
|
||||
use Appwrite\Utopia\Response\Model\AttributeEmail;
|
||||
use Appwrite\Utopia\Response\Model\AttributeEnum;
|
||||
use Appwrite\Utopia\Response\Model\AttributeFloat;
|
||||
use Appwrite\Utopia\Response\Model\AttributeInteger;
|
||||
use Appwrite\Utopia\Response\Model\AttributeIP;
|
||||
use Appwrite\Utopia\Response\Model\AttributeLine;
|
||||
use Appwrite\Utopia\Response\Model\AttributeList;
|
||||
use Appwrite\Utopia\Response\Model\AttributePoint;
|
||||
use Appwrite\Utopia\Response\Model\AttributePolygon;
|
||||
use Appwrite\Utopia\Response\Model\AttributeRelationship;
|
||||
use Appwrite\Utopia\Response\Model\AttributeString;
|
||||
use Appwrite\Utopia\Response\Model\AttributeURL;
|
||||
use Appwrite\Utopia\Response\Model\AuthProvider;
|
||||
use Appwrite\Utopia\Response\Model\BaseList;
|
||||
use Appwrite\Utopia\Response\Model\Branch;
|
||||
use Appwrite\Utopia\Response\Model\Bucket;
|
||||
use Appwrite\Utopia\Response\Model\Collection;
|
||||
use Appwrite\Utopia\Response\Model\Column;
|
||||
use Appwrite\Utopia\Response\Model\ColumnBoolean;
|
||||
use Appwrite\Utopia\Response\Model\ColumnDatetime;
|
||||
use Appwrite\Utopia\Response\Model\ColumnEmail;
|
||||
use Appwrite\Utopia\Response\Model\ColumnEnum;
|
||||
use Appwrite\Utopia\Response\Model\ColumnFloat;
|
||||
use Appwrite\Utopia\Response\Model\ColumnIndex;
|
||||
use Appwrite\Utopia\Response\Model\ColumnInteger;
|
||||
use Appwrite\Utopia\Response\Model\ColumnIP;
|
||||
use Appwrite\Utopia\Response\Model\ColumnLine;
|
||||
use Appwrite\Utopia\Response\Model\ColumnList;
|
||||
use Appwrite\Utopia\Response\Model\ColumnPoint;
|
||||
use Appwrite\Utopia\Response\Model\ColumnPolygon;
|
||||
use Appwrite\Utopia\Response\Model\ColumnRelationship;
|
||||
use Appwrite\Utopia\Response\Model\ColumnString;
|
||||
use Appwrite\Utopia\Response\Model\ColumnURL;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleVariables;
|
||||
use Appwrite\Utopia\Response\Model\Continent;
|
||||
use Appwrite\Utopia\Response\Model\Country;
|
||||
use Appwrite\Utopia\Response\Model\Currency;
|
||||
use Appwrite\Utopia\Response\Model\Database;
|
||||
use Appwrite\Utopia\Response\Model\Deployment;
|
||||
use Appwrite\Utopia\Response\Model\DetectionFramework;
|
||||
use Appwrite\Utopia\Response\Model\DetectionRuntime;
|
||||
use Appwrite\Utopia\Response\Model\DetectionVariable;
|
||||
use Appwrite\Utopia\Response\Model\DevKey;
|
||||
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
|
||||
use Appwrite\Utopia\Response\Model\Error;
|
||||
use Appwrite\Utopia\Response\Model\ErrorDev;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
use Appwrite\Utopia\Response\Model\File;
|
||||
use Appwrite\Utopia\Response\Model\Framework;
|
||||
use Appwrite\Utopia\Response\Model\FrameworkAdapter;
|
||||
use Appwrite\Utopia\Response\Model\Func;
|
||||
use Appwrite\Utopia\Response\Model\Headers;
|
||||
use Appwrite\Utopia\Response\Model\HealthAntivirus;
|
||||
use Appwrite\Utopia\Response\Model\HealthCertificate;
|
||||
use Appwrite\Utopia\Response\Model\HealthQueue;
|
||||
use Appwrite\Utopia\Response\Model\HealthStatus;
|
||||
use Appwrite\Utopia\Response\Model\HealthTime;
|
||||
use Appwrite\Utopia\Response\Model\HealthVersion;
|
||||
use Appwrite\Utopia\Response\Model\Identity;
|
||||
use Appwrite\Utopia\Response\Model\Index;
|
||||
use Appwrite\Utopia\Response\Model\Installation;
|
||||
use Appwrite\Utopia\Response\Model\JWT;
|
||||
use Appwrite\Utopia\Response\Model\Key;
|
||||
use Appwrite\Utopia\Response\Model\Language;
|
||||
use Appwrite\Utopia\Response\Model\Locale;
|
||||
use Appwrite\Utopia\Response\Model\LocaleCode;
|
||||
use Appwrite\Utopia\Response\Model\Log;
|
||||
use Appwrite\Utopia\Response\Model\Membership;
|
||||
use Appwrite\Utopia\Response\Model\Message;
|
||||
use Appwrite\Utopia\Response\Model\Metric;
|
||||
use Appwrite\Utopia\Response\Model\MetricBreakdown;
|
||||
use Appwrite\Utopia\Response\Model\MFAChallenge;
|
||||
use Appwrite\Utopia\Response\Model\MFAFactors;
|
||||
use Appwrite\Utopia\Response\Model\MFARecoveryCodes;
|
||||
use Appwrite\Utopia\Response\Model\MFAType;
|
||||
use Appwrite\Utopia\Response\Model\Migration;
|
||||
use Appwrite\Utopia\Response\Model\MigrationFirebaseProject;
|
||||
use Appwrite\Utopia\Response\Model\MigrationReport;
|
||||
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\Preferences;
|
||||
use Appwrite\Utopia\Response\Model\Project;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepository;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
|
||||
use Appwrite\Utopia\Response\Model\ResourceToken;
|
||||
use Appwrite\Utopia\Response\Model\Row;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
use Appwrite\Utopia\Response\Model\Runtime;
|
||||
use Appwrite\Utopia\Response\Model\Session;
|
||||
use Appwrite\Utopia\Response\Model\Site;
|
||||
use Appwrite\Utopia\Response\Model\Specification;
|
||||
use Appwrite\Utopia\Response\Model\Subscriber;
|
||||
use Appwrite\Utopia\Response\Model\Table;
|
||||
use Appwrite\Utopia\Response\Model\Target;
|
||||
use Appwrite\Utopia\Response\Model\Team;
|
||||
use Appwrite\Utopia\Response\Model\TemplateEmail;
|
||||
use Appwrite\Utopia\Response\Model\TemplateFramework;
|
||||
use Appwrite\Utopia\Response\Model\TemplateFunction;
|
||||
use Appwrite\Utopia\Response\Model\TemplateRuntime;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSite;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSMS;
|
||||
use Appwrite\Utopia\Response\Model\TemplateVariable;
|
||||
use Appwrite\Utopia\Response\Model\Token;
|
||||
use Appwrite\Utopia\Response\Model\Topic;
|
||||
use Appwrite\Utopia\Response\Model\Transaction;
|
||||
use Appwrite\Utopia\Response\Model\UsageBuckets;
|
||||
use Appwrite\Utopia\Response\Model\UsageCollection;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabase;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabases;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunction;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunctions;
|
||||
use Appwrite\Utopia\Response\Model\UsageProject;
|
||||
use Appwrite\Utopia\Response\Model\UsageSite;
|
||||
use Appwrite\Utopia\Response\Model\UsageSites;
|
||||
use Appwrite\Utopia\Response\Model\UsageStorage;
|
||||
use Appwrite\Utopia\Response\Model\UsageTable;
|
||||
use Appwrite\Utopia\Response\Model\UsageUsers;
|
||||
use Appwrite\Utopia\Response\Model\User;
|
||||
use Appwrite\Utopia\Response\Model\Variable;
|
||||
use Appwrite\Utopia\Response\Model\VcsContent;
|
||||
use Appwrite\Utopia\Response\Model\Webhook;
|
||||
|
||||
// General
|
||||
Response::setModel(new None());
|
||||
Response::setModel(new Any());
|
||||
Response::setModel(new Error());
|
||||
Response::setModel(new ErrorDev());
|
||||
|
||||
// Lists
|
||||
Response::setModel(new BaseList('Rows List', Response::MODEL_ROW_LIST, 'rows', Response::MODEL_ROW));
|
||||
Response::setModel(new BaseList('Documents List', Response::MODEL_DOCUMENT_LIST, 'documents', Response::MODEL_DOCUMENT));
|
||||
Response::setModel(new BaseList('Tables List', Response::MODEL_TABLE_LIST, 'tables', Response::MODEL_TABLE));
|
||||
Response::setModel(new BaseList('Collections List', Response::MODEL_COLLECTION_LIST, 'collections', Response::MODEL_COLLECTION));
|
||||
Response::setModel(new BaseList('Databases List', Response::MODEL_DATABASE_LIST, 'databases', Response::MODEL_DATABASE));
|
||||
Response::setModel(new BaseList('Indexes List', Response::MODEL_INDEX_LIST, 'indexes', Response::MODEL_INDEX));
|
||||
Response::setModel(new BaseList('Column Indexes List', Response::MODEL_COLUMN_INDEX_LIST, 'indexes', Response::MODEL_COLUMN_INDEX));
|
||||
Response::setModel(new BaseList('Users List', Response::MODEL_USER_LIST, 'users', Response::MODEL_USER));
|
||||
Response::setModel(new BaseList('Sessions List', Response::MODEL_SESSION_LIST, 'sessions', Response::MODEL_SESSION));
|
||||
Response::setModel(new BaseList('Identities List', Response::MODEL_IDENTITY_LIST, 'identities', Response::MODEL_IDENTITY));
|
||||
Response::setModel(new BaseList('Logs List', Response::MODEL_LOG_LIST, 'logs', Response::MODEL_LOG));
|
||||
Response::setModel(new BaseList('Files List', Response::MODEL_FILE_LIST, 'files', Response::MODEL_FILE));
|
||||
Response::setModel(new BaseList('Buckets List', Response::MODEL_BUCKET_LIST, 'buckets', Response::MODEL_BUCKET));
|
||||
Response::setModel(new BaseList('Resource Tokens List', Response::MODEL_RESOURCE_TOKEN_LIST, 'tokens', Response::MODEL_RESOURCE_TOKEN));
|
||||
Response::setModel(new BaseList('Teams List', Response::MODEL_TEAM_LIST, 'teams', Response::MODEL_TEAM));
|
||||
Response::setModel(new BaseList('Memberships List', Response::MODEL_MEMBERSHIP_LIST, 'memberships', Response::MODEL_MEMBERSHIP));
|
||||
Response::setModel(new BaseList('Sites List', Response::MODEL_SITE_LIST, 'sites', Response::MODEL_SITE));
|
||||
Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_SITE_LIST, 'templates', Response::MODEL_TEMPLATE_SITE));
|
||||
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
|
||||
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
|
||||
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
|
||||
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
|
||||
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
|
||||
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
|
||||
Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT));
|
||||
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
|
||||
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
|
||||
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
|
||||
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, false));
|
||||
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));
|
||||
Response::setModel(new BaseList('Currencies List', Response::MODEL_CURRENCY_LIST, 'currencies', Response::MODEL_CURRENCY));
|
||||
Response::setModel(new BaseList('Phones List', Response::MODEL_PHONE_LIST, 'phones', Response::MODEL_PHONE));
|
||||
Response::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'metrics', Response::MODEL_METRIC, true, false));
|
||||
Response::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
|
||||
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
|
||||
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
|
||||
Response::setModel(new BaseList('Locale codes list', Response::MODEL_LOCALE_CODE_LIST, 'localeCodes', Response::MODEL_LOCALE_CODE));
|
||||
Response::setModel(new BaseList('Provider list', Response::MODEL_PROVIDER_LIST, 'providers', Response::MODEL_PROVIDER));
|
||||
Response::setModel(new BaseList('Message list', Response::MODEL_MESSAGE_LIST, 'messages', Response::MODEL_MESSAGE));
|
||||
Response::setModel(new BaseList('Topic list', Response::MODEL_TOPIC_LIST, 'topics', Response::MODEL_TOPIC));
|
||||
Response::setModel(new BaseList('Subscriber list', Response::MODEL_SUBSCRIBER_LIST, 'subscribers', Response::MODEL_SUBSCRIBER));
|
||||
Response::setModel(new BaseList('Target list', Response::MODEL_TARGET_LIST, 'targets', Response::MODEL_TARGET));
|
||||
Response::setModel(new BaseList('Transaction List', Response::MODEL_TRANSACTION_LIST, 'transactions', Response::MODEL_TRANSACTION));
|
||||
Response::setModel(new BaseList('Migrations List', Response::MODEL_MIGRATION_LIST, 'migrations', Response::MODEL_MIGRATION));
|
||||
Response::setModel(new BaseList('Migrations Firebase Projects List', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', Response::MODEL_MIGRATION_FIREBASE_PROJECT));
|
||||
Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICATION_LIST, 'specifications', Response::MODEL_SPECIFICATION));
|
||||
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
|
||||
|
||||
// Entities
|
||||
Response::setModel(new Database());
|
||||
|
||||
// Collection API Models
|
||||
Response::setModel(new Collection());
|
||||
Response::setModel(new Attribute());
|
||||
Response::setModel(new AttributeList());
|
||||
Response::setModel(new AttributeString());
|
||||
Response::setModel(new AttributeInteger());
|
||||
Response::setModel(new AttributeFloat());
|
||||
Response::setModel(new AttributeBoolean());
|
||||
Response::setModel(new AttributeEmail());
|
||||
Response::setModel(new AttributeEnum());
|
||||
Response::setModel(new AttributeIP());
|
||||
Response::setModel(new AttributeURL());
|
||||
Response::setModel(new AttributeDatetime());
|
||||
Response::setModel(new AttributeRelationship());
|
||||
Response::setModel(new AttributePoint());
|
||||
Response::setModel(new AttributeLine());
|
||||
Response::setModel(new AttributePolygon());
|
||||
|
||||
// Table API Models
|
||||
Response::setModel(new Table());
|
||||
Response::setModel(new Column());
|
||||
Response::setModel(new ColumnList());
|
||||
Response::setModel(new ColumnString());
|
||||
Response::setModel(new ColumnInteger());
|
||||
Response::setModel(new ColumnFloat());
|
||||
Response::setModel(new ColumnBoolean());
|
||||
Response::setModel(new ColumnEmail());
|
||||
Response::setModel(new ColumnEnum());
|
||||
Response::setModel(new ColumnIP());
|
||||
Response::setModel(new ColumnURL());
|
||||
Response::setModel(new ColumnDatetime());
|
||||
Response::setModel(new ColumnRelationship());
|
||||
Response::setModel(new ColumnPoint());
|
||||
Response::setModel(new ColumnLine());
|
||||
Response::setModel(new ColumnPolygon());
|
||||
Response::setModel(new Index());
|
||||
Response::setModel(new ColumnIndex());
|
||||
Response::setModel(new Row());
|
||||
Response::setModel(new ModelDocument());
|
||||
Response::setModel(new Log());
|
||||
Response::setModel(new User());
|
||||
Response::setModel(new AlgoMd5());
|
||||
Response::setModel(new AlgoSha());
|
||||
Response::setModel(new AlgoPhpass());
|
||||
Response::setModel(new AlgoBcrypt());
|
||||
Response::setModel(new AlgoScrypt());
|
||||
Response::setModel(new AlgoScryptModified());
|
||||
Response::setModel(new AlgoArgon2());
|
||||
Response::setModel(new Account());
|
||||
Response::setModel(new Preferences());
|
||||
Response::setModel(new Session());
|
||||
Response::setModel(new Identity());
|
||||
Response::setModel(new Token());
|
||||
Response::setModel(new JWT());
|
||||
Response::setModel(new Locale());
|
||||
Response::setModel(new LocaleCode());
|
||||
Response::setModel(new File());
|
||||
Response::setModel(new Bucket());
|
||||
Response::setModel(new ResourceToken());
|
||||
Response::setModel(new Team());
|
||||
Response::setModel(new Membership());
|
||||
Response::setModel(new Site());
|
||||
Response::setModel(new TemplateSite());
|
||||
Response::setModel(new TemplateFramework());
|
||||
Response::setModel(new Func());
|
||||
Response::setModel(new TemplateFunction());
|
||||
Response::setModel(new TemplateRuntime());
|
||||
Response::setModel(new TemplateVariable());
|
||||
Response::setModel(new Installation());
|
||||
Response::setModel(new ProviderRepository());
|
||||
Response::setModel(new ProviderRepositoryFramework());
|
||||
Response::setModel(new ProviderRepositoryRuntime());
|
||||
Response::setModel(new DetectionFramework());
|
||||
Response::setModel(new DetectionRuntime());
|
||||
Response::setModel(new DetectionVariable());
|
||||
Response::setModel(new VcsContent());
|
||||
Response::setModel(new Branch());
|
||||
Response::setModel(new Runtime());
|
||||
Response::setModel(new Framework());
|
||||
Response::setModel(new FrameworkAdapter());
|
||||
Response::setModel(new Deployment());
|
||||
Response::setModel(new Execution());
|
||||
Response::setModel(new Project());
|
||||
Response::setModel(new Webhook());
|
||||
Response::setModel(new Key());
|
||||
Response::setModel(new DevKey());
|
||||
Response::setModel(new MockNumber());
|
||||
Response::setModel(new AuthProvider());
|
||||
Response::setModel(new Platform());
|
||||
Response::setModel(new Variable());
|
||||
Response::setModel(new Country());
|
||||
Response::setModel(new Continent());
|
||||
Response::setModel(new Language());
|
||||
Response::setModel(new Currency());
|
||||
Response::setModel(new Phone());
|
||||
Response::setModel(new HealthAntivirus());
|
||||
Response::setModel(new HealthQueue());
|
||||
Response::setModel(new HealthStatus());
|
||||
Response::setModel(new HealthCertificate());
|
||||
Response::setModel(new HealthTime());
|
||||
Response::setModel(new HealthVersion());
|
||||
Response::setModel(new Metric());
|
||||
Response::setModel(new MetricBreakdown());
|
||||
Response::setModel(new UsageDatabases());
|
||||
Response::setModel(new UsageDatabase());
|
||||
Response::setModel(new UsageTable());
|
||||
Response::setModel(new UsageCollection());
|
||||
Response::setModel(new UsageUsers());
|
||||
Response::setModel(new UsageStorage());
|
||||
Response::setModel(new UsageBuckets());
|
||||
Response::setModel(new UsageFunctions());
|
||||
Response::setModel(new UsageFunction());
|
||||
Response::setModel(new UsageSites());
|
||||
Response::setModel(new UsageSite());
|
||||
Response::setModel(new UsageProject());
|
||||
Response::setModel(new Headers());
|
||||
Response::setModel(new Specification());
|
||||
Response::setModel(new Rule());
|
||||
Response::setModel(new TemplateSMS());
|
||||
Response::setModel(new TemplateEmail());
|
||||
Response::setModel(new ConsoleVariables());
|
||||
Response::setModel(new MFAChallenge());
|
||||
Response::setModel(new MFARecoveryCodes());
|
||||
Response::setModel(new MFAType());
|
||||
Response::setModel(new MFAFactors());
|
||||
Response::setModel(new Provider());
|
||||
Response::setModel(new Message());
|
||||
Response::setModel(new Topic());
|
||||
Response::setModel(new Transaction());
|
||||
Response::setModel(new Subscriber());
|
||||
Response::setModel(new Target());
|
||||
Response::setModel(new Migration());
|
||||
Response::setModel(new MigrationReport());
|
||||
Response::setModel(new MigrationFirebaseProject());
|
||||
|
||||
// Tests (keep last)
|
||||
Response::setModel(new Mock());
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\GraphQL\Cache as GraphQLCache;
|
||||
use Appwrite\GraphQL\Promises\Adapter\Swoole;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
use Appwrite\PubSub\Adapter\Redis as PubSub;
|
||||
@@ -8,6 +9,7 @@ use Appwrite\URL\URL as AppwriteURL;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Database\PDOProxy;
|
||||
use Swoole\Table;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\CLI\Console;
|
||||
@@ -324,6 +326,12 @@ $register->set('pools', function () {
|
||||
Config::setParam('pools-' . $key, $config);
|
||||
}
|
||||
|
||||
$reconnectAttempts = (int) System::getEnv('_APP_CONNECTIONS_RECONNECT_ATTEMPTS', 5);
|
||||
$reconnectSleep = (int) System::getEnv('_APP_CONNECTIONS_RECONNECT_SLEEP', 2);
|
||||
|
||||
$group->setReconnectAttempts($reconnectAttempts);
|
||||
$group->setReconnectSleep($reconnectSleep);
|
||||
|
||||
return $group;
|
||||
});
|
||||
|
||||
@@ -360,6 +368,8 @@ $register->set('smtp', function () {
|
||||
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 10; /* Connection timeout */
|
||||
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
|
||||
|
||||
$from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
$email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
@@ -383,6 +393,23 @@ $register->set('passwordsDictionary', function () {
|
||||
$register->set('promiseAdapter', function () {
|
||||
return new Swoole();
|
||||
});
|
||||
|
||||
$graphqlFlags = new Table(100_000); // 100k projects max
|
||||
$graphqlFlags->column('timestamp', Table::TYPE_INT, 8);
|
||||
$graphqlFlags->create();
|
||||
|
||||
$register->set('graphqlFlags', fn () => $graphqlFlags);
|
||||
|
||||
$register->set('graphqlCache', function () use ($graphqlFlags) {
|
||||
$maxMB = (int) System::getEnv('_APP_GRAPHQL_SCHEMA_CACHE_MB', 50);
|
||||
return new GraphQLCache($maxMB, $graphqlFlags);
|
||||
});
|
||||
|
||||
$register->set('graphqlAPISchema', function () {
|
||||
// Container for API queries/mutations lazy init
|
||||
return new \stdClass();
|
||||
});
|
||||
|
||||
$register->set('hooks', function () {
|
||||
return new Hooks();
|
||||
});
|
||||
|
||||
+435
-102
@@ -15,10 +15,12 @@ use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\GraphQL\Schema;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Network\Platform;
|
||||
@@ -129,6 +131,9 @@ App::setResource('queueForMails', function (Publisher $publisher) {
|
||||
App::setResource('queueForBuilds', function (Publisher $publisher) {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
App::setResource('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
App::setResource('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
}, ['publisher']);
|
||||
@@ -153,6 +158,9 @@ App::setResource('queueForAudits', function (Publisher $publisher) {
|
||||
App::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
App::setResource('eventProcessor', function () {
|
||||
return new EventProcessor();
|
||||
}, []);
|
||||
App::setResource('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
@@ -226,7 +234,7 @@ App::setResource('allowedSchemes', function (Document $project) {
|
||||
/**
|
||||
* Rule associated with a request origin.
|
||||
*/
|
||||
App::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project) {
|
||||
App::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
|
||||
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
if (empty($domain)) {
|
||||
return new Document();
|
||||
@@ -234,7 +242,7 @@ App::setResource('rule', function (Request $request, Database $dbForPlatform, Do
|
||||
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
$rule = Authorization::skip(function () use ($dbForPlatform, $domain, $isMd5) {
|
||||
$rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) {
|
||||
if ($isMd5) {
|
||||
return $dbForPlatform->getDocument('rules', md5($domain));
|
||||
}
|
||||
@@ -249,7 +257,7 @@ App::setResource('rule', function (Request $request, Database $dbForPlatform, Do
|
||||
}
|
||||
|
||||
return $rule;
|
||||
}, ['request', 'dbForPlatform', 'project']);
|
||||
}, ['request', 'dbForPlatform', 'project', 'authorization']);
|
||||
|
||||
/**
|
||||
* CORS service
|
||||
@@ -317,7 +325,7 @@ App::setResource('redirectValidator', function (Document $devKey, array $allowed
|
||||
return new Redirect($allowedHostnames, $allowedSchemes);
|
||||
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
|
||||
|
||||
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) {
|
||||
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
|
||||
/**
|
||||
* Handles user authentication and session validation.
|
||||
*
|
||||
@@ -337,7 +345,7 @@ App::setResource('user', function (string $mode, Document $project, Document $co
|
||||
* overwriting the previous value.
|
||||
*/
|
||||
|
||||
Authorization::setDefaultStatus(true);
|
||||
$authorization->setDefaultStatus(true);
|
||||
|
||||
$store->setKey('a_session_' . $project->getId());
|
||||
|
||||
@@ -404,7 +412,7 @@ App::setResource('user', function (string $mode, Document $project, Document $co
|
||||
}
|
||||
// if (APP_MODE_ADMIN === $mode) {
|
||||
// if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) {
|
||||
// Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
|
||||
// $authorization->setDefaultStatus(false); // Cancel security segmentation for admin users.
|
||||
// } else {
|
||||
// $user = new Document([]);
|
||||
// }
|
||||
@@ -436,9 +444,9 @@ App::setResource('user', function (string $mode, Document $project, Document $co
|
||||
$dbForPlatform->setMetadata('user', $user->getId());
|
||||
|
||||
return $user;
|
||||
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']);
|
||||
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
|
||||
|
||||
App::setResource('project', function ($dbForPlatform, $request, $console) {
|
||||
App::setResource('project', function ($dbForPlatform, $request, $console, $authorization) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
/** @var Utopia\Database\Document $console */
|
||||
@@ -449,10 +457,10 @@ App::setResource('project', function ($dbForPlatform, $request, $console) {
|
||||
return $console;
|
||||
}
|
||||
|
||||
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
|
||||
return $project;
|
||||
}, ['dbForPlatform', 'request', 'console']);
|
||||
}, ['dbForPlatform', 'request', 'console', 'authorization']);
|
||||
|
||||
App::setResource('session', function (User $user, Store $store, Token $proofForToken) {
|
||||
if ($user->isEmpty()) {
|
||||
@@ -475,10 +483,6 @@ App::setResource('session', function (User $user, Store $store, Token $proofForT
|
||||
return;
|
||||
}, ['user', 'store', 'proofForToken']);
|
||||
|
||||
App::setResource('console', function () {
|
||||
return new Document(Config::getParam('console'));
|
||||
}, []);
|
||||
|
||||
App::setResource('store', function (): Store {
|
||||
return new Store();
|
||||
});
|
||||
@@ -509,7 +513,15 @@ App::setResource('proofForCode', function (): Code {
|
||||
return $code;
|
||||
});
|
||||
|
||||
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
|
||||
App::setResource('console', function () {
|
||||
return new Document(Config::getParam('console'));
|
||||
}, []);
|
||||
|
||||
App::setResource('authorization', function () {
|
||||
return new Authorization();
|
||||
}, []);
|
||||
|
||||
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage, Authorization $authorization) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
@@ -525,6 +537,8 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
@@ -545,14 +559,218 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
/**
|
||||
* This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd, but it is **intentional**.
|
||||
*
|
||||
* Accounts can be created in many ways beyond `createAccount`
|
||||
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
|
||||
*/
|
||||
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
|
||||
// Only trigger events for user creation with the database listener.
|
||||
if ($document->getCollection() !== 'users') {
|
||||
return;
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
->setEvent('users.[userId].create')
|
||||
->setParam('userId', $document->getId())
|
||||
->setPayload($response->output($document, Response::MODEL_USER));
|
||||
|
||||
// Trigger functions, webhooks, and realtime events
|
||||
$queueForFunctions
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
|
||||
|
||||
/** Trigger webhooks events only if a project has them enabled */
|
||||
if (!empty($project->getAttribute('webhooks'))) {
|
||||
$queueForWebhooks
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
/** Trigger realtime events only for non console events */
|
||||
if ($queueForEvents->getProject()->getId() !== 'console') {
|
||||
$queueForRealtime
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Purge function events cache when functions are created, updated or deleted.
|
||||
*/
|
||||
$functionsEventsCacheListener = function (string $event, Document $document, Document $project, Database $dbForProject) {
|
||||
|
||||
|
||||
if ($document->getCollection() !== 'functions') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return;
|
||||
}
|
||||
|
||||
$hostname = $dbForProject->getAdapter()->getHostname();
|
||||
$cacheKey = \sprintf(
|
||||
'%s-cache-%s:%s:%s:project:%s:functions:events',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$project->getId()
|
||||
);
|
||||
|
||||
$dbForProject->getCache()->purge($cacheKey);
|
||||
};
|
||||
|
||||
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
|
||||
$value = 1;
|
||||
|
||||
switch ($event) {
|
||||
case Database::EVENT_DOCUMENT_DELETE:
|
||||
$value = -1;
|
||||
break;
|
||||
case Database::EVENT_DOCUMENTS_DELETE:
|
||||
$value = -1 * $document->getAttribute('modified', 0);
|
||||
break;
|
||||
case Database::EVENT_DOCUMENTS_CREATE:
|
||||
$value = $document->getAttribute('modified', 0);
|
||||
break;
|
||||
case Database::EVENT_DOCUMENTS_UPSERT:
|
||||
$value = $document->getAttribute('created', 0);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case $document->getCollection() === 'teams':
|
||||
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
|
||||
break;
|
||||
case $document->getCollection() === 'users':
|
||||
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'sessions': // sessions
|
||||
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
|
||||
break;
|
||||
case $document->getCollection() === 'databases': // databases
|
||||
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_COLLECTIONS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DOCUMENTS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
|
||||
break;
|
||||
case $document->getCollection() === 'buckets': //buckets
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_BUCKETS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'bucket_'): // files
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$bucketInternalId = $parts[1];
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_FILES, $value) // per project
|
||||
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
|
||||
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
|
||||
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
|
||||
break;
|
||||
case $document->getCollection() === 'functions':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_FUNCTIONS, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'sites':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_SITES, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'deployments':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
|
||||
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
|
||||
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function
|
||||
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value)
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $value) // per function
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Clone the queues, to prevent events triggered by the database listener
|
||||
// from overwriting the events that are supposed to be triggered in the shutdown hook.
|
||||
$queueForEventsClone = new Event($publisher);
|
||||
$queueForFunctions = new Func($publisherFunctions);
|
||||
$queueForWebhooks = new Webhook($publisherWebhooks);
|
||||
$queueForRealtime = new Realtime();
|
||||
|
||||
|
||||
$database
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
|
||||
$project,
|
||||
$document,
|
||||
$response,
|
||||
$queueForEventsClone->from($queueForEvents),
|
||||
$queueForFunctions->from($queueForEvents),
|
||||
$queueForWebhooks->from($queueForEvents),
|
||||
$queueForRealtime->from($queueForEvents)
|
||||
))
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
|
||||
->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
|
||||
;
|
||||
|
||||
|
||||
return $database;
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'project']);
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'queueForStatsUsage', 'authorization']);
|
||||
|
||||
App::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
|
||||
App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', 'console')
|
||||
@@ -562,12 +780,12 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
|
||||
$database->setDocumentType('users', User::class);
|
||||
|
||||
return $database;
|
||||
}, ['pools', 'cache']);
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
|
||||
App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
|
||||
$databases = [];
|
||||
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
@@ -579,13 +797,16 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
$configure = (function (Database $database) use ($project, $dsn) {
|
||||
$configure = (function (Database $database) use ($project, $dsn, $authorization) {
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
$database->setDocumentType('users', User::class);
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES)
|
||||
->setDocumentType('users', User::class)
|
||||
;
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
@@ -615,12 +836,12 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache']);
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
App::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, &$database) {
|
||||
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int) $project->getSequence());
|
||||
return $database;
|
||||
@@ -630,6 +851,8 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
@@ -642,7 +865,7 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache']);
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
App::setResource('audit', function ($dbForProject) {
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
@@ -841,7 +1064,17 @@ App::setResource('promiseAdapter', function ($register) {
|
||||
return $register->get('promiseAdapter');
|
||||
}, ['register']);
|
||||
|
||||
App::setResource('schema', function ($utopia, $dbForProject) {
|
||||
App::setResource('graphqlCache', function ($register) {
|
||||
return $register->get('graphqlCache');
|
||||
}, ['register']);
|
||||
|
||||
App::setResource('graphqlAPISchema', function ($register) {
|
||||
return $register->get('graphqlAPISchema');
|
||||
}, ['register']);
|
||||
|
||||
App::setResource('schema', function ($utopia, $dbForProject, $project, $graphqlCache, $authorization) {
|
||||
|
||||
$projectId = $project->getId();
|
||||
|
||||
$complexity = function (int $complexity, array $args) {
|
||||
$queries = Query::parseQueries($args['queries'] ?? []);
|
||||
@@ -851,82 +1084,174 @@ App::setResource('schema', function ($utopia, $dbForProject) {
|
||||
return $complexity * $limit;
|
||||
};
|
||||
|
||||
$attributes = function (int $limit, int $offset) use ($dbForProject) {
|
||||
$attrs = Authorization::skip(fn () => $dbForProject->find('attributes', [
|
||||
Query::limit($limit),
|
||||
Query::offset($offset),
|
||||
]));
|
||||
$types = null;
|
||||
|
||||
return \array_map(function ($attr) {
|
||||
return $attr->getArrayCopy();
|
||||
}, $attrs);
|
||||
$attributes = function (int $limit, ?Document $last) use ($dbForProject, $projectId, $authorization, &$types) {
|
||||
// Console project doesn't have user-created databases/collections
|
||||
if ($projectId === 'console') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Lazy load database types on first pagination call
|
||||
if ($types === null) {
|
||||
$types = [];
|
||||
$databases = $authorization->skip(fn () => $dbForProject->find('databases', [
|
||||
Query::limit(APP_LIMIT_COUNT),
|
||||
]));
|
||||
foreach ($databases as $db) {
|
||||
$dbType = $db->getAttribute('type', 'legacy');
|
||||
if (!\in_array($dbType, ['legacy', 'tablesdb'])) {
|
||||
Console::warning("Unknown database type '{$dbType}' for database {$db->getId()}, using 'legacy'");
|
||||
$dbType = 'legacy';
|
||||
}
|
||||
$types[$db->getId()] = $dbType;
|
||||
}
|
||||
}
|
||||
|
||||
$queries = [
|
||||
Query::equal('status', ['available']),
|
||||
Query::limit($limit),
|
||||
];
|
||||
|
||||
if ($last !== null) {
|
||||
$queries[] = Query::cursorAfter($last);
|
||||
}
|
||||
|
||||
$attributes = $authorization->skip(fn () => $dbForProject->find('attributes', $queries));
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$dbId = $attribute->getAttribute('databaseId');
|
||||
$attribute->setAttribute('databaseType', $types[$dbId] ?? 'legacy');
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
};
|
||||
|
||||
$urls = [
|
||||
'list' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
},
|
||||
'create' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
},
|
||||
'read' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}";
|
||||
},
|
||||
'update' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}";
|
||||
},
|
||||
'delete' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['documentId']}";
|
||||
},
|
||||
'legacy' => [
|
||||
'list' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
},
|
||||
'create' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents";
|
||||
},
|
||||
'read' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
},
|
||||
'update' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
},
|
||||
'delete' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return "/v1/databases/$databaseId/collections/$collectionId/documents/{$args['id']}";
|
||||
},
|
||||
],
|
||||
'tablesdb' => [
|
||||
'list' => function (string $databaseId, string $tableId, array $args) {
|
||||
return "/v1/tablesdb/$databaseId/tables/$tableId/rows";
|
||||
},
|
||||
'create' => function (string $databaseId, string $tableId, array $args) {
|
||||
return "/v1/tablesdb/$databaseId/tables/$tableId/rows";
|
||||
},
|
||||
'read' => function (string $databaseId, string $tableId, array $args) {
|
||||
return "/v1/tablesdb/$databaseId/tables/$tableId/rows/{$args['id']}";
|
||||
},
|
||||
'update' => function (string $databaseId, string $tableId, array $args) {
|
||||
return "/v1/tablesdb/$databaseId/tables/$tableId/rows/{$args['id']}";
|
||||
},
|
||||
'delete' => function (string $databaseId, string $tableId, array $args) {
|
||||
return "/v1/tablesdb/$databaseId/tables/$tableId/rows/{$args['id']}";
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
// NOTE: `params` and `urls` are not used internally in the `Schema::build` function below!
|
||||
$params = [
|
||||
'list' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return ['queries' => $args['queries']];
|
||||
},
|
||||
'create' => function (string $databaseId, string $collectionId, array $args) {
|
||||
$id = $args['id'] ?? 'unique()';
|
||||
$permissions = $args['permissions'] ?? null;
|
||||
'legacy' => [
|
||||
'list' => function (string $databaseId, string $collectionId, array $args) {
|
||||
return ['queries' => $args['queries'] ?? []];
|
||||
},
|
||||
'create' => function (string $databaseId, string $collectionId, array $args) {
|
||||
$id = $args['id'] ?? 'unique()';
|
||||
$permissions = $args['permissions'] ?? null;
|
||||
|
||||
unset($args['id']);
|
||||
unset($args['permissions']);
|
||||
unset($args['id']);
|
||||
unset($args['permissions']);
|
||||
|
||||
// Order must be the same as the route params
|
||||
return [
|
||||
'databaseId' => $databaseId,
|
||||
'documentId' => $id,
|
||||
'collectionId' => $collectionId,
|
||||
'data' => $args,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
},
|
||||
'update' => function (string $databaseId, string $collectionId, array $args) {
|
||||
$documentId = $args['id'];
|
||||
$permissions = $args['permissions'] ?? null;
|
||||
// Order must be the same as the route params
|
||||
return [
|
||||
'databaseId' => $databaseId,
|
||||
'documentId' => $id,
|
||||
'collectionId' => $collectionId,
|
||||
'data' => $args,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
},
|
||||
'update' => function (string $databaseId, string $collectionId, array $args) {
|
||||
$documentId = $args['id'];
|
||||
$permissions = $args['permissions'] ?? null;
|
||||
|
||||
unset($args['id']);
|
||||
unset($args['permissions']);
|
||||
unset($args['id']);
|
||||
unset($args['permissions']);
|
||||
|
||||
// Order must be the same as the route params
|
||||
return [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'documentId' => $documentId,
|
||||
'data' => $args,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
},
|
||||
// Order must be the same as the route params
|
||||
return [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
'documentId' => $documentId,
|
||||
'data' => $args,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
},
|
||||
],
|
||||
'tablesdb' => [
|
||||
'list' => function (string $databaseId, string $tableId, array $args) {
|
||||
return ['queries' => $args['queries'] ?? []];
|
||||
},
|
||||
'create' => function (string $databaseId, string $tableId, array $args) {
|
||||
$id = $args['id'] ?? 'unique()';
|
||||
$permissions = $args['permissions'] ?? null;
|
||||
|
||||
unset($args['id']);
|
||||
unset($args['permissions']);
|
||||
|
||||
// Order must be the same as the route params
|
||||
return [
|
||||
'databaseId' => $databaseId,
|
||||
'rowId' => $id,
|
||||
'tableId' => $tableId,
|
||||
'data' => $args,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
},
|
||||
'update' => function (string $databaseId, string $tableId, array $args) {
|
||||
$rowId = $args['id'];
|
||||
$permissions = $args['permissions'] ?? null;
|
||||
|
||||
unset($args['id']);
|
||||
unset($args['permissions']);
|
||||
|
||||
// Order must be the same as the route params
|
||||
return [
|
||||
'databaseId' => $databaseId,
|
||||
'tableId' => $tableId,
|
||||
'rowId' => $rowId,
|
||||
'data' => $args,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return Schema::build(
|
||||
$schema = new Schema($projectId);
|
||||
|
||||
return $schema->build(
|
||||
$utopia,
|
||||
$graphqlCache,
|
||||
$complexity,
|
||||
$attributes,
|
||||
$urls,
|
||||
$params,
|
||||
);
|
||||
}, ['utopia', 'dbForProject']);
|
||||
}, ['utopia', 'dbForProject', 'project', 'graphqlCache', 'authorization']);
|
||||
|
||||
App::setResource('gitHub', function (Cache $cache) {
|
||||
return new VcsGitHub($cache);
|
||||
@@ -954,7 +1279,7 @@ App::setResource('smsRates', function () {
|
||||
return [];
|
||||
});
|
||||
|
||||
App::setResource('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform) {
|
||||
App::setResource('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
|
||||
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
|
||||
|
||||
// Check if given key match project's development keys
|
||||
@@ -973,7 +1298,7 @@ App::setResource('devKey', function (Request $request, Document $project, array
|
||||
$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(), $key));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
@@ -990,15 +1315,15 @@ App::setResource('devKey', function (Request $request, Document $project, array
|
||||
|
||||
/** Update access time as well */
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$key = Authorization::skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
return $key;
|
||||
}, ['request', 'project', 'servers', 'dbForPlatform']);
|
||||
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
|
||||
|
||||
App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request) {
|
||||
App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request, Authorization $authorization) {
|
||||
$teamInternalId = '';
|
||||
if ($project->getId() !== 'console') {
|
||||
$teamInternalId = $project->getAttribute('teamInternalId', '');
|
||||
@@ -1008,7 +1333,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
|
||||
if (str_starts_with($path, '/v1/projects/:projectId')) {
|
||||
$uri = $request->getURI();
|
||||
$pid = explode('/', $uri)[3];
|
||||
$p = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $pid));
|
||||
$p = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $pid));
|
||||
$teamInternalId = $p->getAttribute('teamInternalId', '');
|
||||
} elseif ($path === '/v1/projects') {
|
||||
$teamId = $request->getParam('teamId', '');
|
||||
@@ -1017,7 +1342,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$team = Authorization::skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
|
||||
$team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
|
||||
return $team;
|
||||
}
|
||||
}
|
||||
@@ -1026,14 +1351,14 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$team = Authorization::skip(function () use ($dbForPlatform, $teamInternalId) {
|
||||
$team = $authorization->skip(function () use ($dbForPlatform, $teamInternalId) {
|
||||
return $dbForPlatform->findOne('teams', [
|
||||
Query::equal('$sequence', [$teamInternalId]),
|
||||
]);
|
||||
});
|
||||
|
||||
return $team;
|
||||
}, ['project', 'dbForPlatform', 'utopia', 'request']);
|
||||
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
|
||||
|
||||
App::setResource(
|
||||
'isResourceBlocked',
|
||||
@@ -1071,7 +1396,7 @@ App::setResource('apiKey', function (Request $request, Document $project): ?Key
|
||||
|
||||
App::setResource('executor', fn () => new Executor());
|
||||
|
||||
App::setResource('resourceToken', function ($project, $dbForProject, $request) {
|
||||
App::setResource('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
|
||||
$tokenJWT = $request->getParam('token');
|
||||
|
||||
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
|
||||
@@ -1089,7 +1414,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$token = Authorization::skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId));
|
||||
$token = $authorization->skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId));
|
||||
|
||||
if ($token->isEmpty()) {
|
||||
return new Document([]);
|
||||
@@ -1107,7 +1432,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
|
||||
}
|
||||
|
||||
return match ($token->getAttribute('resourceType')) {
|
||||
TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject) {
|
||||
TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject, $authorization) {
|
||||
$sequences = explode(':', $token->getAttribute('resourceInternalId'));
|
||||
$ids = explode(':', $token->getAttribute('resourceId'));
|
||||
|
||||
@@ -1118,7 +1443,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
|
||||
$accessedAt = $token->getAttribute('accessedAt', 0);
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
|
||||
$token->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
|
||||
}
|
||||
|
||||
return new Document([
|
||||
@@ -1133,8 +1458,16 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
|
||||
};
|
||||
}
|
||||
return new Document([]);
|
||||
}, ['project', 'dbForProject', 'request']);
|
||||
}, ['project', 'dbForProject', 'request', 'authorization']);
|
||||
|
||||
App::setResource('transactionState', function (Database $dbForProject) {
|
||||
return new TransactionState($dbForProject);
|
||||
}, ['dbForProject']);
|
||||
App::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) {
|
||||
return new TransactionState($dbForProject, $authorization);
|
||||
}, ['dbForProject', 'authorization']);
|
||||
|
||||
App::setResource('executionsRetentionCount', function (Document $project, array $plan) {
|
||||
if ($project->getId() === 'console' || empty($plan)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) ($plan['executionsRetentionCount'] ?? 100);
|
||||
}, ['project', 'plan']);
|
||||
|
||||
+44
-14
@@ -29,10 +29,10 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
@@ -67,6 +67,7 @@ if (!function_exists('getConsoleDB')) {
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$database = new Database($adapter, getCache());
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setNamespace('_console')
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', '_console');
|
||||
@@ -123,6 +124,7 @@ if (!function_exists('getProjectDB')) {
|
||||
}
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId());
|
||||
|
||||
@@ -309,7 +311,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
'value' => '{}'
|
||||
]);
|
||||
|
||||
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document));
|
||||
$statsDocument = $database->getAuthorization()->skip(fn () => $database->createDocument('realtime', $document));
|
||||
break;
|
||||
} catch (Throwable) {
|
||||
Console::warning("Collection not ready. Retrying connection ({$attempts})...");
|
||||
@@ -339,7 +341,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
->setAttribute('timestamp', DateTime::now())
|
||||
->setAttribute('value', json_encode($payload));
|
||||
|
||||
Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
|
||||
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
|
||||
} catch (Throwable $th) {
|
||||
$logError($th, "updateWorkerDocument");
|
||||
}
|
||||
@@ -370,7 +372,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
|
||||
$payload = [];
|
||||
|
||||
$list = Authorization::skip(fn () => $database->find('realtime', [
|
||||
$list = $database->getAuthorization()->skip(fn () => $database->find('realtime', [
|
||||
Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)),
|
||||
]));
|
||||
|
||||
@@ -464,17 +466,18 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
|
||||
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
|
||||
$consoleDatabase = getConsoleDB();
|
||||
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $user */
|
||||
$user = $database->getDocument('users', $userId);
|
||||
|
||||
$roles = $user->getRoles();
|
||||
$roles = $user->getRoles($database->getAuthorization());
|
||||
$channels = $realtime->connections[$connection]['channels'];
|
||||
$queries = $realtime->connections[$connection]['queries'] ?? [];
|
||||
|
||||
$realtime->unsubscribe($connection);
|
||||
$realtime->subscribe($projectId, $connection, $roles, $channels);
|
||||
$realtime->subscribe($projectId, $connection, $roles, $channels, $queries);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +529,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
try {
|
||||
/** @var Document $project */
|
||||
$project = $app->getResource('project');
|
||||
$authorization = $app->getResource('authorization');
|
||||
|
||||
/*
|
||||
* Project Check
|
||||
@@ -537,7 +541,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
if (
|
||||
array_key_exists('realtime', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['realtime']
|
||||
&& !(User::isPrivileged(Authorization::getRoles()) || User::isApp(Authorization::getRoles()))
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -573,9 +577,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
|
||||
}
|
||||
|
||||
$roles = $user->getRoles();
|
||||
$roles = $user->getRoles($authorization);
|
||||
|
||||
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
|
||||
try {
|
||||
$queries = Realtime::convertQueries($request->getQuery('queries', []));
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $e->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Channels Check
|
||||
@@ -584,7 +593,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
|
||||
}
|
||||
|
||||
$realtime->subscribe($project->getId(), $connection, $roles, $channels);
|
||||
$realtime->subscribe($project->getId(), $connection, $roles, $channels, $queries);
|
||||
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
|
||||
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
|
||||
|
||||
@@ -592,6 +603,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
'type' => 'connected',
|
||||
'data' => [
|
||||
'channels' => array_keys($channels),
|
||||
'queries' => $queries,
|
||||
'user' => $user
|
||||
]
|
||||
]));
|
||||
@@ -614,6 +626,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$code = 500;
|
||||
}
|
||||
|
||||
|
||||
$message = $th->getMessage();
|
||||
|
||||
// sanitize 0 && 5xx errors
|
||||
@@ -643,12 +656,19 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
|
||||
try {
|
||||
$response = new Response(new SwooleResponse());
|
||||
$projectId = $realtime->connections[$connection]['projectId'];
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
|
||||
// Get authorization from connection (stored during onOpen)
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
|
||||
$database = getConsoleDB();
|
||||
$database->setAuthorization($authorization);
|
||||
|
||||
if ($projectId !== 'console') {
|
||||
$project = Authorization::skip(fn () => $database->getDocument('projects', $projectId));
|
||||
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
|
||||
|
||||
$database = getProjectDB($project);
|
||||
$database->setAuthorization($authorization);
|
||||
} else {
|
||||
$project = null;
|
||||
}
|
||||
@@ -712,9 +732,19 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
|
||||
}
|
||||
|
||||
$roles = $user->getRoles();
|
||||
$roles = $user->getRoles($database->getAuthorization());
|
||||
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
|
||||
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
|
||||
|
||||
// Preserve authorization before subscribe overwrites the connection array
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
|
||||
$queries = $realtime->connections[$connection]['queries'];
|
||||
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels, $queries);
|
||||
|
||||
// Restore authorization after subscribe
|
||||
if ($authorization !== null) {
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
}
|
||||
|
||||
$user = $response->output($user, Response::MODEL_ACCOUNT);
|
||||
$server->send([$connection], json_encode([
|
||||
|
||||
@@ -165,6 +165,7 @@ $enableAssistant = $this->getParam('enableAssistant', false);
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES
|
||||
- _APP_SMS_PROVIDER
|
||||
- _APP_SMS_FROM
|
||||
- _APP_GRAPHQL_INTROSPECTION
|
||||
- _APP_GRAPHQL_MAX_BATCH_SIZE
|
||||
- _APP_GRAPHQL_MAX_COMPLEXITY
|
||||
- _APP_GRAPHQL_MAX_DEPTH
|
||||
@@ -180,7 +181,7 @@ $enableAssistant = $this->getParam('enableAssistant', false);
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: <?php echo $organization; ?>/console:7.4.7
|
||||
image: <?php echo $organization; ?>/console:7.5.7
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
+52
-23
@@ -14,6 +14,7 @@ use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
@@ -48,19 +49,31 @@ use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
|
||||
Authorization::disable();
|
||||
Runtime::enableCoroutine();
|
||||
|
||||
Server::setResource('register', fn () => $register);
|
||||
|
||||
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register) {
|
||||
Server::setResource('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);
|
||||
$dbForPlatform->setNamespace('_console');
|
||||
$dbForPlatform->setDocumentType('users', User::class);
|
||||
|
||||
$dbForPlatform
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setDocumentType('users', User::class)
|
||||
;
|
||||
|
||||
|
||||
return $dbForPlatform;
|
||||
}, ['cache', 'register']);
|
||||
}, ['cache', 'register', 'authorization']);
|
||||
|
||||
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
@@ -73,7 +86,7 @@ Server::setResource('project', function (Message $message, Database $dbForPlatfo
|
||||
return $dbForPlatform->getDocument('projects', $project->getId());
|
||||
}, ['message', 'dbForPlatform']);
|
||||
|
||||
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $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;
|
||||
}
|
||||
@@ -105,15 +118,18 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
}, ['cache', 'register', 'message', 'project', 'dbForPlatform']);
|
||||
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
|
||||
|
||||
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
|
||||
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, &$databases): Database {
|
||||
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
@@ -127,7 +143,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
|
||||
|
||||
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)) {
|
||||
@@ -164,15 +180,18 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
|
||||
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'dbForPlatform', 'cache']);
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
return function (?Document $project = null) use ($pools, $cache, $database) {
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int)$project->getSequence());
|
||||
return $database;
|
||||
@@ -182,6 +201,8 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
|
||||
@@ -194,7 +215,7 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
|
||||
|
||||
return $database;
|
||||
};
|
||||
}, ['pools', 'cache']);
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('abuseRetention', function () {
|
||||
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
|
||||
@@ -307,6 +328,10 @@ 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']);
|
||||
@@ -465,6 +490,14 @@ Server::setResource('getAudit', function (Database $dbForPlatform, callable $get
|
||||
};
|
||||
}, ['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');
|
||||
@@ -509,7 +542,8 @@ $worker
|
||||
->inject('log')
|
||||
->inject('pools')
|
||||
->inject('project')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($worker, $queueName) {
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($worker, $queueName) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
if ($logger) {
|
||||
@@ -525,7 +559,7 @@ $worker
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('roles', Authorization::getRoles());
|
||||
$log->addExtra('roles', $authorization->getRoles());
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
@@ -544,9 +578,4 @@ $worker
|
||||
Console::error('[Error] Line: ' . $error->getLine());
|
||||
});
|
||||
|
||||
$worker->workerStart()
|
||||
->action(function () use ($workerName) {
|
||||
Console::info("Worker $workerName started");
|
||||
});
|
||||
|
||||
$worker->start();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec php /usr/src/code/app/worker.php screenshots "$@"
|
||||
+11
-11
@@ -47,32 +47,32 @@
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "1.*",
|
||||
"utopia-php/analytics": "0.10.*",
|
||||
"utopia-php/audit": "2.0.2-rc1",
|
||||
"utopia-php/audit": "2.*",
|
||||
"utopia-php/auth": "0.5.*",
|
||||
"utopia-php/cache": "0.13.*",
|
||||
"utopia-php/cli": "0.15.*",
|
||||
"utopia-php/config": "1.*.*",
|
||||
"utopia-php/database": "3.*.*",
|
||||
"utopia-php/config": "1.*",
|
||||
"utopia-php/database": "4.*",
|
||||
"utopia-php/detector": "0.2.*",
|
||||
"utopia-php/domains": "0.9.*",
|
||||
"utopia-php/domains": "0.11.*",
|
||||
"utopia-php/emails": "0.6.*",
|
||||
"utopia-php/dns": "1.4.*",
|
||||
"utopia-php/dns": "1.5.*",
|
||||
"utopia-php/dsn": "0.2.1",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/fetch": "0.4.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/image": "0.8.*",
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/migration": "1.3.*",
|
||||
"utopia-php/migration": "1.*",
|
||||
"utopia-php/orchestration": "0.9.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/pools": "0.8.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.11.*",
|
||||
"utopia-php/queue": "0.15.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "0.18.*",
|
||||
"utopia-php/swoole": "0.8.*",
|
||||
"utopia-php/swoole": "1.*",
|
||||
"utopia-php/system": "0.9.*",
|
||||
"utopia-php/telemetry": "0.1.*",
|
||||
"utopia-php/vcs": "0.13.*",
|
||||
@@ -83,7 +83,7 @@
|
||||
"chillerlan/php-qrcode": "4.4.*",
|
||||
"adhocore/jwt": "1.1.*",
|
||||
"spomky-labs/otphp": "10.0.*",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"webonyx/graphql-php": "15.24.*",
|
||||
"league/csv": "9.24.*",
|
||||
"enshrined/svg-sanitize": "0.22.*"
|
||||
},
|
||||
@@ -109,4 +109,4 @@
|
||||
"tbachert/spi": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+222
-172
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "b873febd2b03c32ec61a57b690cc44a2",
|
||||
"content-hash": "7d3c04ff783454cb9ae8eff5f3e7088e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -69,16 +69,16 @@
|
||||
},
|
||||
{
|
||||
"name": "appwrite/appwrite",
|
||||
"version": "15.1.0",
|
||||
"version": "19.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-for-php.git",
|
||||
"reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b"
|
||||
"reference": "8738e812062f899c85b2598eef43d6a247f08a56"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/c438b3885071ac7c0329199dce5e6f6a24dd215b",
|
||||
"reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56",
|
||||
"reference": "8738e812062f899c85b2598eef43d6a247f08a56",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -87,7 +87,7 @@
|
||||
"php": ">=7.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.6.6",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"phpunit/phpunit": "^10"
|
||||
},
|
||||
"type": "library",
|
||||
@@ -104,10 +104,10 @@
|
||||
"support": {
|
||||
"email": "team@appwrite.io",
|
||||
"issues": "https://github.com/appwrite/sdk-for-php/issues",
|
||||
"source": "https://github.com/appwrite/sdk-for-php/tree/15.1.0",
|
||||
"source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0",
|
||||
"url": "https://appwrite.io/support"
|
||||
},
|
||||
"time": "2025-08-01T04:50:51+00:00"
|
||||
"time": "2025-12-18T08:07:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "appwrite/php-clamav",
|
||||
@@ -756,16 +756,16 @@
|
||||
},
|
||||
{
|
||||
"name": "google/protobuf",
|
||||
"version": "v4.33.2",
|
||||
"version": "v4.33.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/protocolbuffers/protobuf-php.git",
|
||||
"reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318"
|
||||
"reference": "22d28025cda0d223a2e48c2e16c5284ecc9f5402"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318",
|
||||
"reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318",
|
||||
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/22d28025cda0d223a2e48c2e16c5284ecc9f5402",
|
||||
"reference": "22d28025cda0d223a2e48c2e16c5284ecc9f5402",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -794,9 +794,9 @@
|
||||
"proto"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.2"
|
||||
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.4"
|
||||
},
|
||||
"time": "2025-12-05T22:12:22+00:00"
|
||||
"time": "2026-01-12T17:58:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/csv",
|
||||
@@ -1365,16 +1365,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/exporter-otlp",
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/exporter-otlp.git",
|
||||
"reference": "07b02bc71838463f6edcc78d3485c04b48fb263d"
|
||||
"reference": "62e680d587beb42e5247aa6ecd89ad1ca406e8ca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/07b02bc71838463f6edcc78d3485c04b48fb263d",
|
||||
"reference": "07b02bc71838463f6edcc78d3485c04b48fb263d",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/62e680d587beb42e5247aa6ecd89ad1ca406e8ca",
|
||||
"reference": "62e680d587beb42e5247aa6ecd89ad1ca406e8ca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1425,7 +1425,7 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-11-13T08:04:37+00:00"
|
||||
"time": "2026-01-15T09:31:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/gen-otlp-protobuf",
|
||||
@@ -1492,16 +1492,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sdk",
|
||||
"version": "1.10.0",
|
||||
"version": "1.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/sdk.git",
|
||||
"reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99"
|
||||
"reference": "d91f21addcdb42da9a451c002777f8318432461a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99",
|
||||
"reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/d91f21addcdb42da9a451c002777f8318432461a",
|
||||
"reference": "d91f21addcdb42da9a451c002777f8318432461a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1585,7 +1585,7 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-11-25T10:59:15+00:00"
|
||||
"time": "2026-01-15T11:21:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sem-conv",
|
||||
@@ -3455,24 +3455,25 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/abuse",
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/abuse.git",
|
||||
"reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828"
|
||||
"reference": "15cd5dbefa4453e8a2d90649a7078e242966ac3f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/611fa66a97e87c0dbbc133a717d970da7a5ca828",
|
||||
"reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/15cd5dbefa4453e8a2d90649a7078e242966ac3f",
|
||||
"reference": "15cd5dbefa4453e8a2d90649a7078e242966ac3f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"appwrite/appwrite": "19.*",
|
||||
"ext-curl": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-redis": "*",
|
||||
"php": ">=8.0",
|
||||
"utopia-php/database": "*"
|
||||
"utopia-php/database": "4.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.*",
|
||||
@@ -3500,9 +3501,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/abuse/issues",
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.0.2"
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.2.1"
|
||||
},
|
||||
"time": "2025-10-20T07:18:33+00:00"
|
||||
"time": "2026-01-15T02:09:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/analytics",
|
||||
@@ -3552,23 +3553,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/audit",
|
||||
"version": "2.0.2-rc1",
|
||||
"version": "2.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/audit.git",
|
||||
"reference": "7b35dab40bce66bda56eeeacd2bbcbf1e823f05f"
|
||||
"reference": "1301ab2607667b9f86456f86895f3e26f8c0c9a7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/audit/zipball/7b35dab40bce66bda56eeeacd2bbcbf1e823f05f",
|
||||
"reference": "7b35dab40bce66bda56eeeacd2bbcbf1e823f05f",
|
||||
"url": "https://api.github.com/repos/utopia-php/audit/zipball/1301ab2607667b9f86456f86895f3e26f8c0c9a7",
|
||||
"reference": "1301ab2607667b9f86456f86895f3e26f8c0c9a7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"utopia-php/database": "3.*",
|
||||
"utopia-php/fetch": "^0.4.2",
|
||||
"utopia-php/validators": "^0.1.0"
|
||||
"utopia-php/database": "4.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/validators": "0.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.*",
|
||||
@@ -3595,9 +3596,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/audit/issues",
|
||||
"source": "https://github.com/utopia-php/audit/tree/2.0.2-rc1"
|
||||
"source": "https://github.com/utopia-php/audit/tree/2.0.4"
|
||||
},
|
||||
"time": "2025-12-24T01:20:43+00:00"
|
||||
"time": "2026-01-14T07:22:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/auth",
|
||||
@@ -3898,16 +3899,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "3.6.1",
|
||||
"version": "4.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "c8c1b2f5770245dd4006e2680681e3efbe8b1fa7"
|
||||
"reference": "8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/c8c1b2f5770245dd4006e2680681e3efbe8b1fa7",
|
||||
"reference": "c8c1b2f5770245dd4006e2680681e3efbe8b1fa7",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23",
|
||||
"reference": "8e6a033d4da09a2f2ac1f79fd85fcfa2da018d23",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3950,9 +3951,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/3.6.1"
|
||||
"source": "https://github.com/utopia-php/database/tree/4.5.2"
|
||||
},
|
||||
"time": "2025-12-16T09:55:41+00:00"
|
||||
"time": "2026-01-15T04:23:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4001,22 +4002,22 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dns",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/dns.git",
|
||||
"reference": "5daf8b683dad877491c4df84c6be24850b2f363b"
|
||||
"reference": "a1f490ba425b1a5128e7aaa24eff560900812d21"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/5daf8b683dad877491c4df84c6be24850b2f363b",
|
||||
"reference": "5daf8b683dad877491c4df84c6be24850b2f363b",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/a1f490ba425b1a5128e7aaa24eff560900812d21",
|
||||
"reference": "a1f490ba425b1a5128e7aaa24eff560900812d21",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"utopia-php/console": "0.0.*",
|
||||
"utopia-php/domains": "0.9.*",
|
||||
"utopia-php/domains": "0.11.*",
|
||||
"utopia-php/span": "1.0.*",
|
||||
"utopia-php/telemetry": "*",
|
||||
"utopia-php/validators": "0.*"
|
||||
},
|
||||
@@ -4052,22 +4053,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/dns/issues",
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.4.1"
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.5.3"
|
||||
},
|
||||
"time": "2025-12-17T09:09:08+00:00"
|
||||
"time": "2026-01-13T11:39:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
"version": "0.9.2",
|
||||
"version": "0.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/domains.git",
|
||||
"reference": "52b654f8a0e170bfa2e54cb47755b256822477c7"
|
||||
"reference": "f333e23e721ca5cd3bd21063fa88304114b0467d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/52b654f8a0e170bfa2e54cb47755b256822477c7",
|
||||
"reference": "52b654f8a0e170bfa2e54cb47755b256822477c7",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/f333e23e721ca5cd3bd21063fa88304114b0467d",
|
||||
"reference": "f333e23e721ca5cd3bd21063fa88304114b0467d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4114,9 +4115,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/domains/issues",
|
||||
"source": "https://github.com/utopia-php/domains/tree/0.9.2"
|
||||
"source": "https://github.com/utopia-php/domains/tree/0.11.0"
|
||||
},
|
||||
"time": "2025-11-26T12:16:36+00:00"
|
||||
"time": "2026-01-13T09:40:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dsn",
|
||||
@@ -4167,23 +4168,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/emails",
|
||||
"version": "0.6.3",
|
||||
"version": "0.6.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/emails.git",
|
||||
"reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918"
|
||||
"reference": "178e57a0f9a24139500c94ce73d166800197b6cf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/emails/zipball/9524d7f7bd1651a06fef8a3d964f774b04fe2918",
|
||||
"reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918",
|
||||
"url": "https://api.github.com/repos/utopia-php/emails/zipball/178e57a0f9a24139500c94ce73d166800197b6cf",
|
||||
"reference": "178e57a0f9a24139500c94ce73d166800197b6cf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"utopia-php/cli": "^0.15",
|
||||
"utopia-php/domains": "^0.9",
|
||||
"utopia-php/fetch": "^0.4",
|
||||
"utopia-php/domains": "^0.11",
|
||||
"utopia-php/fetch": "^0.5",
|
||||
"utopia-php/validators": "0.*"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -4221,26 +4222,26 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/emails/issues",
|
||||
"source": "https://github.com/utopia-php/emails/tree/0.6.3"
|
||||
"source": "https://github.com/utopia-php/emails/tree/0.6.5"
|
||||
},
|
||||
"time": "2025-11-26T12:27:47+00:00"
|
||||
"time": "2026-01-13T09:55:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/fetch",
|
||||
"version": "0.4.2",
|
||||
"version": "0.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/fetch.git",
|
||||
"reference": "83986d1be75a2fae4e684107fe70dd78a8e19b77"
|
||||
"reference": "a96a010e1c273f3888765449687baf58cbc61fcd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/83986d1be75a2fae4e684107fe70dd78a8e19b77",
|
||||
"reference": "83986d1be75a2fae4e684107fe70dd78a8e19b77",
|
||||
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/a96a010e1c273f3888765449687baf58cbc61fcd",
|
||||
"reference": "a96a010e1c273f3888765449687baf58cbc61fcd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0"
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.5.0",
|
||||
@@ -4260,29 +4261,29 @@
|
||||
"description": "A simple library that provides an interface for making HTTP Requests.",
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/fetch/issues",
|
||||
"source": "https://github.com/utopia-php/fetch/tree/0.4.2"
|
||||
"source": "https://github.com/utopia-php/fetch/tree/0.5.1"
|
||||
},
|
||||
"time": "2025-04-25T13:48:02+00:00"
|
||||
"time": "2025-12-18T16:25:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/framework",
|
||||
"version": "0.33.35",
|
||||
"version": "0.33.37",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "82b139fb04f30045db51b0d322224f222da32313"
|
||||
"reference": "30a119d76531d89da9240496940c84fcd9e1758b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/82b139fb04f30045db51b0d322224f222da32313",
|
||||
"reference": "82b139fb04f30045db51b0d322224f222da32313",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/30a119d76531d89da9240496940c84fcd9e1758b",
|
||||
"reference": "30a119d76531d89da9240496940c84fcd9e1758b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"utopia-php/compression": "0.1.*",
|
||||
"utopia-php/telemetry": "0.1.*",
|
||||
"utopia-php/validators": "0.1.*"
|
||||
"utopia-php/validators": "0.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.*",
|
||||
@@ -4308,9 +4309,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.33.35"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.33.37"
|
||||
},
|
||||
"time": "2025-12-12T08:33:52+00:00"
|
||||
"time": "2026-01-13T10:10:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
@@ -4515,25 +4516,25 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.3.9",
|
||||
"version": "1.4.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "c55ec67c74663190cda10fd79297422147be7e85"
|
||||
"reference": "3fe751902012d09d323420cd3523be1ed855e868"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c55ec67c74663190cda10fd79297422147be7e85",
|
||||
"reference": "c55ec67c74663190cda10fd79297422147be7e85",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/3fe751902012d09d323420cd3523be1ed855e868",
|
||||
"reference": "3fe751902012d09d323420cd3523be1ed855e868",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"appwrite/appwrite": "15.*",
|
||||
"appwrite/appwrite": "19.*",
|
||||
"ext-curl": "*",
|
||||
"ext-openssl": "*",
|
||||
"php": ">=8.1",
|
||||
"utopia-php/console": "0.0.*",
|
||||
"utopia-php/database": "3.*",
|
||||
"utopia-php/database": "4.*",
|
||||
"utopia-php/dsn": "0.2.*",
|
||||
"utopia-php/storage": "0.18.*"
|
||||
},
|
||||
@@ -4564,9 +4565,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.3.9"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.4.4"
|
||||
},
|
||||
"time": "2025-12-08T08:45:09+00:00"
|
||||
"time": "2026-01-16T10:00:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -4681,16 +4682,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/platform",
|
||||
"version": "0.7.13",
|
||||
"version": "0.7.14",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/platform.git",
|
||||
"reference": "77a863a920122e2c6a6bc6ee5548d366a3f4c6c7"
|
||||
"reference": "9f18ce63f1425ae2dae57468200e4a5d1239d57b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/77a863a920122e2c6a6bc6ee5548d366a3f4c6c7",
|
||||
"reference": "77a863a920122e2c6a6bc6ee5548d366a3f4c6c7",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/9f18ce63f1425ae2dae57468200e4a5d1239d57b",
|
||||
"reference": "9f18ce63f1425ae2dae57468200e4a5d1239d57b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4699,7 +4700,7 @@
|
||||
"php": ">=8.0",
|
||||
"utopia-php/cli": "0.15.*",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/queue": "0.11.*"
|
||||
"utopia-php/queue": "0.15.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.*",
|
||||
@@ -4726,9 +4727,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/platform/issues",
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.7.13"
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.7.14"
|
||||
},
|
||||
"time": "2025-12-08T10:02:40+00:00"
|
||||
"time": "2026-01-06T15:39:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/pools",
|
||||
@@ -4837,23 +4838,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/queue",
|
||||
"version": "0.11.2",
|
||||
"version": "0.15.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/queue.git",
|
||||
"reference": "a854f7c4abc18e0eca55fc5608cd7088d71eb19f"
|
||||
"reference": "6abb268ba7ec00dea4e5201b007776ea1bce9242"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/queue/zipball/a854f7c4abc18e0eca55fc5608cd7088d71eb19f",
|
||||
"reference": "a854f7c4abc18e0eca55fc5608cd7088d71eb19f",
|
||||
"url": "https://api.github.com/repos/utopia-php/queue/zipball/6abb268ba7ec00dea4e5201b007776ea1bce9242",
|
||||
"reference": "6abb268ba7ec00dea4e5201b007776ea1bce9242",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"php-amqplib/php-amqplib": "^3.7",
|
||||
"utopia-php/cli": "0.15.*",
|
||||
"utopia-php/fetch": "0.4.*",
|
||||
"utopia-php/console": "0.0.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/pools": "0.8.*",
|
||||
"utopia-php/telemetry": "*"
|
||||
@@ -4897,9 +4898,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/queue/issues",
|
||||
"source": "https://github.com/utopia-php/queue/tree/0.11.2"
|
||||
"source": "https://github.com/utopia-php/queue/tree/0.15.0"
|
||||
},
|
||||
"time": "2025-12-17T09:32:35+00:00"
|
||||
"time": "2026-01-06T12:41:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/registry",
|
||||
@@ -4953,6 +4954,49 @@
|
||||
},
|
||||
"time": "2021-03-10T10:45:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/span",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/span.git",
|
||||
"reference": "f2f6c499ded3a776e8019902e83d140ff0f89693"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/span/zipball/f2f6c499ded3a776e8019902e83d140ff0f89693",
|
||||
"reference": "f2f6c499ded3a776e8019902e83d140ff0f89693",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.0",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"swoole/ide-helper": "^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-swoole": "Required for coroutine-based storage"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Utopia\\Span\\": "src/Span/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Simple span tracing library for PHP with coroutine support",
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/span/issues",
|
||||
"source": "https://github.com/utopia-php/span/tree/1.0.0"
|
||||
},
|
||||
"time": "2026-01-12T20:05:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/storage",
|
||||
"version": "0.18.18",
|
||||
@@ -5013,22 +5057,22 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/swoole",
|
||||
"version": "0.8.5",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/swoole.git",
|
||||
"reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58"
|
||||
"reference": "95a937acb393dbf95cccba239d55886e2848ab0b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/e42b6b8e44c457a7b35d8a857d7af1d67d667c58",
|
||||
"reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58",
|
||||
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/95a937acb393dbf95cccba239d55886e2848ab0b",
|
||||
"reference": "95a937acb393dbf95cccba239d55886e2848ab0b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-swoole": "*",
|
||||
"php": ">=8.0",
|
||||
"utopia-php/framework": "0.33.35"
|
||||
"utopia-php/framework": "0.33.37"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
@@ -5058,9 +5102,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/swoole/issues",
|
||||
"source": "https://github.com/utopia-php/swoole/tree/0.8.5"
|
||||
"source": "https://github.com/utopia-php/swoole/tree/1.0.0"
|
||||
},
|
||||
"time": "2025-12-15T14:03:23+00:00"
|
||||
"time": "2026-01-14T14:00:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/system",
|
||||
@@ -5170,16 +5214,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/validators",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/validators.git",
|
||||
"reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080"
|
||||
"reference": "30b6030a5b100fc1dff34506e5053759594b2a20"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/validators/zipball/5c57d5b6cf964f8981807c1d3ea8df620c869080",
|
||||
"reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080",
|
||||
"url": "https://api.github.com/repos/utopia-php/validators/zipball/30b6030a5b100fc1dff34506e5053759594b2a20",
|
||||
"reference": "30b6030a5b100fc1dff34506e5053759594b2a20",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5187,7 +5231,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.*",
|
||||
"phpstan/phpstan": "1.*",
|
||||
"phpstan/phpstan": "2.*",
|
||||
"phpunit/phpunit": "11.*"
|
||||
},
|
||||
"type": "library",
|
||||
@@ -5209,9 +5253,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/validators/issues",
|
||||
"source": "https://github.com/utopia-php/validators/tree/0.1.0"
|
||||
"source": "https://github.com/utopia-php/validators/tree/0.2.0"
|
||||
},
|
||||
"time": "2025-11-18T11:05:46+00:00"
|
||||
"time": "2026-01-13T09:16:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
@@ -5371,38 +5415,47 @@
|
||||
},
|
||||
{
|
||||
"name": "webonyx/graphql-php",
|
||||
"version": "v14.11.10",
|
||||
"version": "v15.24.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/webonyx/graphql-php.git",
|
||||
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19"
|
||||
"reference": "030a04d22d52d7fc07049d0e3b683d2b40f90457"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9c2fdebc6aa01d831bc2969da00e8588cffef19",
|
||||
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19",
|
||||
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/030a04d22d52d7fc07049d0e3b683d2b40f90457",
|
||||
"reference": "030a04d22d52d7fc07049d0e3b683d2b40f90457",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8"
|
||||
"php": "^7.4 || ^8"
|
||||
},
|
||||
"require-dev": {
|
||||
"amphp/amp": "^2.3",
|
||||
"doctrine/coding-standard": "^6.0",
|
||||
"nyholm/psr7": "^1.2",
|
||||
"amphp/amp": "^2.6",
|
||||
"amphp/http-server": "^2.1",
|
||||
"dms/phpunit-arraysubset-asserts": "dev-master",
|
||||
"ergebnis/composer-normalize": "^2.28",
|
||||
"friendsofphp/php-cs-fixer": "3.86.0",
|
||||
"mll-lab/php-cs-fixer-config": "5.11.0",
|
||||
"nyholm/psr7": "^1.5",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "0.12.82",
|
||||
"phpstan/phpstan-phpunit": "0.12.18",
|
||||
"phpstan/phpstan-strict-rules": "0.12.9",
|
||||
"phpunit/phpunit": "^7.2 || ^8.5",
|
||||
"psr/http-message": "^1.0",
|
||||
"react/promise": "2.*",
|
||||
"simpod/php-coveralls-mirror": "^3.0"
|
||||
"phpstan/extension-installer": "^1.1",
|
||||
"phpstan/phpstan": "2.1.22",
|
||||
"phpstan/phpstan-phpunit": "2.0.7",
|
||||
"phpstan/phpstan-strict-rules": "2.0.6",
|
||||
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
|
||||
"psr/http-message": "^1 || ^2",
|
||||
"react/http": "^1.6",
|
||||
"react/promise": "^2.0 || ^3.0",
|
||||
"rector/rector": "^2.0",
|
||||
"symfony/polyfill-php81": "^1.23",
|
||||
"symfony/var-exporter": "^5 || ^6 || ^7",
|
||||
"thecodingmachine/safe": "^1.3 || ^2 || ^3"
|
||||
},
|
||||
"suggest": {
|
||||
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
|
||||
"psr/http-message": "To use standard GraphQL server",
|
||||
"react/promise": "To leverage async resolving on React PHP platform"
|
||||
},
|
||||
@@ -5424,7 +5477,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/webonyx/graphql-php/issues",
|
||||
"source": "https://github.com/webonyx/graphql-php/tree/v14.11.10"
|
||||
"source": "https://github.com/webonyx/graphql-php/tree/v15.24.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5432,22 +5485,22 @@
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-05T14:23:37+00:00"
|
||||
"time": "2025-08-20T10:09:37+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.8.6",
|
||||
"version": "1.8.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "b6cc29d3bd247e193f3c06b4168dc69d884645f0"
|
||||
"reference": "1bc5a39bf87d3c2064f2f8d45fa712340338bc41"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b6cc29d3bd247e193f3c06b4168dc69d884645f0",
|
||||
"reference": "b6cc29d3bd247e193f3c06b4168dc69d884645f0",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/1bc5a39bf87d3c2064f2f8d45fa712340338bc41",
|
||||
"reference": "1bc5a39bf87d3c2064f2f8d45fa712340338bc41",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5483,9 +5536,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.6"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.17"
|
||||
},
|
||||
"time": "2025-12-31T10:22:17+00:00"
|
||||
"time": "2026-01-19T12:13:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/annotations",
|
||||
@@ -5566,30 +5619,29 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/instantiator",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/instantiator.git",
|
||||
"reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
|
||||
"reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
|
||||
"reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
|
||||
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7",
|
||||
"reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
"php": "^8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^11",
|
||||
"doctrine/coding-standard": "^14",
|
||||
"ext-pdo": "*",
|
||||
"ext-phar": "*",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/phpstan": "^1.9.4",
|
||||
"phpstan/phpstan-phpunit": "^1.3",
|
||||
"phpunit/phpunit": "^9.5.27",
|
||||
"vimeo/psalm": "^5.4"
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^10.5.58"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -5616,7 +5668,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/instantiator/issues",
|
||||
"source": "https://github.com/doctrine/instantiator/tree/2.0.0"
|
||||
"source": "https://github.com/doctrine/instantiator/tree/2.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5632,7 +5684,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-12-30T00:23:10+00:00"
|
||||
"time": "2026-01-05T06:47:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/lexer",
|
||||
@@ -5713,16 +5765,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.26.0",
|
||||
"version": "v1.27.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pint.git",
|
||||
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
|
||||
"reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
|
||||
"reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5733,9 +5785,9 @@
|
||||
"php": "^8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.90.0",
|
||||
"illuminate/view": "^12.40.1",
|
||||
"larastan/larastan": "^3.8.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.92.4",
|
||||
"illuminate/view": "^12.44.0",
|
||||
"larastan/larastan": "^3.8.1",
|
||||
"laravel-zero/framework": "^12.0.4",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/termwind": "^2.3.3",
|
||||
@@ -5776,7 +5828,7 @@
|
||||
"issues": "https://github.com/laravel/pint/issues",
|
||||
"source": "https://github.com/laravel/pint"
|
||||
},
|
||||
"time": "2025-11-25T21:15:52+00:00"
|
||||
"time": "2026-01-05T16:49:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "matthiasmullie/minify",
|
||||
@@ -8562,16 +8614,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v8.0.0",
|
||||
"version": "v8.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149"
|
||||
"reference": "0cbbd88ec836f8757641c651bb995335846abb78"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/a0a750500c4ce900d69ba4e9faf16f82c10ee149",
|
||||
"reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/0cbbd88ec836f8757641c651bb995335846abb78",
|
||||
"reference": "0cbbd88ec836f8757641c651bb995335846abb78",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8603,7 +8655,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.0"
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8623,7 +8675,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-16T16:25:44+00:00"
|
||||
"time": "2025-12-19T10:01:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
@@ -8945,9 +8997,7 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {
|
||||
"utopia-php/audit": 5
|
||||
},
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
|
||||
+63
-3
@@ -200,9 +200,11 @@ services:
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES
|
||||
- _APP_SMS_PROVIDER
|
||||
- _APP_SMS_FROM
|
||||
- _APP_GRAPHQL_INTROSPECTION
|
||||
- _APP_GRAPHQL_MAX_BATCH_SIZE
|
||||
- _APP_GRAPHQL_MAX_COMPLEXITY
|
||||
- _APP_GRAPHQL_MAX_DEPTH
|
||||
- _APP_GRAPHQL_SCHEMA_CACHE_MB
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
- _APP_VCS_GITHUB_APP_ID
|
||||
@@ -230,7 +232,7 @@ services:
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: appwrite/console:7.4.11
|
||||
image: appwrite/console:7.5.7
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -466,14 +468,12 @@ services:
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
- appwrite-sites:/storage/sites:rw
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_BROWSER_HOST
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
@@ -529,6 +529,65 @@ services:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
appwrite-worker-screenshots:
|
||||
entrypoint: worker-screenshots
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-screenshots
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
# Specific
|
||||
- _APP_BROWSER_HOST
|
||||
# Basic
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_LOGGING_CONFIG
|
||||
# Database
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
# Storage
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_S3_ENDPOINT
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- _APP_STORAGE_BACKBLAZE_SECRET
|
||||
- _APP_STORAGE_BACKBLAZE_REGION
|
||||
- _APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- _APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- _APP_STORAGE_LINODE_SECRET
|
||||
- _APP_STORAGE_LINODE_REGION
|
||||
- _APP_STORAGE_LINODE_BUCKET
|
||||
- _APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_WASABI_BUCKET
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
appwrite-worker-certificates:
|
||||
entrypoint: worker-certificates
|
||||
<<: *x-logging
|
||||
@@ -823,6 +882,7 @@ services:
|
||||
- _APP_DB_PASS
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
- _APP_INTERVAL_DOMAIN_VERIFICATION
|
||||
- _APP_INTERVAL_CLEANUP_STALE_EXECUTIONS
|
||||
|
||||
appwrite-task-stats-resources:
|
||||
container_name: appwrite-task-stats-resources
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
appwrite projects update-labels \
|
||||
--project-id <PROJECT_ID> \
|
||||
--labels one two three
|
||||
@@ -1,5 +1,39 @@
|
||||
# Change Log
|
||||
|
||||
## 13.0.1
|
||||
|
||||
- Fix `project init` command leading to Cannot convert to BigInt error
|
||||
- Fix filter out unwanted attributes being pulled in the config file
|
||||
|
||||
## 13.0.0
|
||||
|
||||
- Mark release as stable
|
||||
- Feat: add pull sync on destruction of remote resources (+ confirmation)
|
||||
- Fix: refine zod schema to check string size
|
||||
- Validate using zod schema during push cli command
|
||||
- Maintain order of keys in local config
|
||||
|
||||
## 13.0.0-rc.5
|
||||
|
||||
- Fix push all command not working correctly
|
||||
|
||||
## 13.0.0-rc.4
|
||||
|
||||
- Fix CLI ES module import issues
|
||||
|
||||
## 13.0.0-rc.3
|
||||
|
||||
- Add `Schema` class for programmatically pushing and pulling appwrite config
|
||||
- Add client side db generation using `schema.db.generate()` command
|
||||
|
||||
## 13.0.0-rc.2
|
||||
|
||||
- Fixes a lot of typescript errors throughout the codebase
|
||||
|
||||
## 13.0.0-rc.1
|
||||
|
||||
- Migrates codebase from JavaScript to TypeScript
|
||||
|
||||
## 12.0.1
|
||||
|
||||
Fix type generation for `point`, `lineString` and `polygon` columns
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
## 20.1.1
|
||||
|
||||
* Fix boolean parameter not handled correctly in Client requests
|
||||
|
||||
## 20.1.0
|
||||
|
||||
* Added ability to create columns and indexes synchronously while creating a table
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
## 20.3.3
|
||||
|
||||
* Fix boolean parameter not handled correctly in Client requests
|
||||
|
||||
## 20.3.2
|
||||
|
||||
* Fix OAuth2 browser infinite redirect issue
|
||||
|
||||
@@ -20,10 +20,12 @@ use Utopia\Database\Validator\Authorization;
|
||||
class TransactionState
|
||||
{
|
||||
private Database $dbForProject;
|
||||
|
||||
public function __construct(Database $dbForProject)
|
||||
private Authorization $authorization;
|
||||
/** @var Authorization $authorization */
|
||||
public function __construct(Database $dbForProject, Authorization $authorization)
|
||||
{
|
||||
$this->dbForProject = $dbForProject;
|
||||
$this->authorization = $authorization;
|
||||
}
|
||||
|
||||
|
||||
@@ -342,12 +344,12 @@ class TransactionState
|
||||
*/
|
||||
private function getTransactionState(string $transactionId): array
|
||||
{
|
||||
$transaction = Authorization::skip(fn () => $this->dbForProject->getDocument('transactions', $transactionId));
|
||||
$transaction = $this->authorization->skip(fn () => $this->dbForProject->getDocument('transactions', $transactionId));
|
||||
if ($transaction->isEmpty() || $transaction->getAttribute('status') !== 'pending') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$operations = Authorization::skip(fn () => $this->dbForProject->find('transactionLogs', [
|
||||
$operations = $this->authorization->skip(fn () => $this->dbForProject->find('transactionLogs', [
|
||||
Query::equal('transactionInternalId', [$transaction->getSequence()]),
|
||||
Query::orderAsc(),
|
||||
Query::limit(PHP_INT_MAX)
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
namespace Appwrite\Deletes;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Limit as LimitException;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class Targets
|
||||
@@ -42,12 +44,17 @@ class Targets
|
||||
MESSAGE_TYPE_PUSH => 'pushTotal',
|
||||
default => throw new Exception('Invalid target provider type'),
|
||||
};
|
||||
$database->decreaseDocumentAttribute(
|
||||
'topics',
|
||||
$topicId,
|
||||
$totalAttribute,
|
||||
min: 0
|
||||
);
|
||||
|
||||
try {
|
||||
$database->decreaseDocumentAttribute(
|
||||
'topics',
|
||||
$topicId,
|
||||
$totalAttribute,
|
||||
min: 0
|
||||
);
|
||||
} catch (LimitException $e) {
|
||||
Console::error("Delete subscribers decreaseDocumentAttribute (topicId={$topicId}): {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -39,6 +39,9 @@ class Event
|
||||
public const BUILDS_QUEUE_NAME = 'v1-builds';
|
||||
public const BUILDS_CLASS_NAME = 'BuildsV1';
|
||||
|
||||
public const SCREENSHOTS_QUEUE_NAME = 'v1-screenshots';
|
||||
public const SCREENSHOTS_CLASS_NAME = 'ScreenshotsV1';
|
||||
|
||||
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
|
||||
public const MESSAGING_CLASS_NAME = 'MessagingV1';
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Screenshot extends Event
|
||||
{
|
||||
protected string $deploymentId = '';
|
||||
|
||||
public function __construct(protected Publisher $publisher)
|
||||
{
|
||||
parent::__construct($publisher);
|
||||
|
||||
$this
|
||||
->setQueue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
|
||||
->setClass(System::getEnv('_APP_SCREENSHOTS_CLASS_NAME', Event::SCREENSHOTS_CLASS_NAME));
|
||||
}
|
||||
|
||||
public function setDeploymentId(string $deploymentId): self
|
||||
{
|
||||
$this->deploymentId = $deploymentId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function preparePayload(): array
|
||||
{
|
||||
$platform = $this->platform;
|
||||
if (empty($platform)) {
|
||||
$platform = Config::getParam('platform', []);
|
||||
}
|
||||
|
||||
return [
|
||||
'project' => $this->project,
|
||||
'deploymentId' => $this->deploymentId,
|
||||
'platform' => $platform,
|
||||
];
|
||||
}
|
||||
|
||||
public function reset(): self
|
||||
{
|
||||
$this->deploymentId = '';
|
||||
parent::reset();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Functions;
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class EventProcessor
|
||||
{
|
||||
/**
|
||||
* Get function events for a project, using Redis cache
|
||||
* @param Document|null $project
|
||||
* @param Database $dbForProject
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public function getFunctionsEvents(?Document $project, Database $dbForProject): array
|
||||
{
|
||||
if ($project === null ||
|
||||
$project->isEmpty() ||
|
||||
$project->getId() === 'console') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$hostname = $dbForProject->getAdapter()->getHostname();
|
||||
$cacheKey = \sprintf(
|
||||
'%s-cache-%s:%s:%s:project:%s:functions:events',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$project->getId()
|
||||
);
|
||||
|
||||
$ttl = 3600; // 1 hour cache TTL
|
||||
$cachedFunctionEvents = $dbForProject->getCache()->load($cacheKey, $ttl);
|
||||
|
||||
if ($cachedFunctionEvents !== false) {
|
||||
return \json_decode($cachedFunctionEvents, true) ?? [];
|
||||
}
|
||||
|
||||
try {
|
||||
$events = [];
|
||||
$limit = 100;
|
||||
$sum = 100;
|
||||
$offset = 0;
|
||||
|
||||
while ($sum >= $limit) {
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::select(['$id', 'events']),
|
||||
Query::limit($limit),
|
||||
Query::offset($offset),
|
||||
Query::orderAsc('$sequence'),
|
||||
]);
|
||||
|
||||
$sum = \count($functions);
|
||||
$offset = $offset + $limit;
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$functionEvents = $function->getAttribute('events', []);
|
||||
if (!empty($functionEvents)) {
|
||||
$events = array_merge($events, $functionEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$uniqueEvents = \array_flip(\array_unique($events));
|
||||
$dbForProject->getCache()->save($cacheKey, \json_encode($uniqueEvents));
|
||||
|
||||
return $uniqueEvents;
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webhook events for a project from the project's webhooks attribute
|
||||
* @param Document|null $project
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public function getWebhooksEvents(?Document $project): array
|
||||
{
|
||||
if ($project === null || $project->isEmpty() || $project->getId() === 'console') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$webhooks = $project->getAttribute('webhooks', []);
|
||||
if (empty($webhooks)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$events = [];
|
||||
foreach ($webhooks as $webhook) {
|
||||
if ($webhook->getAttribute('enabled', false) !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$webhookEvents = $webhook->getAttribute('events', []);
|
||||
if (!empty($webhookEvents)) {
|
||||
$events = array_merge($events, $webhookEvents);
|
||||
}
|
||||
}
|
||||
|
||||
return \array_flip(\array_unique($events));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use GraphQL\Type\Schema as GQLSchema;
|
||||
use Swoole\Lock;
|
||||
use Swoole\Table;
|
||||
|
||||
/**
|
||||
* LRU Cache for GraphQL Schemas keyed by project ID.
|
||||
*
|
||||
* Uses a combination of array storage and access tracking to implement
|
||||
* least-recently-used eviction when the cache reaches memory capacity.
|
||||
*
|
||||
* This class is designed to be instantiated once per Swoole worker and
|
||||
* registered for reuse across requests. Thread-safe via Swoole mutex locks.
|
||||
*
|
||||
* Dirty flags are stored in a shared Swoole Table to propagate cache
|
||||
* invalidation across all workers.
|
||||
*/
|
||||
class Cache
|
||||
{
|
||||
/**
|
||||
* @var array<string, GQLSchema> Cache storage: projectId => schema
|
||||
*/
|
||||
private array $cache = [];
|
||||
|
||||
/**
|
||||
* @var array<string, int> Access timestamps: projectId => nanoseconds (hrtime)
|
||||
*/
|
||||
private array $accessTimes = [];
|
||||
|
||||
/**
|
||||
* @var array<string, int> Memory usage per schema: projectId => bytes
|
||||
*/
|
||||
private array $memorySizes = [];
|
||||
|
||||
/**
|
||||
* @var int Maximum cache size in bytes
|
||||
*/
|
||||
private int $maxBytes;
|
||||
|
||||
/**
|
||||
* @var int Current total memory usage in bytes
|
||||
*/
|
||||
private int $currentBytes = 0;
|
||||
|
||||
/**
|
||||
* @var Table|null Shared Swoole Table for dirty flags (shared across workers)
|
||||
*/
|
||||
private ?Table $dirty;
|
||||
|
||||
/**
|
||||
* @var array<string, int> Local dirty flags (used when no shared table available)
|
||||
*/
|
||||
private array $local = [];
|
||||
|
||||
/**
|
||||
* @var Lock Swoole mutex lock for thread safety within this worker
|
||||
*/
|
||||
private Lock $lock;
|
||||
|
||||
/**
|
||||
* Heuristic constants for memory estimation.
|
||||
* These are approximations - actual memory usage varies based on resolver closures,
|
||||
* description lengths, and type complexity. The values are tuned for relative
|
||||
* comparison between schemas rather than absolute accuracy.
|
||||
*/
|
||||
private const int BYTES_PER_TYPE = 2048; // ~2KB base per type
|
||||
private const int BYTES_PER_FIELD = 768; // ~768 bytes per field (includes resolver overhead estimate)
|
||||
|
||||
/**
|
||||
* Create a new cache instance.
|
||||
*
|
||||
* @param int $maxMB Maximum cache size in megabytes (default: 50)
|
||||
* @param Table|null $dirty Shared Swoole Table for cross-worker dirty flag propagation
|
||||
*/
|
||||
public function __construct(int $maxMB = 50, ?Table $dirty = null)
|
||||
{
|
||||
$this->maxBytes = \max(1, $maxMB) * 1024 * 1024;
|
||||
$this->dirty = $dirty;
|
||||
$this->lock = new Lock(SWOOLE_MUTEX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the maximum cache size in megabytes.
|
||||
*
|
||||
* @param int $megabytes Maximum cache size in MB (minimum 1 MB)
|
||||
*/
|
||||
public function setMaxSizeMB(int $megabytes): void
|
||||
{
|
||||
$bytes = \max(1, $megabytes) * 1024 * 1024;
|
||||
if ($this->maxBytes === $bytes) {
|
||||
return;
|
||||
}
|
||||
$this->maxBytes = $bytes;
|
||||
$this->evictIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current max size in megabytes.
|
||||
*/
|
||||
public function getMaxSizeMB(): int
|
||||
{
|
||||
return (int) ($this->maxBytes / 1024 / 1024);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current memory usage in bytes.
|
||||
*/
|
||||
public function getCurrentBytes(): int
|
||||
{
|
||||
return $this->currentBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a schema from cache if it exists and is not dirty.
|
||||
* Updates access time on hit.
|
||||
*/
|
||||
public function get(string $projectId): ?GQLSchema
|
||||
{
|
||||
$this->lock->lock();
|
||||
try {
|
||||
if ($this->isDirty($projectId)) {
|
||||
$this->clearDirty($projectId);
|
||||
if (isset($this->cache[$projectId])) {
|
||||
$this->removeInternal($projectId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($this->cache[$projectId])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->accessTimes[$projectId] = \hrtime(true);
|
||||
|
||||
return $this->cache[$projectId];
|
||||
} finally {
|
||||
$this->lock->unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a schema in the cache.
|
||||
* Evicts least recently used entries if memory limit would be exceeded.
|
||||
*/
|
||||
public function set(string $projectId, GQLSchema $schema): void
|
||||
{
|
||||
$this->lock->lock();
|
||||
try {
|
||||
$this->clearDirty($projectId);
|
||||
|
||||
$schemaSize = $this->calculateSchemaSize($schema);
|
||||
|
||||
// Reject schemas larger than max cache size
|
||||
if ($schemaSize > $this->maxBytes) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update existing entry
|
||||
if (isset($this->cache[$projectId])) {
|
||||
$oldSize = $this->memorySizes[$projectId] ?? 0;
|
||||
$this->currentBytes = \max(0, $this->currentBytes - $oldSize) + $schemaSize;
|
||||
|
||||
$this->cache[$projectId] = $schema;
|
||||
$this->memorySizes[$projectId] = $schemaSize;
|
||||
$this->accessTimes[$projectId] = \hrtime(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evict until we have room for the new schema
|
||||
while ($this->currentBytes + $schemaSize > $this->maxBytes && !empty($this->cache)) {
|
||||
$this->evictLRU();
|
||||
}
|
||||
|
||||
$this->cache[$projectId] = $schema;
|
||||
$this->memorySizes[$projectId] = $schemaSize;
|
||||
$this->accessTimes[$projectId] = \hrtime(true);
|
||||
$this->currentBytes += $schemaSize;
|
||||
} finally {
|
||||
$this->lock->unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the memory size of a schema in bytes.
|
||||
*
|
||||
* Uses heuristic estimation for relative sizing in LRU eviction.
|
||||
* Actual memory usage varies based on resolver closures, descriptions, and type complexity.
|
||||
*/
|
||||
private function calculateSchemaSize(GQLSchema $schema): int
|
||||
{
|
||||
$typeMap = $schema->getTypeMap();
|
||||
$typeCount = \count($typeMap);
|
||||
$fieldCount = 0;
|
||||
|
||||
foreach ($typeMap as $type) {
|
||||
if (\method_exists($type, 'getFields')) {
|
||||
$fieldCount += \count($type->getFields());
|
||||
}
|
||||
}
|
||||
|
||||
return ($typeCount * self::BYTES_PER_TYPE) + ($fieldCount * self::BYTES_PER_FIELD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a project's schema as dirty (needs rebuild).
|
||||
* Uses shared Swoole Table to propagate across all workers when available,
|
||||
* otherwise falls back to local array (for single-worker/test scenarios).
|
||||
*/
|
||||
public function setDirty(string $projectId): void
|
||||
{
|
||||
if ($this->dirty !== null) {
|
||||
$this->dirty->set($projectId, ['timestamp' => \time()]);
|
||||
} else {
|
||||
$this->local[$projectId] = \time();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a project's schema is dirty.
|
||||
*/
|
||||
public function isDirty(string $projectId): bool
|
||||
{
|
||||
if ($this->dirty !== null) {
|
||||
return $this->dirty->exists($projectId);
|
||||
}
|
||||
return isset($this->local[$projectId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a project's dirty flag (from shared table or local).
|
||||
*/
|
||||
private function clearDirty(string $projectId): void
|
||||
{
|
||||
if ($this->dirty !== null) {
|
||||
$this->dirty->del($projectId);
|
||||
} else {
|
||||
unset($this->local[$projectId]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific project's schema from cache.
|
||||
*/
|
||||
public function remove(string $projectId): void
|
||||
{
|
||||
$this->lock->lock();
|
||||
try {
|
||||
$this->removeInternal($projectId);
|
||||
} finally {
|
||||
$this->lock->unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal remove method (without locking - must be called within lock).
|
||||
*/
|
||||
private function removeInternal(string $projectId): void
|
||||
{
|
||||
if (isset($this->memorySizes[$projectId])) {
|
||||
$this->currentBytes = \max(0, $this->currentBytes - $this->memorySizes[$projectId]);
|
||||
}
|
||||
|
||||
unset($this->cache[$projectId]);
|
||||
unset($this->accessTimes[$projectId]);
|
||||
unset($this->memorySizes[$projectId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached schemas.
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->lock->lock();
|
||||
try {
|
||||
$this->cache = [];
|
||||
$this->accessTimes = [];
|
||||
$this->memorySizes = [];
|
||||
$this->local = [];
|
||||
$this->currentBytes = 0;
|
||||
} finally {
|
||||
$this->lock->unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current cache size (number of schemas).
|
||||
*/
|
||||
public function size(): int
|
||||
{
|
||||
return \count($this->cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict least recently used entries if over memory capacity.
|
||||
*/
|
||||
private function evictIfNeeded(): void
|
||||
{
|
||||
while ($this->currentBytes > $this->maxBytes && !empty($this->cache)) {
|
||||
$this->evictLRU();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict the least recently used entry.
|
||||
* Must be called within a locked context.
|
||||
*/
|
||||
private function evictLRU(): void
|
||||
{
|
||||
if (empty($this->accessTimes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lruProject = \array_key_first($this->accessTimes);
|
||||
$lruTime = $this->accessTimes[$lruProject];
|
||||
|
||||
foreach ($this->accessTimes as $projectId => $time) {
|
||||
if ($time < $lruTime) {
|
||||
$lruTime = $time;
|
||||
$lruProject = $projectId;
|
||||
}
|
||||
}
|
||||
|
||||
$this->removeInternal($lruProject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics for monitoring.
|
||||
*
|
||||
* @return array{schemas: int, memoryMB: float, maxMemoryMB: int, dirty: int}
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
return [
|
||||
'schemas' => \count($this->cache),
|
||||
'memoryMB' => \round($this->currentBytes / 1024 / 1024, 2),
|
||||
'maxMemoryMB' => $this->getMaxSizeMB(),
|
||||
'dirty' => $this->dirty !== null
|
||||
? $this->dirty->count()
|
||||
: \count($this->local),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,4 @@ class Exception extends AppwriteException implements ClientAware
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'appwrite';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,16 +73,25 @@ abstract class Adapter implements PromiseAdapter
|
||||
/**
|
||||
* Create a new promise that is rejected with the given reason.
|
||||
*
|
||||
* @param mixed $reason
|
||||
* @param \Throwable $reason
|
||||
* @return GQLPromise
|
||||
*/
|
||||
abstract public function createRejected(mixed $reason): GQLPromise;
|
||||
abstract public function createRejected(\Throwable $reason): GQLPromise;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Synchronously wait for promise completion and return the result.
|
||||
*
|
||||
* @param GQLPromise $promise
|
||||
* @return mixed
|
||||
* @throws \Throwable
|
||||
*/
|
||||
abstract public function wait(GQLPromise $promise): mixed;
|
||||
}
|
||||
|
||||
@@ -4,39 +4,137 @@ namespace Appwrite\GraphQL\Promises\Adapter;
|
||||
|
||||
use Appwrite\GraphQL\Promises\Adapter;
|
||||
use Appwrite\Promises\Swoole as SwoolePromise;
|
||||
use GraphQL\Executor\Promise\Adapter\SyncPromise;
|
||||
use GraphQL\Executor\Promise\Promise as GQLPromise;
|
||||
|
||||
class Swoole extends Adapter
|
||||
{
|
||||
/**
|
||||
* Wait for promise completion and return the result.
|
||||
*
|
||||
* @param GQLPromise $promise
|
||||
* @return mixed
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function wait(GQLPromise $promise): mixed
|
||||
{
|
||||
/** @var SwoolePromise $swoolePromise */
|
||||
$swoolePromise = $promise->adoptedPromise;
|
||||
|
||||
// Run both graphql-php's SyncPromise queue and our SwoolePromise queue
|
||||
// graphql-php's Deferred uses SyncPromise::getQueue() internally
|
||||
$syncQueue = SyncPromise::getQueue();
|
||||
$swooleQueue = SwoolePromise::getQueue();
|
||||
|
||||
while ($swoolePromise->state === SwoolePromise::PENDING) {
|
||||
// Run graphql-php's SyncPromise queue first (handles Deferred)
|
||||
if (!$syncQueue->isEmpty()) {
|
||||
SyncPromise::runQueue();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Then run our SwoolePromise queue
|
||||
if (!$swooleQueue->isEmpty()) {
|
||||
SwoolePromise::runQueue();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Both queues empty but promise still pending - this shouldn't happen
|
||||
// in a properly resolved promise chain
|
||||
break;
|
||||
}
|
||||
|
||||
if ($swoolePromise->state === SwoolePromise::FULFILLED) {
|
||||
return $swoolePromise->result;
|
||||
}
|
||||
|
||||
if ($swoolePromise->state === SwoolePromise::REJECTED) {
|
||||
throw $swoolePromise->result;
|
||||
}
|
||||
|
||||
throw new \Exception('Could not resolve promise - still pending');
|
||||
}
|
||||
|
||||
public function create(callable $resolver): GQLPromise
|
||||
{
|
||||
$promise = new SwoolePromise(function ($resolve, $reject) use ($resolver) {
|
||||
$resolver($resolve, $reject);
|
||||
});
|
||||
// Create without executor - don't enqueue anything
|
||||
$promise = new SwoolePromise();
|
||||
|
||||
try {
|
||||
// Call resolver synchronously - it may call resolve/reject
|
||||
$resolver(
|
||||
[$promise, 'resolve'],
|
||||
[$promise, 'reject']
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$promise->reject($e);
|
||||
}
|
||||
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function createFulfilled($value = null): GQLPromise
|
||||
{
|
||||
$promise = new SwoolePromise(function ($resolve, $reject) use ($value) {
|
||||
$resolve($value);
|
||||
});
|
||||
// Create without executor and resolve immediately (no coroutine)
|
||||
$promise = new SwoolePromise();
|
||||
$promise->resolve($value);
|
||||
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function createRejected($reason): GQLPromise
|
||||
public function createRejected(\Throwable $reason): GQLPromise
|
||||
{
|
||||
$promise = new SwoolePromise(function ($resolve, $reject) use ($reason) {
|
||||
$reject($reason);
|
||||
});
|
||||
// Create without executor and reject immediately (no coroutine)
|
||||
$promise = new SwoolePromise();
|
||||
$promise->reject($reason);
|
||||
|
||||
return new GQLPromise($promise, $this);
|
||||
}
|
||||
|
||||
public function all(array $promisesOrValues): GQLPromise
|
||||
public function all(iterable $promisesOrValues): GQLPromise
|
||||
{
|
||||
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
|
||||
$promisesOrValues = \is_array($promisesOrValues) ? $promisesOrValues : \iterator_to_array($promisesOrValues);
|
||||
$total = \count($promisesOrValues);
|
||||
|
||||
if ($total === 0) {
|
||||
return $this->createFulfilled([]);
|
||||
}
|
||||
|
||||
// Create the combined promise without executor
|
||||
$combinedPromise = new SwoolePromise();
|
||||
|
||||
$count = 0;
|
||||
$result = [];
|
||||
$rejected = false;
|
||||
|
||||
$checkComplete = static function () use (&$count, $total, &$result, &$rejected, $combinedPromise): void {
|
||||
if (!$rejected && $count === $total) {
|
||||
\ksort($result);
|
||||
$combinedPromise->resolve($result);
|
||||
}
|
||||
};
|
||||
|
||||
foreach ($promisesOrValues as $index => $promiseOrValue) {
|
||||
if ($promiseOrValue instanceof GQLPromise) {
|
||||
$result[$index] = null;
|
||||
// Use GQLPromise::then() which goes through adapter->then()
|
||||
// This matches SyncPromiseAdapter's behavior
|
||||
$promiseOrValue->then(
|
||||
static function ($value) use (&$result, $index, &$count, $checkComplete): void {
|
||||
$result[$index] = $value;
|
||||
++$count;
|
||||
$checkComplete();
|
||||
},
|
||||
[$combinedPromise, 'reject']
|
||||
);
|
||||
} else {
|
||||
$result[$index] = $promiseOrValue;
|
||||
++$count;
|
||||
}
|
||||
}
|
||||
|
||||
$checkComplete();
|
||||
|
||||
return new GQLPromise($combinedPromise, $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ class Resolvers
|
||||
* @param string $collectionId
|
||||
* @param callable $url
|
||||
* @param callable $params
|
||||
* @param string $listKey The key in the response containing the list (e.g., 'documents' or 'rows')
|
||||
* @return callable
|
||||
*/
|
||||
public static function documentList(
|
||||
@@ -125,9 +126,10 @@ class Resolvers
|
||||
string $collectionId,
|
||||
callable $url,
|
||||
callable $params,
|
||||
string $listKey = 'documents',
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $listKey, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -136,8 +138,8 @@ class Resolvers
|
||||
$request->setURI($url($databaseId, $collectionId, $args));
|
||||
$request->setQueryString($params($databaseId, $collectionId, $args));
|
||||
|
||||
$beforeResolve = function ($payload) {
|
||||
return $payload['documents'];
|
||||
$beforeResolve = function ($payload) use ($listKey) {
|
||||
return $payload[$listKey];
|
||||
};
|
||||
|
||||
self::resolve($utopia, $request, $response, $resolve, $reject, $beforeResolve);
|
||||
@@ -286,7 +288,7 @@ class Resolvers
|
||||
$payload = $beforeReject($payload);
|
||||
}
|
||||
$reject(new GQLException(
|
||||
message: $payload['message'],
|
||||
message: $payload['message'] ?? 'Server Error',
|
||||
code: $response->getStatusCode()
|
||||
));
|
||||
return;
|
||||
|
||||
+280
-149
@@ -3,30 +3,116 @@
|
||||
namespace Appwrite\GraphQL;
|
||||
|
||||
use Appwrite\GraphQL\Types\Mapper;
|
||||
use Appwrite\GraphQL\Types\Registry;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema as GQLSchema;
|
||||
use Utopia\App;
|
||||
use Utopia\Exception;
|
||||
use Utopia\Console;
|
||||
use Utopia\Route;
|
||||
|
||||
class Schema
|
||||
{
|
||||
protected static ?GQLSchema $schema = null;
|
||||
protected static array $dirty = [];
|
||||
private Registry $registry;
|
||||
private ?Mapper $mapper = null;
|
||||
private string $projectId;
|
||||
|
||||
/**
|
||||
* Reserved GraphQL type names that cannot be used for collection types.
|
||||
*/
|
||||
private const array RESERVED_TYPES = [
|
||||
'Query', 'Mutation', 'Subscription',
|
||||
'String', 'Int', 'Float', 'Boolean', 'ID',
|
||||
'Input', 'Enum', '__Type', '__Field', '__InputValue',
|
||||
'__EnumValue', '__Directive', '__Schema'
|
||||
];
|
||||
|
||||
/**
|
||||
* Sanitize a string to be a valid GraphQL name.
|
||||
*
|
||||
* GraphQL names must match /^[_A-Za-z][_0-9A-Za-z]*$/
|
||||
* - Must start with a letter or underscore
|
||||
* - Can only contain letters, digits, and underscores
|
||||
* - Cannot start with two underscores (reserved for introspection)
|
||||
*
|
||||
* @param string $name The name to sanitize
|
||||
* @return string The sanitized name
|
||||
*/
|
||||
private function sanitizeGraphQLName(string $name): string
|
||||
{
|
||||
// Replace any non-alphanumeric characters with underscores
|
||||
$sanitized = \preg_replace('/[^A-Za-z0-9_]/', '_', $name);
|
||||
|
||||
// If the name starts with a digit, prefix with underscore
|
||||
if (\preg_match('/^[0-9]/', $sanitized)) {
|
||||
$sanitized = '_' . $sanitized;
|
||||
}
|
||||
|
||||
// If the name starts with double underscore, prefix with 'u' to avoid
|
||||
// collision with GraphQL introspection types (using '_' would still leave '__')
|
||||
if (\str_starts_with($sanitized, '__')) {
|
||||
$sanitized = 'u' . $sanitized;
|
||||
}
|
||||
|
||||
// Ensure the name is not empty
|
||||
if (empty($sanitized)) {
|
||||
$sanitized = '_unnamed';
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Schema instance.
|
||||
*
|
||||
* @param string $projectId The project ID for this schema
|
||||
*/
|
||||
public function __construct(string $projectId)
|
||||
{
|
||||
$this->projectId = $projectId;
|
||||
$this->registry = new Registry($projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the project ID.
|
||||
*/
|
||||
public function getProjectId(): string
|
||||
{
|
||||
return $this->projectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registry instance.
|
||||
*/
|
||||
public function getRegistry(): Registry
|
||||
{
|
||||
return $this->registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapper instance
|
||||
*/
|
||||
public function getMapper(): ?Mapper
|
||||
{
|
||||
return $this->mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a GraphQL schema for a specific project.
|
||||
* Uses LRU cache for collection-based schemas.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param callable $complexity Function to calculate complexity
|
||||
* @param callable $attributes Function to get attributes
|
||||
* @param array $urls Array of functions to get urls for specific method types
|
||||
* @param array $params Array of functions to build parameters for specific method types
|
||||
* @param Cache $cache The schema cache instance
|
||||
* @param callable $complexity Function to calculate complexity
|
||||
* @param callable $attributes Function to get attributes
|
||||
* @param array $urls Array of functions to get urls for specific method types
|
||||
* @param array $params Array of functions to build parameters for specific method types
|
||||
* @return GQLSchema
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function build(
|
||||
public function build(
|
||||
App $utopia,
|
||||
Cache $cache,
|
||||
callable $complexity,
|
||||
callable $attributes,
|
||||
array $urls,
|
||||
@@ -36,44 +122,57 @@ class Schema
|
||||
return $utopia;
|
||||
});
|
||||
|
||||
if (!empty(self::$schema)) {
|
||||
return self::$schema;
|
||||
$cached = $cache->get($this->projectId);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$api = static::api(
|
||||
$utopia,
|
||||
$complexity
|
||||
);
|
||||
//$collections = static::collections(
|
||||
// $utopia,
|
||||
// $complexity,
|
||||
// $attributes,
|
||||
// $urls,
|
||||
// $params,
|
||||
//);
|
||||
try {
|
||||
// Build API schema fresh for each Schema instance to ensure types are properly registered
|
||||
// in this instance's Registry. The full schema is cached by projectId, so this only
|
||||
// runs on cache miss.
|
||||
$api = $this->api($utopia, $complexity);
|
||||
|
||||
$queries = \array_merge_recursive(
|
||||
$api['query'],
|
||||
//$collections['query']
|
||||
);
|
||||
$mutations = \array_merge_recursive(
|
||||
$api['mutation'],
|
||||
//$collections['mutation']
|
||||
);
|
||||
$collections = $this->collections(
|
||||
$utopia,
|
||||
$complexity,
|
||||
$attributes,
|
||||
$urls,
|
||||
$params,
|
||||
);
|
||||
|
||||
\ksort($queries);
|
||||
\ksort($mutations);
|
||||
$queries = \array_merge(
|
||||
$api['query'],
|
||||
$collections['query']
|
||||
);
|
||||
|
||||
return static::$schema = new GQLSchema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => $queries
|
||||
]),
|
||||
'mutation' => new ObjectType([
|
||||
'name' => 'Mutation',
|
||||
'fields' => $mutations
|
||||
])
|
||||
]);
|
||||
$mutations = \array_merge(
|
||||
$api['mutation'],
|
||||
$collections['mutation']
|
||||
);
|
||||
|
||||
\ksort($queries);
|
||||
\ksort($mutations);
|
||||
|
||||
$schema = new GQLSchema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => $queries
|
||||
]),
|
||||
'mutation' => new ObjectType([
|
||||
'name' => 'Mutation',
|
||||
'fields' => $mutations
|
||||
])
|
||||
]);
|
||||
|
||||
$cache->set($this->projectId, $schema);
|
||||
|
||||
return $schema;
|
||||
} catch (\Throwable $e) {
|
||||
// Clear registry on failure to prevent inconsistent state
|
||||
$this->registry->clear();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,13 +182,12 @@ class Schema
|
||||
* @param App $utopia
|
||||
* @param callable $complexity
|
||||
* @return array
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected static function api(App $utopia, callable $complexity): array
|
||||
protected function api(App $utopia, callable $complexity): array
|
||||
{
|
||||
Mapper::init($utopia
|
||||
->getResource('response')
|
||||
->getModels());
|
||||
$models = $utopia->getResource('response')->getModels();
|
||||
$this->mapper = new Mapper($this->registry, $models);
|
||||
|
||||
$queries = [];
|
||||
$mutations = [];
|
||||
@@ -114,7 +212,7 @@ class Schema
|
||||
$methodName = $method->getMethodName();
|
||||
$name = $namespace . \ucfirst($methodName);
|
||||
|
||||
foreach (Mapper::route($utopia, $route, $method, $complexity) as $field) {
|
||||
foreach ($this->mapper->route($utopia, $route, $method, $complexity) as $field) {
|
||||
switch ($route->getMethod()) {
|
||||
case 'GET':
|
||||
$queries[$name] = $field;
|
||||
@@ -126,7 +224,7 @@ class Schema
|
||||
$mutations[$name] = $field;
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("Unsupported method: {$route->getMethod()}");
|
||||
Console::warning("Unsupported method for GraphQL schema generation: {$route->getMethod()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,18 +238,18 @@ class Schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates all of a projects attributes and builds GraphQL
|
||||
* Iterates all of a project's attributes and builds GraphQL
|
||||
* queries and mutations for the collections they make up.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param callable $complexity
|
||||
* @param callable $attributes
|
||||
* @param array $urls
|
||||
* @param array $params
|
||||
* @return array
|
||||
* @param callable(int $complexity, array $args): int $complexity
|
||||
* @param callable(int $limit, string $last): array $attributes
|
||||
* @param array<string, array<string, callable(string $databaseId, string $collectionId, array $args): string>> $urls
|
||||
* @param array<string, array<string, callable(string $databaseId, string $collectionId, array $args): string>> $params
|
||||
* @return array{query: array, mutation: array} Array containing query and mutation field definitions
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected static function collections(
|
||||
protected function collections(
|
||||
App $utopia,
|
||||
callable $complexity,
|
||||
callable $attributes,
|
||||
@@ -162,23 +260,28 @@ class Schema
|
||||
$queryFields = [];
|
||||
$mutationFields = [];
|
||||
$limit = 1000;
|
||||
$offset = 0;
|
||||
$last = null;
|
||||
|
||||
while (!empty($attrs = $attributes($limit, $offset))) {
|
||||
while (!empty($attrs = $attributes($limit, $last))) {
|
||||
foreach ($attrs as $attr) {
|
||||
if ($attr['status'] !== 'available') {
|
||||
continue;
|
||||
}
|
||||
$databaseId = $attr['databaseId'];
|
||||
$collectionId = $attr['collectionId'];
|
||||
$key = $attr['key'];
|
||||
$type = $attr['type'];
|
||||
$array = $attr['array'];
|
||||
$required = $attr['required'];
|
||||
$default = $attr['default'];
|
||||
$escapedKey = str_replace('$', '', $key);
|
||||
$collections[$collectionId][$escapedKey] = [
|
||||
'type' => Mapper::attribute(
|
||||
$databaseId = $attr->getAttribute('databaseId');
|
||||
$collectionId = $attr->getAttribute('collectionId');
|
||||
$databaseType = $attr->getAttribute('databaseType', 'legacy');
|
||||
$key = $attr->getAttribute('key');
|
||||
$type = $attr->getAttribute('type');
|
||||
$array = $attr->getAttribute('array');
|
||||
$required = $attr->getAttribute('required');
|
||||
$default = $attr->getAttribute('default');
|
||||
$escapedKey = \str_replace('$', '', $key);
|
||||
|
||||
// Use composite key for collection grouping
|
||||
$collectionKey = "{$databaseId}_{$collectionId}";
|
||||
|
||||
$collections[$collectionKey]['databaseId'] = $databaseId;
|
||||
$collections[$collectionKey]['collectionId'] = $collectionId;
|
||||
$collections[$collectionKey]['databaseType'] = $databaseType;
|
||||
$collections[$collectionKey]['attributes'][$escapedKey] = [
|
||||
'type' => $this->mapper->attribute(
|
||||
$type,
|
||||
$array,
|
||||
$required
|
||||
@@ -187,82 +290,115 @@ class Schema
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($collections as $collectionId => $attributes) {
|
||||
$objectType = new ObjectType([
|
||||
'name' => $collectionId,
|
||||
'fields' => \array_merge(
|
||||
["_id" => ['type' => Type::string()]],
|
||||
$attributes
|
||||
),
|
||||
]);
|
||||
$attributes = \array_merge(
|
||||
$attributes,
|
||||
Mapper::args('mutate')
|
||||
);
|
||||
// Use the last Document as cursor for pagination
|
||||
$last = \end($attrs) ?: null;
|
||||
}
|
||||
|
||||
$queryFields[$collectionId . 'Get'] = [
|
||||
'type' => $objectType,
|
||||
'args' => Mapper::args('id'),
|
||||
'resolve' => Resolvers::documentGet(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['get'],
|
||||
)
|
||||
];
|
||||
$queryFields[$collectionId . 'List'] = [
|
||||
'type' => Type::listOf($objectType),
|
||||
'args' => Mapper::args('list'),
|
||||
'resolve' => Resolvers::documentList(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['list'],
|
||||
$params['list'],
|
||||
),
|
||||
'complexity' => $complexity,
|
||||
];
|
||||
foreach ($collections as $collectionData) {
|
||||
$databaseId = $collectionData['databaseId'];
|
||||
$collectionId = $collectionData['collectionId'];
|
||||
$databaseType = $collectionData['databaseType'];
|
||||
$attributes = $collectionData['attributes'];
|
||||
|
||||
$mutationFields[$collectionId . 'Create'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $attributes,
|
||||
'resolve' => Resolvers::documentCreate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['create'],
|
||||
$params['create'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Update'] = [
|
||||
'type' => $objectType,
|
||||
'args' => \array_merge(
|
||||
Mapper::args('id'),
|
||||
\array_map(
|
||||
fn ($attr) => $attr['type'] = Type::getNullableType($attr['type']),
|
||||
$attributes
|
||||
)
|
||||
),
|
||||
'resolve' => Resolvers::documentUpdate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['update'],
|
||||
$params['update'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Delete'] = [
|
||||
'type' => Mapper::model('none'),
|
||||
'args' => Mapper::args('id'),
|
||||
'resolve' => Resolvers::documentDelete(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['delete'],
|
||||
)
|
||||
];
|
||||
// Get URLs and params for this database type
|
||||
$typeUrls = $urls[$databaseType] ?? $urls['legacy'];
|
||||
$typeParams = $params[$databaseType] ?? $params['legacy'];
|
||||
|
||||
// Create unique type name for this project's collection
|
||||
$sanitizedProjectId = $this->sanitizeGraphQLName($this->projectId);
|
||||
$sanitizedCollectionId = $this->sanitizeGraphQLName($collectionId);
|
||||
$typeName = $sanitizedProjectId . \ucfirst($sanitizedCollectionId);
|
||||
|
||||
if (\in_array($typeName, self::RESERVED_TYPES)) {
|
||||
throw new \Exception("Type name collision with reserved type: {$typeName}");
|
||||
}
|
||||
$offset += $limit;
|
||||
|
||||
if ($this->registry->has($typeName)) {
|
||||
throw new \Exception("Type name collision detected: {$typeName} already exists in registry");
|
||||
}
|
||||
|
||||
$objectType = new ObjectType([
|
||||
'name' => $typeName,
|
||||
'fields' => \array_merge(
|
||||
["_id" => ['type' => Type::string()]],
|
||||
$attributes
|
||||
),
|
||||
]);
|
||||
|
||||
$mutateAttributes = \array_merge(
|
||||
$attributes,
|
||||
$this->mapper->args('mutate')
|
||||
);
|
||||
|
||||
// Prefix field names with sanitized collection ID to avoid conflicts
|
||||
$queryFields[$sanitizedCollectionId . 'Get'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $this->mapper->args('id'),
|
||||
'resolve' => Resolvers::documentGet(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$typeUrls['read'],
|
||||
)
|
||||
];
|
||||
|
||||
// Determine the list key based on database type (rows for tablesdb, documents for legacy)
|
||||
$listKey = $databaseType === 'tablesdb'
|
||||
? 'rows'
|
||||
: 'documents';
|
||||
|
||||
$queryFields[$sanitizedCollectionId . 'List'] = [
|
||||
'type' => Type::listOf($objectType),
|
||||
'args' => $this->mapper->args('list'),
|
||||
'resolve' => Resolvers::documentList(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$typeUrls['list'],
|
||||
$typeParams['list'],
|
||||
$listKey,
|
||||
),
|
||||
'complexity' => $complexity,
|
||||
];
|
||||
|
||||
$mutationFields[$sanitizedCollectionId . 'Create'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $mutateAttributes,
|
||||
'resolve' => Resolvers::documentCreate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$typeUrls['create'],
|
||||
$typeParams['create'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$sanitizedCollectionId . 'Update'] = [
|
||||
'type' => $objectType,
|
||||
'args' => \array_merge(
|
||||
$this->mapper->args('id'),
|
||||
\array_map(
|
||||
fn ($attr) => ['type' => Type::getNullableType($attr['type'])],
|
||||
$mutateAttributes
|
||||
)
|
||||
),
|
||||
'resolve' => Resolvers::documentUpdate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$typeUrls['update'],
|
||||
$typeParams['update'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$sanitizedCollectionId . 'Delete'] = [
|
||||
'type' => $this->mapper->model('none'),
|
||||
'args' => $this->mapper->args('id'),
|
||||
'resolve' => Resolvers::documentDelete(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$typeUrls['delete'],
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -270,9 +406,4 @@ class Schema
|
||||
'mutation' => $mutationFields
|
||||
];
|
||||
}
|
||||
|
||||
public static function setDirty(string $projectId): void
|
||||
{
|
||||
self::$dirty[$projectId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,53 +5,49 @@ namespace Appwrite\GraphQL;
|
||||
use Appwrite\GraphQL\Types\Assoc;
|
||||
use Appwrite\GraphQL\Types\InputFile;
|
||||
use Appwrite\GraphQL\Types\Json;
|
||||
use Appwrite\GraphQL\Types\Registry;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
class Types
|
||||
{
|
||||
/**
|
||||
* Get the JSON type.
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
public static function json(): Type
|
||||
{
|
||||
if (Registry::has(Json::class)) {
|
||||
return Registry::get(Json::class);
|
||||
}
|
||||
$type = new Json();
|
||||
Registry::set(Json::class, $type);
|
||||
return $type;
|
||||
}
|
||||
private static ?Json $json = null;
|
||||
private static ?Assoc $assoc = null;
|
||||
private static ?InputFile $inputFile = null;
|
||||
|
||||
/**
|
||||
* Get the JSON type.
|
||||
*
|
||||
* @return Json
|
||||
* Thread-safety note: In Swoole, each worker is a separate process with its own
|
||||
* static variables. Within a worker, coroutines are cooperative and only yield
|
||||
* at I/O points. Since these constructors have no I/O, the null check and
|
||||
* assignment execute atomically without needing locks.
|
||||
*/
|
||||
public static function json(): Type
|
||||
{
|
||||
if (self::$json === null) {
|
||||
self::$json = new Json();
|
||||
}
|
||||
return self::$json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Assoc type.
|
||||
*/
|
||||
public static function assoc(): Type
|
||||
{
|
||||
if (Registry::has(Assoc::class)) {
|
||||
return Registry::get(Assoc::class);
|
||||
if (self::$assoc === null) {
|
||||
self::$assoc = new Assoc();
|
||||
}
|
||||
$type = new Assoc();
|
||||
Registry::set(Assoc::class, $type);
|
||||
return $type;
|
||||
return self::$assoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the InputFile type.
|
||||
*
|
||||
* @return InputFile
|
||||
*/
|
||||
public static function inputFile(): Type
|
||||
{
|
||||
if (Registry::has(InputFile::class)) {
|
||||
return Registry::get(InputFile::class);
|
||||
if (self::$inputFile === null) {
|
||||
self::$inputFile = new InputFile();
|
||||
}
|
||||
$type = new InputFile();
|
||||
Registry::set(InputFile::class, $type);
|
||||
return $type;
|
||||
return self::$inputFile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
namespace Appwrite\GraphQL\Types;
|
||||
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
|
||||
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
|
||||
class Assoc extends Json
|
||||
class Assoc extends ScalarType
|
||||
{
|
||||
public $name = 'Assoc';
|
||||
public $description = 'The `Assoc` scalar type represents associative array values.';
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'name' => 'Assoc',
|
||||
'description' => 'The `Assoc` scalar type represents associative array values.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function serialize($value)
|
||||
{
|
||||
|
||||
@@ -8,9 +8,13 @@ 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
|
||||
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'name' => 'InputFile',
|
||||
'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)
|
||||
{
|
||||
|
||||
@@ -14,9 +14,13 @@ 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
|
||||
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'name' => 'Json',
|
||||
'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)
|
||||
{
|
||||
|
||||
@@ -16,19 +16,27 @@ use Utopia\Validator\Nullable;
|
||||
|
||||
class Mapper
|
||||
{
|
||||
private static array $models = [];
|
||||
private static array $args = [];
|
||||
private static array $blacklist = [
|
||||
private Registry $registry;
|
||||
private array $models;
|
||||
private array $args;
|
||||
private array $blacklist = [
|
||||
'/v1/mock',
|
||||
'/v1/graphql',
|
||||
'/v1/account/sessions/oauth2',
|
||||
];
|
||||
|
||||
public static function init(array $models): void
|
||||
/**
|
||||
* Create a new Mapper instance.
|
||||
*
|
||||
* @param Registry $registry The type registry instance
|
||||
* @param array $models The response models
|
||||
*/
|
||||
public function __construct(Registry $registry, array $models)
|
||||
{
|
||||
self::$models = $models;
|
||||
$this->registry = $registry;
|
||||
$this->models = $models;
|
||||
|
||||
self::$args = [
|
||||
$this->args = [
|
||||
'id' => [
|
||||
'id' => [
|
||||
'type' => Type::nonNull(Type::string()),
|
||||
@@ -41,6 +49,10 @@ class Mapper
|
||||
],
|
||||
],
|
||||
'mutate' => [
|
||||
'id' => [
|
||||
'type' => Type::string(),
|
||||
'defaultValue' => null,
|
||||
],
|
||||
'permissions' => [
|
||||
'type' => Type::listOf(Type::nonNull(Type::string())),
|
||||
'defaultValue' => [],
|
||||
@@ -62,9 +74,7 @@ class Mapper
|
||||
'enum' => Type::string()
|
||||
];
|
||||
|
||||
foreach ($defaults as $type => $default) {
|
||||
Registry::set($type, $default);
|
||||
}
|
||||
$this->registry->initBaseTypes($defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,18 +83,27 @@ class Mapper
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public static function args(string $key): array
|
||||
public function args(string $key): array
|
||||
{
|
||||
return self::$args[$key] ?? [];
|
||||
return $this->args[$key] ?? [];
|
||||
}
|
||||
|
||||
public static function route(
|
||||
/**
|
||||
* Map a route to GraphQL fields.
|
||||
*
|
||||
* @param App $utopia
|
||||
* @param Route $route
|
||||
* @param Method $method
|
||||
* @param callable $complexity
|
||||
* @return iterable<array> Iterator of GraphQL field definitions
|
||||
*/
|
||||
public function route(
|
||||
App $utopia,
|
||||
Route $route,
|
||||
Method $method,
|
||||
callable $complexity
|
||||
): iterable {
|
||||
foreach (self::$blacklist as $blacklist) {
|
||||
foreach ($this->blacklist as $blacklist) {
|
||||
if (\str_starts_with($route->getPath(), $blacklist)) {
|
||||
return;
|
||||
}
|
||||
@@ -100,20 +119,20 @@ class Mapper
|
||||
|
||||
if (\is_array($modelName)) {
|
||||
foreach ($modelName as $name) {
|
||||
$models[] = static::$models[$name];
|
||||
$models[] = $this->models[$name];
|
||||
}
|
||||
} else {
|
||||
$models[] = static::$models[$modelName];
|
||||
$models[] = $this->models[$modelName];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If single response, get its model and wrap in array
|
||||
$modelName = $responses->getModel();
|
||||
$models = [static::$models[$modelName]];
|
||||
$models = [$this->models[$modelName]];
|
||||
}
|
||||
|
||||
foreach ($models as $model) {
|
||||
$type = Mapper::model(\ucfirst($model->getType()));
|
||||
$type = $this->model(\ucfirst($model->getType()));
|
||||
$description = $route->getDesc();
|
||||
$params = [];
|
||||
$list = false;
|
||||
@@ -140,7 +159,7 @@ class Mapper
|
||||
$list = true;
|
||||
}
|
||||
|
||||
$parameterType = Mapper::param(
|
||||
$parameterType = $this->param(
|
||||
$utopia,
|
||||
$parameter['validator'],
|
||||
!$optional,
|
||||
@@ -173,14 +192,14 @@ class Mapper
|
||||
* @param string $name
|
||||
* @return Type
|
||||
*/
|
||||
public static function model(string $name): Type
|
||||
public function model(string $name): Type
|
||||
{
|
||||
if (Registry::has($name)) {
|
||||
return Registry::get($name);
|
||||
if ($this->registry->has($name)) {
|
||||
return $this->registry->get($name);
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
$model = self::$models[\lcfirst($name)];
|
||||
$model = $this->models[\lcfirst($name)];
|
||||
|
||||
// If model has additional properties, explicitly add a 'data' field
|
||||
if ($model->isAny()) {
|
||||
@@ -213,9 +232,9 @@ class Mapper
|
||||
$escapedKey = str_replace('$', '_', $key);
|
||||
|
||||
if (\is_array($rule['type'])) {
|
||||
$type = self::getUnionType($escapedKey, $rule);
|
||||
$type = $this->getUnionType($escapedKey, $rule);
|
||||
} else {
|
||||
$type = self::getObjectType($rule);
|
||||
$type = $this->getObjectType($rule);
|
||||
}
|
||||
|
||||
if ($rule['array']) {
|
||||
@@ -237,7 +256,7 @@ class Mapper
|
||||
'fields' => $fields,
|
||||
]);
|
||||
|
||||
Registry::set($name, $type);
|
||||
$this->registry->set($name, $type);
|
||||
|
||||
return $type;
|
||||
}
|
||||
@@ -252,7 +271,7 @@ class Mapper
|
||||
* @return Type
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function param(
|
||||
public function param(
|
||||
App $utopia,
|
||||
Validator|callable $validator,
|
||||
bool $required,
|
||||
@@ -322,7 +341,7 @@ class Mapper
|
||||
$type = Type::boolean();
|
||||
break;
|
||||
case 'Utopia\Validator\ArrayList':
|
||||
$type = Type::listOf(self::param(
|
||||
$type = Type::listOf($this->param(
|
||||
$utopia,
|
||||
$validator->getValidator(),
|
||||
$required,
|
||||
@@ -371,10 +390,10 @@ class Mapper
|
||||
* @return Type
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function attribute(string $type, bool $array, bool $required): Type
|
||||
public function attribute(string $type, bool $array, bool $required): Type
|
||||
{
|
||||
if ($array) {
|
||||
return Type::listOf(self::attribute(
|
||||
return Type::listOf($this->attribute(
|
||||
$type,
|
||||
false,
|
||||
$required
|
||||
@@ -395,103 +414,102 @@ class Mapper
|
||||
return $type;
|
||||
}
|
||||
|
||||
private static function getObjectType(array $rule): Type
|
||||
private function getObjectType(array $rule): Type
|
||||
{
|
||||
$type = $rule['type'];
|
||||
|
||||
if (Registry::has($type)) {
|
||||
return Registry::get($type);
|
||||
if ($this->registry->has($type)) {
|
||||
return $this->registry->get($type);
|
||||
}
|
||||
|
||||
$complexModel = self::$models[$type];
|
||||
return self::model(\ucfirst($complexModel->getType()));
|
||||
$complexModel = $this->models[$type];
|
||||
return $this->model(\ucfirst($complexModel->getType()));
|
||||
}
|
||||
|
||||
private static function getUnionType(string $name, array $rule): Type
|
||||
private function getUnionType(string $name, array $rule): Type
|
||||
{
|
||||
$unionName = \ucfirst($name);
|
||||
|
||||
if (Registry::has($unionName)) {
|
||||
return Registry::get($unionName);
|
||||
if ($this->registry->has($unionName)) {
|
||||
return $this->registry->get($unionName);
|
||||
}
|
||||
|
||||
$types = [];
|
||||
foreach ($rule['type'] as $type) {
|
||||
$types[] = self::model(\ucfirst($type));
|
||||
$types[] = $this->model(\ucfirst($type));
|
||||
}
|
||||
|
||||
// resolveType returns a string type name instead of a Type object.
|
||||
// This ensures GraphQL looks up the type from the schema's type map,
|
||||
// which is essential for cached schemas where the original type instances
|
||||
// must be used (not newly created ones from calling model()).
|
||||
$unionType = new UnionType([
|
||||
'name' => $unionName,
|
||||
'types' => $types,
|
||||
'resolveType' => static function ($object) use ($unionName) {
|
||||
return static::getUnionImplementation($unionName, $object);
|
||||
return self::getUnionTypeName($unionName, $object);
|
||||
},
|
||||
]);
|
||||
|
||||
Registry::set($unionName, $unionType);
|
||||
$this->registry->set($unionName, $unionType);
|
||||
|
||||
return $unionType;
|
||||
}
|
||||
|
||||
private static function getUnionImplementation(string $name, array $object): Type
|
||||
/**
|
||||
* Get the type name for a union member based on the object data.
|
||||
* Returns a string type name that GraphQL will look up in the schema.
|
||||
*
|
||||
* @param string $name The union type name
|
||||
* @param array $object The object data
|
||||
* @return string The type name
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function getUnionTypeName(string $name, array $object): string
|
||||
{
|
||||
// TODO: Find a better way to do this
|
||||
|
||||
switch ($name) {
|
||||
case 'Attributes':
|
||||
return static::getColumnImplementation($object);
|
||||
case 'Columns':
|
||||
return static::getColumnImplementation($object, true);
|
||||
case 'HashOptions':
|
||||
return static::getHashOptionsImplementation($object);
|
||||
}
|
||||
|
||||
throw new Exception('Unknown union type: ' . $name);
|
||||
return match ($name) {
|
||||
'Attributes' => self::getColumnTypeName($object),
|
||||
'Columns' => self::getColumnTypeName($object, true),
|
||||
'HashOptions' => self::getHashOptionsTypeName($object),
|
||||
default => throw new Exception('Unknown union type: ' . $name),
|
||||
};
|
||||
}
|
||||
|
||||
private static function getColumnImplementation(array $object, bool $isColumns = false): Type
|
||||
private static function getColumnTypeName(array $object, bool $isColumns = false): string
|
||||
{
|
||||
$prefix = $isColumns ? 'Column' : 'Attribute';
|
||||
|
||||
return match ($object['type']) {
|
||||
'string' => match ($object['format'] ?? '') {
|
||||
'email' => static::model("{$prefix}Email"),
|
||||
'url' => static::model("{$prefix}Url"),
|
||||
'ip' => static::model("{$prefix}Ip"),
|
||||
default => static::model("{$prefix}String"),
|
||||
'email' => "{$prefix}Email",
|
||||
'url' => "{$prefix}Url",
|
||||
'ip' => "{$prefix}Ip",
|
||||
default => "{$prefix}String",
|
||||
},
|
||||
'enum' => static::model("{$prefix}String"), // TODO: Add enum type (breaking change if added)
|
||||
'integer' => static::model("{$prefix}Integer"),
|
||||
'double' => static::model("{$prefix}Float"),
|
||||
'boolean' => static::model("{$prefix}Boolean"),
|
||||
'datetime' => static::model("{$prefix}Datetime"),
|
||||
'relationship' => static::model("{$prefix}Relationship"),
|
||||
'point' => static::model("{$prefix}Point"),
|
||||
'linestring' => static::model("{$prefix}Line"),
|
||||
'polygon' => static::model("{$prefix}Polygon"),
|
||||
'enum' => "{$prefix}String", // TODO: Add enum type (breaking change if added)
|
||||
'integer' => "{$prefix}Integer",
|
||||
'double' => "{$prefix}Float",
|
||||
'boolean' => "{$prefix}Boolean",
|
||||
'datetime' => "{$prefix}Datetime",
|
||||
'relationship' => "{$prefix}Relationship",
|
||||
'point' => "{$prefix}Point",
|
||||
'linestring' => "{$prefix}Line",
|
||||
'polygon' => "{$prefix}Polygon",
|
||||
default => throw new Exception('Unknown ' . strtolower($prefix) . ' implementation'),
|
||||
};
|
||||
}
|
||||
|
||||
private static function getHashOptionsImplementation(array $object): Type
|
||||
private static function getHashOptionsTypeName(array $object): string
|
||||
{
|
||||
switch ($object['type']) {
|
||||
case 'argon2':
|
||||
return static::model('AlgoArgon2');
|
||||
case 'bcrypt':
|
||||
return static::model('AlgoBcrypt');
|
||||
case 'md5':
|
||||
return static::model('AlgoMd5');
|
||||
case 'phpass':
|
||||
return static::model('AlgoPhpass');
|
||||
case 'scrypt':
|
||||
return static::model('AlgoScrypt');
|
||||
case 'scryptMod':
|
||||
return static::model('AlgoScryptModified');
|
||||
case 'sha':
|
||||
return static::model('AlgoSha');
|
||||
}
|
||||
|
||||
throw new Exception('Unknown hash options implementation');
|
||||
return match ($object['type']) {
|
||||
'argon2' => 'AlgoArgon2',
|
||||
'bcrypt' => 'AlgoBcrypt',
|
||||
'md5' => 'AlgoMd5',
|
||||
'phpass' => 'AlgoPhpass',
|
||||
'scrypt' => 'AlgoScrypt',
|
||||
'scryptMod' => 'AlgoScryptModified',
|
||||
'sha' => 'AlgoSha',
|
||||
default => throw new Exception('Unknown hash options implementation'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,38 +6,129 @@ use GraphQL\Type\Definition\Type;
|
||||
|
||||
class Registry
|
||||
{
|
||||
private static array $register = [];
|
||||
/**
|
||||
* @var array<string, Type> Per-project type storage
|
||||
*/
|
||||
private array $types = [];
|
||||
|
||||
/**
|
||||
* Check if a type exists in the registry.
|
||||
*
|
||||
* @param string $type
|
||||
* @return bool
|
||||
* @var array<string, Type> Shared base types (boolean, string, etc.)
|
||||
*/
|
||||
public static function has(string $type): bool
|
||||
private array $baseTypes = [];
|
||||
|
||||
/**
|
||||
* @var string Current project context
|
||||
*/
|
||||
private string $projectId = '';
|
||||
|
||||
/**
|
||||
* Create a new Registry instance.
|
||||
*
|
||||
* @param string $projectId The project ID for this registry
|
||||
*/
|
||||
public function __construct(string $projectId = '')
|
||||
{
|
||||
return isset(self::$register[$type]);
|
||||
$this->projectId = $projectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current project ID.
|
||||
*/
|
||||
public function getProjectId(): string
|
||||
{
|
||||
return $this->projectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current project ID.
|
||||
*/
|
||||
public function setProjectId(string $projectId): void
|
||||
{
|
||||
$this->projectId = $projectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a type exists in the registry (checks base types first, then project types).
|
||||
*/
|
||||
public function has(string $type): bool
|
||||
{
|
||||
return isset($this->baseTypes[$type]) || isset($this->types[$type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a type from the registry.
|
||||
*
|
||||
* @param string $type
|
||||
* @return Type
|
||||
*/
|
||||
public static function get(string $type): Type
|
||||
public function get(string $type): Type
|
||||
{
|
||||
return self::$register[$type];
|
||||
if (isset($this->baseTypes[$type])) {
|
||||
return $this->baseTypes[$type];
|
||||
}
|
||||
|
||||
if (!isset($this->types[$type])) {
|
||||
throw new \RuntimeException("Type '{$type}' not found in registry for project '{$this->projectId}'");
|
||||
}
|
||||
|
||||
return $this->types[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a type in the registry.
|
||||
*
|
||||
* @param string $type
|
||||
* @param Type $typeObject
|
||||
* @param string $type The type name
|
||||
* @param Type $typeObject The type object
|
||||
* @param bool $isBaseType If true, stores as a shared base type
|
||||
*/
|
||||
public static function set(string $type, Type $typeObject): void
|
||||
public function set(string $type, Type $typeObject, bool $isBaseType = false): void
|
||||
{
|
||||
self::$register[$type] = $typeObject;
|
||||
if ($isBaseType) {
|
||||
$this->baseTypes[$type] = $typeObject;
|
||||
} else {
|
||||
$this->types[$type] = $typeObject;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all project types (keeps base types by default).
|
||||
*
|
||||
* @param bool $includeBaseTypes If true, also clears base types
|
||||
*/
|
||||
public function clear(bool $includeBaseTypes = false): void
|
||||
{
|
||||
$this->types = [];
|
||||
if ($includeBaseTypes) {
|
||||
$this->baseTypes = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize base types that are shared across all schemas.
|
||||
*
|
||||
* @param array<string, Type> $types
|
||||
*/
|
||||
public function initBaseTypes(array $types): void
|
||||
{
|
||||
foreach ($types as $name => $type) {
|
||||
$this->baseTypes[$name] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered types (excluding base types).
|
||||
*
|
||||
* @return array<string, Type>
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all base types.
|
||||
*
|
||||
* @return array<string, Type>
|
||||
*/
|
||||
public function getBaseTypes(): array
|
||||
{
|
||||
return $this->baseTypes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ namespace Appwrite\Messaging\Adapter;
|
||||
|
||||
use Appwrite\Messaging\Adapter as MessagingAdapter;
|
||||
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
|
||||
use Appwrite\Utopia\Database\RuntimeQuery;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class Realtime extends MessagingAdapter
|
||||
{
|
||||
@@ -51,9 +54,10 @@ class Realtime extends MessagingAdapter
|
||||
* @param mixed $identifier
|
||||
* @param array $roles
|
||||
* @param array $channels
|
||||
* @param array $queries
|
||||
* @return void
|
||||
*/
|
||||
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels): void
|
||||
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels, array $queries = []): void
|
||||
{
|
||||
if (!isset($this->subscriptions[$projectId])) { // Init Project
|
||||
$this->subscriptions[$projectId] = [];
|
||||
@@ -72,7 +76,8 @@ class Realtime extends MessagingAdapter
|
||||
$this->connections[$identifier] = [
|
||||
'projectId' => $projectId,
|
||||
'roles' => $roles,
|
||||
'channels' => $channels
|
||||
'channels' => $channels,
|
||||
'queries' => $queries
|
||||
];
|
||||
}
|
||||
|
||||
@@ -206,7 +211,14 @@ class Realtime extends MessagingAdapter
|
||||
/**
|
||||
* To prevent duplicates, we save the connections as array keys.
|
||||
*/
|
||||
$receivers[$id] = 0;
|
||||
$queries = $this->connections[$id]['queries'] ?? [];
|
||||
$payload = $event['data']['payload'] ?? [];
|
||||
if (
|
||||
empty($queries) ||
|
||||
!empty(RuntimeQuery::filter($queries, $payload))
|
||||
) {
|
||||
$receivers[$id] = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -245,6 +257,34 @@ class Realtime extends MessagingAdapter
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the queries from the Query Params into an array.
|
||||
* @param array $queries
|
||||
* @return array
|
||||
*/
|
||||
public static function convertQueries(array $queries): array
|
||||
{
|
||||
$queries = Query::parseQueries($queries);
|
||||
$stack = $queries;
|
||||
$allowedMethods = implode(', ', RuntimeQuery::ALLOWED_QUERIES);
|
||||
while (!empty($stack)) {
|
||||
/** `@var` Query $query */
|
||||
$query = array_pop($stack);
|
||||
$method = $query->getMethod();
|
||||
if (!in_array($method, RuntimeQuery::ALLOWED_QUERIES, true)) {
|
||||
$unsupportedMethod = $method;
|
||||
throw new QueryException(
|
||||
"Query method '{$unsupportedMethod}' is not supported in Realtime queries. Allowed query methods are: {$allowedMethods}"
|
||||
);
|
||||
}
|
||||
if (in_array($method, [Query::TYPE_AND, Query::TYPE_OR], true)) {
|
||||
$stack = array_merge($stack, $query->getValues());
|
||||
}
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create channels array based on the event name and payload.
|
||||
*
|
||||
|
||||
@@ -90,6 +90,7 @@ abstract class Migration
|
||||
'1.7.3' => 'V22',
|
||||
'1.7.4' => 'V22',
|
||||
'1.8.0' => 'V23',
|
||||
'1.8.1' => 'V23',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -99,8 +100,6 @@ abstract class Migration
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
Authorization::disable();
|
||||
Authorization::setDefaultStatus(false);
|
||||
|
||||
$this->collections = Config::getParam('collections', []);
|
||||
|
||||
@@ -128,6 +127,7 @@ abstract class Migration
|
||||
Document $project,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
?callable $getProjectDB = null
|
||||
): self {
|
||||
$this->project = $project;
|
||||
@@ -135,6 +135,9 @@ abstract class Migration
|
||||
$this->dbForPlatform = $dbForPlatform;
|
||||
$this->getProjectDB = $getProjectDB;
|
||||
|
||||
$authorization->disable();
|
||||
$authorization->setDefaultStatus(false);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class V23 extends Migration
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("Failed to migration error attribute size in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'buckets':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'transformations');
|
||||
@@ -148,6 +148,21 @@ class V23 extends Migration
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
case 'users':
|
||||
$attributes = [
|
||||
'emailCanonical',
|
||||
'emailIsFree',
|
||||
'emailIsDisposable',
|
||||
'emailIsCorporate',
|
||||
'emailIsCanonical',
|
||||
];
|
||||
try {
|
||||
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform;
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Coroutine as Co;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
@@ -161,50 +159,4 @@ class Action extends UtopiaAction
|
||||
Console::info("[" . DateTime::now() . "] " . $method . ' ' . $type . ' ' . $project->getSequence() . ' ' . $project->getId() . ' ' . $collectionId . ' ' . $log);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper to apply (request) select queries to response model.
|
||||
*
|
||||
* This prevents default values of rules to be presnet for not-selected attributes
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Document $document
|
||||
* @return void
|
||||
*/
|
||||
public function applySelectQueries(Request $request, Response $response, string $model): void
|
||||
{
|
||||
$queries = $request->getParam('queries', []);
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
$selectQueries = Query::groupByType($queries)['selections'] ?? [];
|
||||
|
||||
// No select queries means no filtering out
|
||||
if (empty($selectQueries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$attributes = [];
|
||||
foreach ($selectQueries as $query) {
|
||||
foreach ($query->getValues() as $attribute) {
|
||||
$attributes[] = $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
// found a wildcard, return!
|
||||
if (\in_array('*', $attributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$responseModel = $response->getModel($model);
|
||||
foreach ($responseModel->getRules() as $ruleName => $rule) {
|
||||
if (\str_starts_with($ruleName, '$')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\in_array($ruleName, $attributes)) {
|
||||
$responseModel->removeRule($ruleName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
namespace Appwrite\Platform;
|
||||
|
||||
use Appwrite\Platform\Modules\Account;
|
||||
use Appwrite\Platform\Modules\Avatars;
|
||||
use Appwrite\Platform\Modules\Console;
|
||||
use Appwrite\Platform\Modules\Core;
|
||||
use Appwrite\Platform\Modules\Databases;
|
||||
use Appwrite\Platform\Modules\Functions;
|
||||
use Appwrite\Platform\Modules\Health;
|
||||
use Appwrite\Platform\Modules\Projects;
|
||||
use Appwrite\Platform\Modules\Proxy;
|
||||
use Appwrite\Platform\Modules\Sites;
|
||||
@@ -20,9 +22,11 @@ class Appwrite extends Platform
|
||||
{
|
||||
parent::__construct(new Core());
|
||||
$this->addModule(new Account\Module());
|
||||
$this->addModule(new Avatars\Module());
|
||||
$this->addModule(new Databases\Module());
|
||||
$this->addModule(new Projects\Module());
|
||||
$this->addModule(new Functions\Module());
|
||||
$this->addModule(new Health\Module());
|
||||
$this->addModule(new Sites\Module());
|
||||
$this->addModule(new Console\Module());
|
||||
$this->addModule(new Proxy\Module());
|
||||
|
||||
@@ -17,8 +17,8 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use libphonenumber\NumberParseException;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Auth\Proofs\Code as ProofsCode;
|
||||
use Utopia\Auth\Proofs\Token as ProofsToken;
|
||||
use Utopia\Database\Database;
|
||||
@@ -196,26 +196,21 @@ class Create extends Action
|
||||
->setRecipients([$phone])
|
||||
->setProviderType(MESSAGE_TYPE_SMS);
|
||||
|
||||
if (isset($plan['authPhone'])) {
|
||||
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
|
||||
$timelimit
|
||||
->setParam('{organizationId}', $project->getAttribute('teamId'));
|
||||
$helper = PhoneNumberUtil::getInstance();
|
||||
try {
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
$abuse = new Abuse($timelimit);
|
||||
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
|
||||
$helper = PhoneNumberUtil::getInstance();
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
} catch (NumberParseException $e) {
|
||||
// Ignore invalid phone number for country code stats
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
break;
|
||||
case Type::EMAIL:
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action as PlatformAction;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Throwable;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Logger\Logger;
|
||||
|
||||
class Action extends PlatformAction
|
||||
{
|
||||
protected function getAppRoot(): string
|
||||
{
|
||||
return \dirname(__DIR__, 6);
|
||||
}
|
||||
|
||||
protected function avatar(string $type, string $code, int $width, int $height, int $quality, Response $response): void
|
||||
{
|
||||
$code = \strtolower($code);
|
||||
$type = \strtolower($type);
|
||||
$set = Config::getParam('avatar-' . $type, []);
|
||||
|
||||
if (empty($set)) {
|
||||
throw new Exception(Exception::AVATAR_SET_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!\array_key_exists($code, $set)) {
|
||||
throw new Exception(Exception::AVATAR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!\extension_loaded('imagick')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
|
||||
}
|
||||
|
||||
$output = 'png';
|
||||
$path = $set[$code]['path'];
|
||||
$type = 'png';
|
||||
|
||||
if (!\is_readable($path)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'File not readable in ' . $path);
|
||||
}
|
||||
|
||||
$image = new Image(\file_get_contents($path));
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
->setContentType('image/png')
|
||||
->file($data);
|
||||
unset($image);
|
||||
}
|
||||
|
||||
protected function getUserGitHub(string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, ?Logger $logger, Authorization $authorization): array
|
||||
{
|
||||
try {
|
||||
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
|
||||
$gitHubSession = null;
|
||||
foreach ($sessions as $session) {
|
||||
if ($session->getAttribute('provider', '') === 'github') {
|
||||
$gitHubSession = $session;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($gitHubSession)) {
|
||||
throw new Exception(Exception::USER_SESSION_NOT_FOUND, 'GitHub session not found.');
|
||||
}
|
||||
|
||||
$provider = $gitHubSession->getAttribute('provider', '');
|
||||
$accessToken = $gitHubSession->getAttribute('providerAccessToken');
|
||||
$accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry');
|
||||
$refreshToken = $gitHubSession->getAttribute('providerRefreshToken');
|
||||
|
||||
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
|
||||
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
|
||||
|
||||
$oAuthProviders = Config::getParam('oAuthProviders');
|
||||
$className = $oAuthProviders[$provider]['class'];
|
||||
if (!\class_exists($className)) {
|
||||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$oauth2 = new $className($appId, $appSecret, '', [], []);
|
||||
|
||||
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
|
||||
if ($isExpired) {
|
||||
try {
|
||||
$oauth2->refreshTokens($refreshToken);
|
||||
|
||||
$accessToken = $oauth2->getAccessToken('');
|
||||
$refreshToken = $oauth2->getRefreshToken('');
|
||||
|
||||
$verificationId = $oauth2->getUserID($accessToken);
|
||||
|
||||
if (empty($verificationId)) {
|
||||
throw new \Exception("Locked tokens."); // Race codition, handeled in catch
|
||||
}
|
||||
|
||||
$gitHubSession
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
|
||||
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession));
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
} catch (Throwable $err) {
|
||||
$index = 0;
|
||||
do {
|
||||
$previousAccessToken = $gitHubSession->getAttribute('providerAccessToken');
|
||||
|
||||
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
|
||||
$gitHubSession = new Document();
|
||||
foreach ($sessions as $session) {
|
||||
if ($session->getAttribute('provider', '') === 'github') {
|
||||
$gitHubSession = $session;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$accessToken = $gitHubSession->getAttribute('providerAccessToken');
|
||||
|
||||
if ($accessToken !== $previousAccessToken) {
|
||||
break;
|
||||
}
|
||||
|
||||
$index++;
|
||||
\usleep(500000);
|
||||
} while ($index < 10);
|
||||
}
|
||||
}
|
||||
|
||||
$oauth2 = new $className($appId, $appSecret, '', [], []);
|
||||
$githubUser = $oauth2->getUserSlug($accessToken);
|
||||
$githubId = $oauth2->getUserID($accessToken);
|
||||
|
||||
return [
|
||||
'name' => $githubUser,
|
||||
'id' => $githubId
|
||||
];
|
||||
} catch (Exception $error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Browsers;
|
||||
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getBrowser';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/avatars/browsers/:code')
|
||||
->desc('Get browser icon')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resource', 'avatar/browser')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'avatars',
|
||||
group: null,
|
||||
name: 'getBrowser',
|
||||
description: '/docs/references/avatars/get-browser.md',
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
type: MethodType::LOCATION,
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::IMAGE_PNG
|
||||
))
|
||||
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.')
|
||||
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $code, int $width, int $height, int $quality, Response $response)
|
||||
{
|
||||
$this->avatar('browsers', $code, $width, $height, $quality, $response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Back;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Imagick;
|
||||
use ImagickDraw;
|
||||
use ImagickPixel;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getCloudCardBack';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/cards/cloud-back')
|
||||
->desc('Get back Of Cloud Card')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'cards/cloud-back')
|
||||
->label('cache.resource', 'card-back/{request.userId}')
|
||||
->label('docs', false)
|
||||
->label('origin', '*')
|
||||
->param('userId', '', new UID(), 'User ID.', true)
|
||||
->param('mock', '', new WhiteList(['golden', 'normal', 'platinum']), 'Mocking behaviour.', true)
|
||||
->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true)
|
||||
->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('response')
|
||||
->inject('heroes')
|
||||
->inject('contributors')
|
||||
->inject('employees')
|
||||
->inject('logger')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization)
|
||||
{
|
||||
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
|
||||
|
||||
if ($user->isEmpty() && empty($mock)) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$mock) {
|
||||
$userId = $user->getId();
|
||||
$email = $user->getAttribute('email', '');
|
||||
|
||||
$gitHub = $this->getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization);
|
||||
$githubId = $gitHub['id'] ?? '';
|
||||
|
||||
$isHero = \array_key_exists($email, $heroes);
|
||||
$isContributor = \in_array($githubId, $contributors);
|
||||
$isEmployee = \array_key_exists($email, $employees);
|
||||
|
||||
$isGolden = $isEmployee || $isHero || $isContributor;
|
||||
$isPlatinum = $user->getSequence() % 100 === 0;
|
||||
} else {
|
||||
$userId = '63e0bcf3c3eb803ba530';
|
||||
|
||||
$isGolden = $mock === 'golden';
|
||||
$isPlatinum = $mock === 'platinum';
|
||||
}
|
||||
|
||||
$userId = 'UID ' . $userId;
|
||||
|
||||
$isPlatinum = $isGolden ? false : $isPlatinum;
|
||||
|
||||
$imagePath = $isGolden ? 'back-golden.png' : ($isPlatinum ? 'back-platinum.png' : 'back.png');
|
||||
|
||||
$baseImage = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $imagePath);
|
||||
|
||||
setlocale(LC_ALL, "en_US.utf8");
|
||||
// $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId);
|
||||
|
||||
$text = new ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont($this->getAppRoot() . '/public/fonts/SourceCodePro-Regular.ttf');
|
||||
$text->setFillColor(new ImagickPixel($isGolden ? '#664A1E' : ($isPlatinum ? '#555555' : '#E8E9F0')));
|
||||
$text->setFontSize(28);
|
||||
$text->setFontWeight(400);
|
||||
$baseImage->annotateImage($text, 512, 596, 0, $userId);
|
||||
|
||||
if (!empty($width) || !empty($height)) {
|
||||
$baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
|
||||
->setContentType('image/png')
|
||||
->file($baseImage->getImageBlob());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Front;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Imagick;
|
||||
use ImagickDraw;
|
||||
use ImagickPixel;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getCloudCard';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/cards/cloud')
|
||||
->desc('Get front Of Cloud Card')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'cards/cloud')
|
||||
->label('cache.resource', 'card/{request.userId}')
|
||||
->label('docs', false)
|
||||
->label('origin', '*')
|
||||
->param('userId', '', new UID(), 'User ID.', true)
|
||||
->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long']), 'Mocking behaviour.', true)
|
||||
->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true)
|
||||
->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('response')
|
||||
->inject('heroes')
|
||||
->inject('contributors')
|
||||
->inject('employees')
|
||||
->inject('logger')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization)
|
||||
{
|
||||
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
|
||||
|
||||
if ($user->isEmpty() && empty($mock)) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$mock) {
|
||||
$name = $user->getAttribute('name', 'Anonymous');
|
||||
$email = $user->getAttribute('email', '');
|
||||
$createdAt = new \DateTime($user->getCreatedAt());
|
||||
|
||||
$gitHub = $this->getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization);
|
||||
$githubName = $gitHub['name'] ?? '';
|
||||
$githubId = $gitHub['id'] ?? '';
|
||||
|
||||
$isHero = \array_key_exists($email, $heroes);
|
||||
$isContributor = \in_array($githubId, $contributors);
|
||||
$isEmployee = \array_key_exists($email, $employees);
|
||||
$employeeNumber = $isEmployee ? $employees[$email]['spot'] : '';
|
||||
|
||||
if ($isHero) {
|
||||
$createdAt = new \DateTime($heroes[$email]['memberSince'] ?? '');
|
||||
} elseif ($isEmployee) {
|
||||
$createdAt = new \DateTime($employees[$email]['memberSince'] ?? '');
|
||||
}
|
||||
|
||||
if (!$isEmployee && !empty($githubName)) {
|
||||
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees));
|
||||
if (!empty($employeeGitHub)) {
|
||||
$isEmployee = true;
|
||||
$employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : '';
|
||||
$createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$isPlatinum = $user->getSequence() % 100 === 0;
|
||||
} else {
|
||||
$name = $mock === 'normal-long' ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian';
|
||||
$createdAt = new \DateTime('now');
|
||||
$githubName = $mock === 'normal-no-github' ? '' : ($mock === 'normal-long' ? 'sir-first-walterobrian-junior' : 'walterobrian');
|
||||
$isHero = $mock === 'hero';
|
||||
$isContributor = $mock === 'contributor';
|
||||
$isEmployee = \str_starts_with($mock, 'employee');
|
||||
$employeeNumber = match ($mock) {
|
||||
'employee' => '1',
|
||||
'employee-2digit' => '18',
|
||||
default => ''
|
||||
};
|
||||
|
||||
$isPlatinum = $mock === 'platinum';
|
||||
}
|
||||
|
||||
if ($isEmployee) {
|
||||
$isContributor = false;
|
||||
$isHero = false;
|
||||
}
|
||||
|
||||
if ($isHero) {
|
||||
$isContributor = false;
|
||||
$isEmployee = false;
|
||||
}
|
||||
|
||||
if ($isContributor) {
|
||||
$isHero = false;
|
||||
$isEmployee = false;
|
||||
}
|
||||
|
||||
$isGolden = $isEmployee || $isHero || $isContributor;
|
||||
$isPlatinum = $isGolden ? false : $isPlatinum;
|
||||
$memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o'));
|
||||
|
||||
$imagePath = $isGolden ? 'front-golden.png' : ($isPlatinum ? 'front-platinum.png' : 'front.png');
|
||||
|
||||
$baseImage = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $imagePath);
|
||||
|
||||
if ($isEmployee) {
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/employee.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 35);
|
||||
|
||||
$text = new ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
|
||||
$text->setFillColor(new ImagickPixel('#FFFADF'));
|
||||
$text->setFontSize(\strlen($employeeNumber) <= 2 ? 54 : 48);
|
||||
$text->setFontWeight(700);
|
||||
$metricsText = $baseImage->queryFontMetrics($text, $employeeNumber);
|
||||
|
||||
$hashtag = new ImagickDraw();
|
||||
$hashtag->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$hashtag->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
|
||||
$hashtag->setFillColor(new ImagickPixel('#FFFADF'));
|
||||
$hashtag->setFontSize(28);
|
||||
$hashtag->setFontWeight(700);
|
||||
$metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#');
|
||||
|
||||
$startX = 898;
|
||||
$totalWidth = $metricsHashtag['textWidth'] + 12 + $metricsText['textWidth'];
|
||||
|
||||
$hashtagX = ($metricsHashtag['textWidth'] / 2);
|
||||
$textX = $hashtagX + 12 + ($metricsText['textWidth'] / 2);
|
||||
|
||||
$hashtagX -= $totalWidth / 2;
|
||||
$textX -= $totalWidth / 2;
|
||||
|
||||
$hashtagX += $startX;
|
||||
$textX += $startX;
|
||||
|
||||
$baseImage->annotateImage($hashtag, $hashtagX, 150, 0, '#');
|
||||
$baseImage->annotateImage($text, $textX, 150, 0, $employeeNumber);
|
||||
}
|
||||
|
||||
if ($isContributor) {
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/contributor.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34);
|
||||
}
|
||||
|
||||
if ($isHero) {
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/hero.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34);
|
||||
}
|
||||
|
||||
setlocale(LC_ALL, "en_US.utf8");
|
||||
// $name = \iconv("utf-8", "ascii//TRANSLIT", $name);
|
||||
// $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince);
|
||||
// $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName);
|
||||
|
||||
$text = new ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
|
||||
$text->setFillColor(new ImagickPixel('#FFFFFF'));
|
||||
|
||||
if (\strlen($name) > 32) {
|
||||
$name = \substr($name, 0, 32);
|
||||
}
|
||||
|
||||
if (\strlen($name) <= 23) {
|
||||
$text->setFontSize(80);
|
||||
$scalingDown = false;
|
||||
} else {
|
||||
$text->setFontSize(54);
|
||||
$scalingDown = true;
|
||||
}
|
||||
$text->setFontWeight(700);
|
||||
$baseImage->annotateImage($text, 512, 477, 0, $name);
|
||||
|
||||
$text = new ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-SemiBold.ttf');
|
||||
$text->setFillColor(new ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC'));
|
||||
$text->setFontSize(27);
|
||||
$text->setFontWeight(600);
|
||||
$text->setTextKerning(1.08);
|
||||
$baseImage->annotateImage($text, 512, 541, 0, \strtoupper($memberSince));
|
||||
|
||||
if (!empty($githubName)) {
|
||||
$text = new ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Regular.ttf');
|
||||
$text->setFillColor(new ImagickPixel('#FFFFFF'));
|
||||
$text->setFontSize($scalingDown ? 28 : 32);
|
||||
$text->setFontWeight(400);
|
||||
$metrics = $baseImage->queryFontMetrics($text, $githubName);
|
||||
|
||||
$baseImage->annotateImage($text, 512 + 20 + 4, 373 + ($scalingDown ? 2 : 0), 0, $githubName);
|
||||
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$precisionFix = 5;
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2) - 20 - 4, 373 - ($metrics['textHeight'] - $precisionFix));
|
||||
}
|
||||
|
||||
if (!empty($width) || !empty($height)) {
|
||||
$baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
|
||||
->setContentType('image/png')
|
||||
->file($baseImage->getImageBlob());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\OG;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Imagick;
|
||||
use ImagickDraw;
|
||||
use ImagickPixel;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getCloudCardOG';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/cards/cloud-og')
|
||||
->desc('Get OG image From Cloud Card')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'cards/cloud-og')
|
||||
->label('cache.resource', 'card-og/{request.userId}')
|
||||
->label('docs', false)
|
||||
->label('origin', '*')
|
||||
->param('userId', '', new UID(), 'User ID.', true)
|
||||
->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long', 'normal-long-right', 'normal-long-middle', 'normal-bg2', 'normal-bg3', 'normal-right', 'normal-middle', 'platinum-right', 'platinum-middle', 'hero-middle', 'hero-right', 'contributor-right', 'employee-right', 'contributor-middle', 'employee-middle', 'employee-2digit-middle', 'employee-2digit-right']), 'Mocking behaviour.', true)
|
||||
->param('width', 0, new Range(0, 1024), 'Resize image card width, Pass an integer between 0 to 1024.', true)
|
||||
->param('height', 0, new Range(0, 1024), 'Resize image card height, Pass an integer between 0 to 1024.', true)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('response')
|
||||
->inject('heroes')
|
||||
->inject('contributors')
|
||||
->inject('employees')
|
||||
->inject('logger')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger, Authorization $authorization)
|
||||
{
|
||||
$user = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $userId));
|
||||
|
||||
if ($user->isEmpty() && empty($mock)) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$mock) {
|
||||
$sequence = $user->getSequence();
|
||||
$bgVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3');
|
||||
$cardVariation = $sequence % 3 === 0 ? '1' : ($sequence % 3 === 1 ? '2' : '3');
|
||||
|
||||
$name = $user->getAttribute('name', 'Anonymous');
|
||||
$email = $user->getAttribute('email', '');
|
||||
$createdAt = new \DateTime($user->getCreatedAt());
|
||||
|
||||
$gitHub = $this->getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger, $authorization);
|
||||
$githubName = $gitHub['name'] ?? '';
|
||||
$githubId = $gitHub['id'] ?? '';
|
||||
|
||||
$isHero = \array_key_exists($email, $heroes);
|
||||
$isContributor = \in_array($githubId, $contributors);
|
||||
$isEmployee = \array_key_exists($email, $employees);
|
||||
$employeeNumber = $isEmployee ? $employees[$email]['spot'] : '';
|
||||
|
||||
if ($isHero) {
|
||||
$createdAt = new \DateTime($heroes[$email]['memberSince'] ?? '');
|
||||
} elseif ($isEmployee) {
|
||||
$createdAt = new \DateTime($employees[$email]['memberSince'] ?? '');
|
||||
}
|
||||
|
||||
if (!$isEmployee && !empty($githubName)) {
|
||||
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees));
|
||||
if (!empty($employeeGitHub)) {
|
||||
$isEmployee = true;
|
||||
$employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : '';
|
||||
$createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$isPlatinum = $user->getSequence() % 100 === 0;
|
||||
} else {
|
||||
$bgVariation = \str_ends_with($mock, '-bg2') ? '2' : (\str_ends_with($mock, '-bg3') ? '3' : '1');
|
||||
$cardVariation = \str_ends_with($mock, '-right') ? '2' : (\str_ends_with($mock, '-middle') ? '3' : '1');
|
||||
$name = \str_starts_with($mock, 'normal-long') ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian';
|
||||
$createdAt = new \DateTime('now');
|
||||
$githubName = $mock === 'normal-no-github' ? '' : (\str_starts_with($mock, 'normal-long') ? 'sir-first-walterobrian-junior' : 'walterobrian');
|
||||
$isHero = \str_starts_with($mock, 'hero');
|
||||
$isContributor = \str_starts_with($mock, 'contributor');
|
||||
$isEmployee = \str_starts_with($mock, 'employee');
|
||||
$employeeNumber = match ($mock) {
|
||||
'employee' => '1',
|
||||
'employee-right' => '1',
|
||||
'employee-middle' => '1',
|
||||
'employee-2digit' => '18',
|
||||
'employee-2digit-right' => '18',
|
||||
'employee-2digit-middle' => '18',
|
||||
default => ''
|
||||
};
|
||||
|
||||
$isPlatinum = \str_starts_with($mock, 'platinum');
|
||||
}
|
||||
|
||||
if ($isEmployee) {
|
||||
$isContributor = false;
|
||||
$isHero = false;
|
||||
}
|
||||
|
||||
if ($isHero) {
|
||||
$isContributor = false;
|
||||
$isEmployee = false;
|
||||
}
|
||||
|
||||
if ($isContributor) {
|
||||
$isHero = false;
|
||||
$isEmployee = false;
|
||||
}
|
||||
|
||||
$isGolden = $isEmployee || $isHero || $isContributor;
|
||||
$isPlatinum = $isGolden ? false : $isPlatinum;
|
||||
$memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o'));
|
||||
|
||||
$baseImage = new Imagick($this->getAppRoot() . "/public/images/cards/cloud/og-background{$bgVariation}.png");
|
||||
|
||||
$cardType = $isGolden ? '-golden' : ($isPlatinum ? '-platinum' : '');
|
||||
|
||||
$image = new Imagick($this->getAppRoot() . "/public/images/cards/cloud/og-card{$cardType}{$cardVariation}.png");
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 1008 / 2 - $image->getImageWidth() / 2, 1008 / 2 - $image->getImageHeight() / 2);
|
||||
|
||||
$imageLogo = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/og-background-logo.png');
|
||||
$imageShadow = new Imagick($this->getAppRoot() . "/public/images/cards/cloud/og-shadow{$cardType}.png");
|
||||
if ($cardVariation === '1') {
|
||||
$baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 32, 1008 - $imageLogo->getImageHeight() - 32);
|
||||
$baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -450, 700);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32);
|
||||
$baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -20, 710);
|
||||
} else {
|
||||
$baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32);
|
||||
$baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -135, 710);
|
||||
}
|
||||
|
||||
if ($isEmployee) {
|
||||
$file = $cardVariation === '3' ? 'employee-skew.png' : 'employee.png';
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $file);
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
|
||||
$hashtag = new ImagickDraw();
|
||||
$hashtag->setTextAlignment(Imagick::ALIGN_LEFT);
|
||||
$hashtag->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
|
||||
$hashtag->setFillColor(new ImagickPixel('#FFFADF'));
|
||||
$hashtag->setFontSize(20);
|
||||
$hashtag->setFontWeight(700);
|
||||
|
||||
$text = new ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_LEFT);
|
||||
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
|
||||
$text->setFillColor(new ImagickPixel('#FFFADF'));
|
||||
$text->setFontSize(\strlen($employeeNumber) <= 1 ? 36 : 28);
|
||||
$text->setFontWeight(700);
|
||||
|
||||
if ($cardVariation === '3') {
|
||||
$hashtag->setFontSize(16);
|
||||
$text->setFontSize(\strlen($employeeNumber) <= 1 ? 30 : 26);
|
||||
|
||||
$hashtag->skewY(20);
|
||||
$hashtag->skewX(20);
|
||||
$text->skewY(20);
|
||||
$text->skewX(20);
|
||||
}
|
||||
|
||||
$metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#');
|
||||
$metricsText = $baseImage->queryFontMetrics($text, $employeeNumber);
|
||||
|
||||
$group = new Imagick();
|
||||
$groupWidth = $metricsHashtag['textWidth'] + 6 + $metricsText['textWidth'];
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$group->newImage($groupWidth, $metricsText['textHeight'], '#00000000');
|
||||
$group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#');
|
||||
$group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber);
|
||||
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), -20);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203);
|
||||
|
||||
$group->rotateImage(new ImagickPixel('#00000000'), -22);
|
||||
|
||||
if (\strlen($employeeNumber) <= 1) {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 660, 245);
|
||||
} else {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 655, 247);
|
||||
}
|
||||
} elseif ($cardVariation === '2') {
|
||||
$group->newImage($groupWidth, $metricsText['textHeight'], '#00000000');
|
||||
$group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#');
|
||||
$group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber);
|
||||
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), 30);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425);
|
||||
|
||||
$group->rotateImage(new ImagickPixel('#00000000'), 32);
|
||||
|
||||
if (\strlen($employeeNumber) <= 1) {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 775, 465);
|
||||
} else {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 767, 470);
|
||||
}
|
||||
} else {
|
||||
$group->newImage(300, 300, '#00000000');
|
||||
|
||||
$hashtag->annotation(0, $metricsText['textHeight'], '#');
|
||||
$text->annotation($metricsHashtag['textWidth'] + 2, $metricsText['textHeight'], $employeeNumber);
|
||||
|
||||
$group->drawImage($hashtag);
|
||||
$group->drawImage($text);
|
||||
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293);
|
||||
|
||||
if (\strlen($employeeNumber) <= 1) {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 670, 317);
|
||||
} else {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 663, 322);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isContributor) {
|
||||
$file = $cardVariation === '3' ? 'contributor-skew.png' : 'contributor.png';
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $file);
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), -20);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), 30);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425);
|
||||
} else {
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isHero) {
|
||||
$file = $cardVariation === '3' ? 'hero-skew.png' : 'hero.png';
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/' . $file);
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), -20);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), 30);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425);
|
||||
} else {
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293);
|
||||
}
|
||||
}
|
||||
|
||||
setlocale(LC_ALL, "en_US.utf8");
|
||||
// $name = \iconv("utf-8", "ascii//TRANSLIT", $name);
|
||||
// $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince);
|
||||
// $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName);
|
||||
|
||||
$textName = new ImagickDraw();
|
||||
$textName->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$textName->setFont($this->getAppRoot() . '/public/fonts/Inter-Bold.ttf');
|
||||
$textName->setFillColor(new ImagickPixel('#FFFFFF'));
|
||||
|
||||
if (\strlen($name) > 32) {
|
||||
$name = \substr($name, 0, 32);
|
||||
}
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
if (\strlen($name) <= 23) {
|
||||
$scalingDown = false;
|
||||
$textName->setFontSize(54);
|
||||
} else {
|
||||
$scalingDown = true;
|
||||
$textName->setFontSize(36);
|
||||
}
|
||||
} elseif ($cardVariation === '2') {
|
||||
if (\strlen($name) <= 23) {
|
||||
$scalingDown = false;
|
||||
$textName->setFontSize(50);
|
||||
} else {
|
||||
$scalingDown = true;
|
||||
$textName->setFontSize(34);
|
||||
}
|
||||
} else {
|
||||
if (\strlen($name) <= 23) {
|
||||
$scalingDown = false;
|
||||
$textName->setFontSize(44);
|
||||
} else {
|
||||
$scalingDown = true;
|
||||
$textName->setFontSize(32);
|
||||
}
|
||||
}
|
||||
|
||||
$textName->setFontWeight(700);
|
||||
|
||||
$textMember = new ImagickDraw();
|
||||
$textMember->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$textMember->setFont($this->getAppRoot() . '/public/fonts/Inter-Medium.ttf');
|
||||
$textMember->setFillColor(new ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC'));
|
||||
$textMember->setFontWeight(500);
|
||||
$textMember->setTextKerning(1.12);
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$textMember->setFontSize(21);
|
||||
|
||||
$baseImage->annotateImage($textName, 550, 600, -22, $name);
|
||||
$baseImage->annotateImage($textMember, 585, 635, -22, $memberSince);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$textMember->setFontSize(20);
|
||||
|
||||
$baseImage->annotateImage($textName, 435, 590, 31.37, $name);
|
||||
$baseImage->annotateImage($textMember, 412, 628, 31.37, $memberSince);
|
||||
} else {
|
||||
$textMember->setFontSize(16);
|
||||
|
||||
$textName->skewY(20);
|
||||
$textName->skewX(20);
|
||||
$textName->annotation(320, 700, $name);
|
||||
|
||||
$textMember->skewY(20);
|
||||
$textMember->skewX(20);
|
||||
$textMember->annotation(330, 735, $memberSince);
|
||||
|
||||
$baseImage->drawImage($textName);
|
||||
$baseImage->drawImage($textMember);
|
||||
}
|
||||
|
||||
if (!empty($githubName)) {
|
||||
$text = new ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_LEFT);
|
||||
$text->setFont($this->getAppRoot() . '/public/fonts/Inter-Regular.ttf');
|
||||
$text->setFillColor(new ImagickPixel('#FFFFFF'));
|
||||
$text->setFontSize($scalingDown ? 16 : 20);
|
||||
$text->setFontWeight(400);
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$metrics = $baseImage->queryFontMetrics($text, $githubName);
|
||||
|
||||
$group = new Imagick();
|
||||
$groupWidth = $metrics['textWidth'] + 32 + 4;
|
||||
$group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000');
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1);
|
||||
$precisionFix = -1;
|
||||
|
||||
$group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0);
|
||||
$group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName);
|
||||
|
||||
$group->rotateImage(new ImagickPixel('#00000000'), -22);
|
||||
$x = 510 - $group->getImageWidth() / 2;
|
||||
$y = 530 - $group->getImageHeight() / 2;
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$metrics = $baseImage->queryFontMetrics($text, $githubName);
|
||||
|
||||
$group = new Imagick();
|
||||
$groupWidth = $metrics['textWidth'] + 32 + 4;
|
||||
$group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000');
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1);
|
||||
$precisionFix = -1;
|
||||
|
||||
$group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0);
|
||||
$group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName);
|
||||
|
||||
$group->rotateImage(new ImagickPixel('#00000000'), 31.11);
|
||||
$x = 485 - $group->getImageWidth() / 2;
|
||||
$y = 530 - $group->getImageHeight() / 2;
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y);
|
||||
} else {
|
||||
$text->skewY(20);
|
||||
$text->skewX(20);
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
|
||||
$text->annotation(320 + 15 + 2, 640, $githubName);
|
||||
$metrics = $baseImage->queryFontMetrics($text, $githubName);
|
||||
|
||||
$image = new Imagick($this->getAppRoot() . '/public/images/cards/cloud/github-skew.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2), 518 + \strlen($githubName) * 1.3);
|
||||
|
||||
$baseImage->drawImage($text);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($width) || !empty($height)) {
|
||||
$baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
|
||||
->setContentType('image/png')
|
||||
->file($baseImage->getImageBlob());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\CreditCards;
|
||||
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getCreditCard';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/avatars/credit-cards/:code')
|
||||
->desc('Get credit card icon')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resource', 'avatar/credit-card')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'avatars',
|
||||
group: null,
|
||||
name: 'getCreditCard',
|
||||
description: '/docs/references/avatars/get-credit-card.md',
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
type: MethodType::LOCATION,
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::IMAGE_PNG
|
||||
))
|
||||
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.')
|
||||
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $code, int $width, int $height, int $quality, Response $response)
|
||||
{
|
||||
$this->avatar('credit-cards', $code, $width, $height, $quality, $response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Favicon;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\URL\URL as URLParse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use enshrined\svgSanitize\Sanitizer as SvgSanitizer;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Fetch\Client;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\URL;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getFavicon';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/avatars/favicon')
|
||||
->desc('Get favicon')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resource', 'avatar/favicon')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'avatars',
|
||||
group: null,
|
||||
name: 'getFavicon',
|
||||
description: '/docs/references/avatars/get-favicon.md',
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
type: MethodType::LOCATION,
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::IMAGE
|
||||
))
|
||||
->param('url', '', new URL(['http', 'https']), 'Website URL which you want to fetch the favicon from.')
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $url, Response $response)
|
||||
{
|
||||
$width = 56;
|
||||
$height = 56;
|
||||
$quality = 80;
|
||||
$output = 'png';
|
||||
$type = 'png';
|
||||
|
||||
if (!\extension_loaded('imagick')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
|
||||
}
|
||||
|
||||
$domain = new Domain(\parse_url($url, PHP_URL_HOST));
|
||||
|
||||
if (!$domain->isKnown()) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
try {
|
||||
$res = $client
|
||||
->setAllowRedirects(true)
|
||||
->setMaxRedirects(5)
|
||||
->setUserAgent(\sprintf(
|
||||
APP_USERAGENT,
|
||||
System::getEnv('_APP_VERSION', 'UNKNOWN'),
|
||||
System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY))
|
||||
))
|
||||
->fetch($url);
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->strictErrorChecking = false;
|
||||
@$doc->loadHTML($res->getBody());
|
||||
|
||||
$links = $doc->getElementsByTagName('link') ?? [];
|
||||
$outputHref = '';
|
||||
$outputExt = '';
|
||||
$space = 0;
|
||||
|
||||
foreach ($links as $link) { /* @var $link DOMElement */
|
||||
$href = $link->getAttribute('href');
|
||||
$rel = $link->getAttribute('rel');
|
||||
$sizes = $link->getAttribute('sizes');
|
||||
$absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href)));
|
||||
|
||||
switch (\strtolower($rel)) {
|
||||
case 'icon':
|
||||
case 'shortcut icon':
|
||||
//case 'apple-touch-icon':
|
||||
$ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION);
|
||||
|
||||
switch ($ext) {
|
||||
case 'svg':
|
||||
// SVG icons are prioritized by assigning the maximum possible value.
|
||||
$space = PHP_INT_MAX;
|
||||
$outputHref = $absolute;
|
||||
$outputExt = $ext;
|
||||
break;
|
||||
case 'ico':
|
||||
case 'png':
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
$size = \explode('x', \strtolower($sizes));
|
||||
|
||||
$sizeWidth = (int) ($size[0] ?? 0);
|
||||
$sizeHeight = (int) ($size[1] ?? 0);
|
||||
|
||||
if (($sizeWidth * $sizeHeight) >= $space) {
|
||||
$space = $sizeWidth * $sizeHeight;
|
||||
$outputHref = $absolute;
|
||||
$outputExt = $ext;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($outputHref) || empty($outputExt)) {
|
||||
$default = \parse_url($url);
|
||||
|
||||
$outputHref = $default['scheme'] . '://' . $default['host'] . '/favicon.ico';
|
||||
$outputExt = 'ico';
|
||||
}
|
||||
|
||||
$domain = new Domain(\parse_url($outputHref, PHP_URL_HOST));
|
||||
|
||||
if (!$domain->isKnown()) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
try {
|
||||
$res = $client
|
||||
->setAllowRedirects(true)
|
||||
->setMaxRedirects(5)
|
||||
->fetch($outputHref);
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
if ($res->getStatusCode() !== 200) {
|
||||
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND);
|
||||
}
|
||||
|
||||
$data = $res->getBody();
|
||||
|
||||
if ('ico' === $outputExt) { // Skip crop, Imagick isn\'t supporting icon files
|
||||
if (
|
||||
empty($data) ||
|
||||
stripos($data, '<html') === 0 ||
|
||||
stripos($data, '<!doc') === 0
|
||||
) {
|
||||
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found');
|
||||
}
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
->setContentType('image/x-icon')
|
||||
->file($data);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('svg' === $outputExt) { // Skip crop, Imagick isn\'t supporting svg files
|
||||
$sanitizer = new SvgSanitizer();
|
||||
$sanitizer->minify(true);
|
||||
$cleanSvg = $sanitizer->sanitize($data);
|
||||
if ($cleanSvg === false) {
|
||||
throw new Exception(Exception::AVATAR_SVG_SANITIZATION_FAILED);
|
||||
}
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
->setContentType('image/svg+xml')
|
||||
->file($cleanSvg);
|
||||
return;
|
||||
}
|
||||
|
||||
$image = new Image($data);
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
->setContentType('image/png')
|
||||
->file($data);
|
||||
unset($image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Flags;
|
||||
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getFlag';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/avatars/flags/:code')
|
||||
->desc('Get country flag')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resource', 'avatar/flag')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'avatars',
|
||||
group: null,
|
||||
name: 'getFlag',
|
||||
description: '/docs/references/avatars/get-flag.md',
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
type: MethodType::LOCATION,
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::IMAGE_PNG
|
||||
))
|
||||
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.')
|
||||
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $code, int $width, int $height, int $quality, Response $response)
|
||||
{
|
||||
$this->avatar('flags', $code, $width, $height, $quality, $response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Image;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Fetch\Client;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\URL;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getImage';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/avatars/image')
|
||||
->desc('Get image from URL')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resource', 'avatar/image')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'avatars',
|
||||
group: null,
|
||||
name: 'getImage',
|
||||
description: '/docs/references/avatars/get-image.md',
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
type: MethodType::LOCATION,
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::IMAGE
|
||||
))
|
||||
->param('url', '', new URL(['http', 'https']), 'Image URL which you want to crop.')
|
||||
->param('width', 400, new Range(0, 2000), 'Resize preview image width, Pass an integer between 0 to 2000. Defaults to 400.', true)
|
||||
->param('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000. Defaults to 400.', true)
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $url, int $width, int $height, Response $response)
|
||||
{
|
||||
$quality = 80;
|
||||
$output = 'png';
|
||||
$type = 'png';
|
||||
|
||||
if (!\extension_loaded('imagick')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
|
||||
}
|
||||
|
||||
$domain = new Domain(\parse_url($url, PHP_URL_HOST));
|
||||
|
||||
if (!$domain->isKnown()) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
try {
|
||||
$res = $client
|
||||
->setAllowRedirects(false)
|
||||
->fetch($url);
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
if ($res->getStatusCode() !== 200) {
|
||||
throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$image = new Image($res->getBody());
|
||||
} catch (\Throwable $exception) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unable to parse image');
|
||||
}
|
||||
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
->setContentType('image/png')
|
||||
->file($data);
|
||||
unset($image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Initials;
|
||||
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Imagick;
|
||||
use ImagickDraw;
|
||||
use ImagickPixel;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\HexColor;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getInitials';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/avatars/initials')
|
||||
->desc('Get user initials')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache.resource', 'avatar/initials')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'avatars',
|
||||
group: null,
|
||||
name: 'getInitials',
|
||||
description: '/docs/references/avatars/get-initials.md',
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
type: MethodType::LOCATION,
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::IMAGE_PNG
|
||||
))
|
||||
->param('name', '', new Text(128), 'Full Name. When empty, current user name or email will be used. Max length: 128 chars.', true)
|
||||
->param('width', 500, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('height', 500, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
|
||||
->param('background', '', new HexColor(), 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true)
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $name, int $width, int $height, string $background, Response $response, Document $user)
|
||||
{
|
||||
$themes = [
|
||||
['background' => '#FD366E'], // Default (Pink)
|
||||
['background' => '#FE9567'], // Orange
|
||||
['background' => '#7C67FE'], // Purple
|
||||
['background' => '#68A3FE'], // Blue
|
||||
['background' => '#85DBD8'], // Mint
|
||||
];
|
||||
|
||||
$name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', ''));
|
||||
$words = \explode(' ', \strtoupper($name));
|
||||
// if there is no space, try to split by `_` underscore
|
||||
$words = (count($words) == 1) ? \explode('_', \strtoupper($name)) : $words;
|
||||
|
||||
$initials = '';
|
||||
$code = 0;
|
||||
|
||||
foreach ($words as $key => $w) {
|
||||
if (ctype_alnum($w[0] ?? '')) {
|
||||
$initials .= $w[0];
|
||||
$code += ord($w[0]);
|
||||
|
||||
if ($key == 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rand = \substr($code, -1);
|
||||
|
||||
$rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand;
|
||||
|
||||
$background = (!empty($background)) ? '#' . $background : $themes[$rand]['background'];
|
||||
|
||||
$image = new Imagick();
|
||||
$punch = new Imagick();
|
||||
$draw = new ImagickDraw();
|
||||
$fontSize = \min($width, $height) / 2;
|
||||
|
||||
$punch->newImage($width, $height, 'transparent');
|
||||
|
||||
$draw->setFont($this->getAppRoot() . '/app/assets/fonts/inter-v8-latin-regular.woff2');
|
||||
$image->setFont($this->getAppRoot() . '/app/assets/fonts/inter-v8-latin-regular.woff2');
|
||||
|
||||
$draw->setFillColor(new ImagickPixel('black'));
|
||||
$draw->setFontSize($fontSize);
|
||||
|
||||
$draw->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials);
|
||||
|
||||
$punch->drawImage($draw);
|
||||
$punch->negateImage(true, Imagick::CHANNEL_ALPHA);
|
||||
|
||||
$image->newImage($width, $height, $background);
|
||||
$image->setImageFormat("png");
|
||||
$image->compositeImage($punch, Imagick::COMPOSITE_COPYOPACITY, 0, 0);
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
|
||||
->setContentType('image/png')
|
||||
->file($image->getImageBlob());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\QR;
|
||||
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getQR';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/avatars/qr')
|
||||
->desc('Get QR code')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'avatars',
|
||||
group: null,
|
||||
name: 'getQR',
|
||||
description: '/docs/references/avatars/get-qr.md',
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
type: MethodType::LOCATION,
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::IMAGE_PNG
|
||||
))
|
||||
->param('text', '', new Text(512), 'Plain text to be converted to QR code image.')
|
||||
->param('size', 400, new Range(1, 1000), 'QR code size. Pass an integer between 1 to 1000. Defaults to 400.', true)
|
||||
->param('margin', 1, new Range(0, 10), 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true)
|
||||
->param('download', false, new Boolean(true), 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true)
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $text, int $size, int $margin, bool $download, Response $response)
|
||||
{
|
||||
$download = ($download === '1' || $download === 'true' || $download === 1 || $download === true);
|
||||
$options = new QROptions([
|
||||
'addQuietzone' => true,
|
||||
'quietzoneSize' => $margin,
|
||||
'outputType' => QRCode::OUTPUT_IMAGICK,
|
||||
'scale' => 15,
|
||||
]);
|
||||
|
||||
$qrcode = new QRCode($options);
|
||||
|
||||
if ($download) {
|
||||
$response->addHeader('Content-Disposition', 'attachment; filename="qr.png"');
|
||||
}
|
||||
|
||||
$image = new Image($qrcode->render($text));
|
||||
$image->crop((int) $size, (int) $size);
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
|
||||
->setContentType('image/png')
|
||||
->send($image->output('png', 90));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Screenshots;
|
||||
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Fetch\Client;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Platform\Action as UtopiaAction;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Assoc;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getScreenshot';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(UtopiaAction::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/avatars/screenshots')
|
||||
->desc('Get webpage screenshot')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('usage.metric', METRIC_AVATARS_SCREENSHOTS_GENERATED)
|
||||
->label('abuse-limit', 60)
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'avatar/screenshot')
|
||||
->label('cache.resource', 'screenshot/{request.url}/{request.width}/{request.height}/{request.scale}/{request.theme}/{request.userAgent}/{request.fullpage}/{request.locale}/{request.timezone}/{request.latitude}/{request.longitude}/{request.accuracy}/{request.touch}/{request.permissions}/{request.sleep}/{request.quality}/{request.output}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'avatars',
|
||||
group: null,
|
||||
name: 'getScreenshot',
|
||||
description: '/docs/references/avatars/get-screenshot.md',
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
type: MethodType::LOCATION,
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::IMAGE_PNG
|
||||
))
|
||||
->param('url', '', new URL(['http', 'https']), 'Website URL which you want to capture.', example: 'https://example.com')
|
||||
->param('headers', [], new Assoc(), 'HTTP headers to send with the browser request. Defaults to empty.', true, example: '{"Authorization":"Bearer token123","X-Custom-Header":"value"}')
|
||||
->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true, example: '1920')
|
||||
->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true, example: '1080')
|
||||
->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true, example: '2')
|
||||
->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark')
|
||||
->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15')
|
||||
->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true')
|
||||
->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US')
|
||||
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york')
|
||||
->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true, example: '37.7749')
|
||||
->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194')
|
||||
->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100')
|
||||
->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true, example: 'true')
|
||||
->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]')
|
||||
->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true, example: '3')
|
||||
->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true, example: '800')
|
||||
->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true, example: '600')
|
||||
->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85')
|
||||
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg')
|
||||
->inject('response')
|
||||
->inject('queueForStatsUsage')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, StatsUsage $queueForStatsUsage)
|
||||
{
|
||||
if (!\extension_loaded('imagick')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
|
||||
}
|
||||
|
||||
$domain = new Domain(\parse_url($url, PHP_URL_HOST));
|
||||
|
||||
if (!$domain->isKnown()) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
$client->setTimeout(30 * 1000); // 30 seconds
|
||||
$client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON);
|
||||
|
||||
// Convert indexed array to empty array (should not happen due to Assoc validator)
|
||||
if (is_array($headers) && count($headers) > 0 && array_keys($headers) === range(0, count($headers) - 1)) {
|
||||
$headers = [];
|
||||
}
|
||||
|
||||
// Create a new object to ensure proper JSON serialization
|
||||
$headersObject = new \stdClass();
|
||||
foreach ($headers as $key => $value) {
|
||||
$headersObject->$key = $value;
|
||||
}
|
||||
|
||||
// Create the config with headers as an object
|
||||
// The custom browser service accepts: url, theme, headers, sleep, viewport, userAgent, fullPage, locale, timezoneId, geolocation, hasTouch, scale
|
||||
$config = [
|
||||
'url' => $url,
|
||||
'theme' => $theme,
|
||||
'headers' => $headersObject,
|
||||
'sleep' => $sleep * 1000, // Convert seconds to milliseconds
|
||||
'waitUntil' => 'load',
|
||||
'viewport' => [
|
||||
'width' => $viewportWidth,
|
||||
'height' => $viewportHeight
|
||||
]
|
||||
];
|
||||
|
||||
// Add scale if not default
|
||||
if ($scale != 1) {
|
||||
$config['deviceScaleFactor'] = $scale;
|
||||
}
|
||||
|
||||
// Add optional parameters that were set, preserving arrays as arrays
|
||||
if (!empty($userAgent)) {
|
||||
$config['userAgent'] = $userAgent;
|
||||
}
|
||||
|
||||
if ($fullpage) {
|
||||
$config['fullPage'] = true;
|
||||
}
|
||||
|
||||
if (!empty($locale)) {
|
||||
$config['locale'] = $locale;
|
||||
}
|
||||
|
||||
if (!empty($timezone)) {
|
||||
$config['timezoneId'] = $timezone;
|
||||
}
|
||||
|
||||
// Add geolocation if any coordinates are provided
|
||||
if ($latitude != 0 || $longitude != 0) {
|
||||
$config['geolocation'] = [
|
||||
'latitude' => $latitude,
|
||||
'longitude' => $longitude,
|
||||
'accuracy' => $accuracy
|
||||
];
|
||||
}
|
||||
|
||||
if ($touch) {
|
||||
$config['hasTouch'] = true;
|
||||
}
|
||||
|
||||
// Add permissions if provided (preserve as array)
|
||||
if (!empty($permissions)) {
|
||||
$config['permissions'] = $permissions; // Keep as array
|
||||
}
|
||||
|
||||
try {
|
||||
$browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1');
|
||||
|
||||
$fetchResponse = $client->fetch(
|
||||
url: $browserEndpoint . '/screenshots',
|
||||
method: 'POST',
|
||||
body: $config
|
||||
);
|
||||
|
||||
if ($fetchResponse->getStatusCode() >= 400) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot service failed: ' . $fetchResponse->getBody());
|
||||
}
|
||||
|
||||
$screenshot = $fetchResponse->getBody();
|
||||
|
||||
if (empty($screenshot)) {
|
||||
throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND, 'Screenshot not generated');
|
||||
}
|
||||
|
||||
// Determine if image processing is needed
|
||||
$needsProcessing = ($width > 0 || $height > 0) || $quality !== -1 || !empty($output);
|
||||
|
||||
if ($needsProcessing) {
|
||||
// Process image with cropping, quality adjustment, or format conversion
|
||||
$image = new Image($screenshot);
|
||||
|
||||
$image->crop($width, $height);
|
||||
|
||||
$output = $output ?: 'png'; // Default to PNG if not specified
|
||||
$resizedScreenshot = $image->output($output, $quality);
|
||||
unset($image);
|
||||
} else {
|
||||
// Return original screenshot without processing
|
||||
$resizedScreenshot = $screenshot;
|
||||
$output = 'png'; // Screenshots are typically PNG by default
|
||||
}
|
||||
|
||||
// Set content type based on output format
|
||||
$outputs = Config::getParam('storage-outputs');
|
||||
$contentType = $outputs[$output] ?? $outputs['png'];
|
||||
|
||||
$queueForStatsUsage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1);
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
->setContentType($contentType)
|
||||
->file($resizedScreenshot);
|
||||
|
||||
|
||||
} catch (\Throwable $th) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED, 'Screenshot generation failed: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars;
|
||||
|
||||
use Appwrite\Platform\Modules\Avatars\Services\Http;
|
||||
use Utopia\Platform;
|
||||
|
||||
class Module extends Platform\Module
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->addService('http', new Http());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Browsers\Get as GetBrowser;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Back\Get as GetCloudCardBack;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\Front\Get as GetCloudCard;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Cards\Cloud\OG\Get as GetCloudCardOG;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\CreditCards\Get as GetCreditCard;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Favicon\Get as GetFavicon;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Flags\Get as GetFlag;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Image\Get as GetImage;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Initials\Get as GetInitials;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\QR\Get as GetQR;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Screenshots\Get as GetScreenshot;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Http extends Service
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->type = Service::TYPE_HTTP;
|
||||
|
||||
$this->addAction(GetCreditCard::getName(), new GetCreditCard());
|
||||
$this->addAction(GetBrowser::getName(), new GetBrowser());
|
||||
$this->addAction(GetFlag::getName(), new GetFlag());
|
||||
$this->addAction(GetImage::getName(), new GetImage());
|
||||
$this->addAction(GetFavicon::getName(), new GetFavicon());
|
||||
$this->addAction(GetQR::getName(), new GetQR());
|
||||
$this->addAction(GetInitials::getName(), new GetInitials());
|
||||
$this->addAction(GetScreenshot::getName(), new GetScreenshot());
|
||||
$this->addAction(GetCloudCard::getName(), new GetCloudCard());
|
||||
$this->addAction(GetCloudCardBack::getName(), new GetCloudCardBack());
|
||||
$this->addAction(GetCloudCardOG::getName(), new GetCloudCardOG());
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\System\System;
|
||||
@@ -106,6 +107,7 @@ class Base extends Action
|
||||
'resourceType' => 'functions',
|
||||
'entrypoint' => $entrypoint,
|
||||
'buildCommands' => $function->getAttribute('commands', ''),
|
||||
'startCommand' => $function->getAttribute('startCommand', ''),
|
||||
'type' => 'vcs',
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getSequence(),
|
||||
@@ -142,7 +144,7 @@ class Base extends Action
|
||||
return $deployment;
|
||||
}
|
||||
|
||||
public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, string $referenceType = 'branch', string $reference = ''): Document
|
||||
public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, Authorization $authorization, string $referenceType = 'branch', string $reference = ''): Document
|
||||
{
|
||||
$deploymentId = ID::unique();
|
||||
$providerInstallationId = $installation->getAttribute('providerInstallationId', '');
|
||||
@@ -202,6 +204,7 @@ class Base extends Action
|
||||
'resourceInternalId' => $site->getSequence(),
|
||||
'resourceType' => 'sites',
|
||||
'buildCommands' => implode(' && ', $commands),
|
||||
'startCommand' => $site->getAttribute('startCommand', ''),
|
||||
'buildOutput' => $site->getAttribute('outputDirectory', ''),
|
||||
'adapter' => $site->getAttribute('adapter', ''),
|
||||
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
|
||||
@@ -239,7 +242,7 @@ class Base extends Action
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
$ruleId = $isMd5 ? md5($domain) : ID::unique();
|
||||
|
||||
Authorization::skip(
|
||||
$authorization->skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
@@ -265,7 +268,7 @@ class Base extends Action
|
||||
$domain = "commit-" . substr($commitDetails['commitHash'], 0, 16) . ".{$sitesDomain}";
|
||||
$ruleId = md5($domain);
|
||||
try {
|
||||
Authorization::skip(
|
||||
$authorization->skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
@@ -302,7 +305,7 @@ class Base extends Action
|
||||
$domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}";
|
||||
$ruleId = md5($domain);
|
||||
try {
|
||||
Authorization::skip(
|
||||
$authorization->skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
@@ -328,6 +331,8 @@ class Base extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$this->updateEmptyManualRule($project, $site, $deployment, $dbForPlatform, $authorization);
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($site)
|
||||
@@ -336,4 +341,34 @@ class Base extends Action
|
||||
|
||||
return $deployment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update empty manual rule for deployment.
|
||||
* In case of first deployment, deployment ID will be empty in the rules, so we need to update it here.
|
||||
*
|
||||
* @param \Utopia\Database\Document $project
|
||||
* @param \Utopia\Database\Document $resource
|
||||
* @param \Utopia\Database\Document $deployment
|
||||
* @param \Utopia\Database\Database $dbForPlatform
|
||||
* @return void
|
||||
*/
|
||||
public static function updateEmptyManualRule(Document $project, Document $resource, Document $deployment, Database $dbForPlatform, Authorization $authorization)
|
||||
{
|
||||
$resourceType = $resource->getCollection() === 'sites' ? 'site' : 'function';
|
||||
|
||||
$queries = [
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::equal('deploymentResourceInternalId', [$resource->getSequence()]),
|
||||
Query::equal('deploymentResourceType', [$resourceType]),
|
||||
Query::equal('deploymentId', ['']),
|
||||
Query::equal('type', ['deployment']),
|
||||
Query::equal('trigger', ['manual']),
|
||||
];
|
||||
$dbForPlatform->forEach('rules', function (Document $rule) use ($deployment, $dbForPlatform, $authorization) {
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'deploymentInternalId' => $deployment->getSequence(),
|
||||
])));
|
||||
}, $queries);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ class Get extends Action
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -68,7 +69,8 @@ class Get extends Action
|
||||
string $type,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
array $platform
|
||||
array $platform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$domains = $platform['hostnames'] ?? [];
|
||||
if ($type === 'rules') {
|
||||
@@ -121,7 +123,7 @@ class Get extends Action
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
|
||||
}
|
||||
|
||||
$document = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
|
||||
$document = $authorization->skip(fn () => $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$value]),
|
||||
]));
|
||||
|
||||
|
||||
+5
-5
@@ -292,7 +292,7 @@ abstract class Action extends UtopiaAction
|
||||
};
|
||||
}
|
||||
|
||||
protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): Document
|
||||
protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): Document
|
||||
{
|
||||
$key = $attribute->getAttribute('key');
|
||||
$type = $attribute->getAttribute('type', '');
|
||||
@@ -310,7 +310,7 @@ abstract class Action extends UtopiaAction
|
||||
throw new Exception($this->getSpatialTypeNotSupportedException(), params: [$type]);
|
||||
}
|
||||
|
||||
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($db->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
@@ -371,7 +371,7 @@ abstract class Action extends UtopiaAction
|
||||
\in_array($attribute->getAttribute('type'), Database::SPATIAL_TYPES) &&
|
||||
$attribute->getAttribute('required')
|
||||
) {
|
||||
$hasData = !Authorization::skip(fn () => $dbForProject
|
||||
$hasData = !$authorization->skip(fn () => $dbForProject
|
||||
->findOne('database_' . $db->getSequence() . '_collection_' . $collection->getSequence()))
|
||||
->isEmpty();
|
||||
|
||||
@@ -472,9 +472,9 @@ abstract class Action extends UtopiaAction
|
||||
return $attribute;
|
||||
}
|
||||
|
||||
protected function updateAttribute(string $databaseId, string $collectionId, string $key, Database $dbForProject, Event $queueForEvents, string $type, int $size = null, string $filter = null, string|bool|int|float|array $default = null, bool $required = null, int|float|null $min = null, int|float|null $max = null, array $elements = null, array $options = [], string $newKey = null): Document
|
||||
protected function updateAttribute(string $databaseId, string $collectionId, string $key, Database $dbForProject, Event $queueForEvents, Authorization $authorization, string $type, int $size = null, string $filter = null, string|bool|int|float|array $default = null, bool $required = null, int|float|null $min = null, int|float|null $max = null, array $elements = null, array $options = [], string $newKey = null): Document
|
||||
{
|
||||
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($db->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user