Compare commits

...
Author SHA1 Message Date
Chirag Aggarwal 37746aaa12 Restore redirectStatusCode description noting 301 default 2026-04-03 19:09:18 +05:30
Chirag Aggarwal c8cfab29c2 Revert redirectStatusCode default back to 301
Keep response model default aligned with the runtime fallback
in general.php for legacy/null records.
2026-04-03 19:09:09 +05:30
Chirag Aggarwal e24d73b610 Fix Rule response model docs and default
- Fix redirectStatusCode default from 301 to 0 to match database default (null) and avoid misleading non-redirect rules
- Standardize field descriptions to use consistent "Only applicable if type is X" phrasing
- Fix typo in deploymentResourceId description
2026-04-03 19:05:47 +05:30
Torsten DittmannandGitHub c08e5f706c Merge pull request #11745 from appwrite/fix-yahoo-oauth-scopes 2026-04-02 19:44:10 +04:00
Matej BačoandGitHub 58a90cca4a Merge pull request #10643 from appwrite/feat-disposable-emails
feat: add support for blocking disposable email addresses
2026-04-02 13:03:32 +02:00
Matej Bačo 90e705f8c5 Improve docs 2026-04-02 12:26:03 +02:00
Matej Bačo 17076e4a00 Fix formatting 2026-04-02 11:55:32 +02:00
Matej Bačo 7c50bbc500 Merge branch '1.9.x' into feat-disposable-emails 2026-04-02 11:05:19 +02:00
Chirag AggarwalandGitHub 387668bb47 Merge pull request #11748 from appwrite/feat-smtp-messaging-adapter
Replace PHPMailer with utopia-php/messaging SMTP adapter
2026-04-02 12:11:58 +05:30
Chirag Aggarwal b6e95f4502 test: remove redundant headers empty-key case 2026-04-02 11:05:03 +05:30
Chirag Aggarwal 1900492aad Merge branch '1.9.x' into feat-smtp-messaging-adapter 2026-04-02 10:58:09 +05:30
Chirag AggarwalandGitHub 96f4ef7d22 Merge pull request #11751 from appwrite/chore/remove-phpstan-baseline
chore: remove phpstan baseline
2026-04-02 10:22:21 +05:30
Chirag AggarwalandGitHub 8e02a5a227 Merge pull request #11757 from appwrite/air/fix-flaky-migration-test-580aa1bc-d
Fix flaky vectordb migration test
2026-04-02 10:19:15 +05:30
Chirag Aggarwal c2dd8aceda use stable 2026-04-02 09:42:59 +05:30
Chirag Aggarwal 24fcf4247c Fix flaky vectordb migration test 2026-04-02 09:41:20 +05:30
Chirag Aggarwal 2dc20ef0eb Update utopia-php/messaging lock to latest 2026-04-02 09:31:48 +05:30
Chirag Aggarwal 04943b6313 Pass recipient display name in EmailMessage to field
Use associative array format ['email' => ..., 'name' => ...] for
the to field so the recipient display name appears in the To header.
2026-04-02 09:29:57 +05:30
Chirag Aggarwal eb366cf94b fix: preserve cors max age type 2026-04-02 08:25:59 +05:30
Chirag Aggarwal 77b4f8b7a0 style: apply formatter 2026-04-02 08:23:51 +05:30
Chirag Aggarwal 24bd4c3d7b fix: preserve migration resource stats 2026-04-02 08:11:57 +05:30
Chirag Aggarwal 33f8e35b62 chore: remove phpstan baseline 2026-04-01 23:01:11 +05:30
Chirag Aggarwal 2c1060f57a Fix null array key in HeadersTest
Cast null to string to avoid invalid array key type error
while preserving the test's intent of validating empty keys.
2026-04-01 21:31:08 +05:30
Chirag Aggarwal 30e0ca81bd Fix Usage::fromArray() to use static return type with new static()
Keep covariant return type with parent Base::fromArray(). The
new static() is safe here because the cloud subclass constructor
is backwards-compatible via optional params.
2026-04-01 21:30:36 +05:30
Chirag Aggarwal 92cc382a6b Change Usage::fromArray() return type from static to self
The cloud subclass has a different constructor signature so
new static() is unsafe. Use self since subclasses that need
deserialization should override fromArray() themselves.
2026-04-01 21:25:22 +05:30
Chirag Aggarwal 7d428ffe83 trigger ci 2026-04-01 21:03:07 +05:30
Chirag Aggarwal fb96aecbef Use new static() in Usage::fromArray() for late static binding
Required since the class is no longer final and the return type
is static, so subclasses get the correct type.
2026-04-01 19:52:16 +05:30
Matej Bačo 80d5d4716a Remove base errors 2026-04-01 15:54:26 +02:00
Matej Bačo c1dde09070 Merge branch '1.9.x' into feat-disposable-emails 2026-04-01 15:14:10 +02:00
Chirag Aggarwal d531c29fc8 Remove final from Usage class to allow cloud override 2026-04-01 17:20:34 +05:30
Chirag Aggarwal 7044601603 lock file 2026-04-01 17:05:14 +05:30
Chirag Aggarwal 53bc126015 Remove stale PHPMailer baseline entry from PHPStan
The PHPMailer $Port type error no longer occurs since registers.php
now uses the SMTP adapter instead of PHPMailer directly.
2026-04-01 17:01:15 +05:30
Chirag Aggarwal f1dc468e50 Restore replyTo elseif logic to preserve old behavior
customMailOptions replyTo and smtp replyTo are mutually exclusive,
matching the original PHPMailer implementation.
2026-04-01 16:37:08 +05:30
Chirag Aggarwal ce63b00cf4 lock file 2026-04-01 16:29:06 +05:30
Chirag Aggarwal b9aaecba25 Replace PHPMailer with utopia-php/messaging SMTP adapter
Use Utopia\Messaging\Adapter\Email\SMTP instead of raw PHPMailer
for the smtp register, Mails worker, and Doctor task. This enables
swapping email adapters (e.g. Resend) via DI override in downstream
repos by type-hinting against the EmailAdapter base class.
2026-04-01 16:25:33 +05:30
Torsten Dittmann d9c606a1c2 fix(oauth): update Yahoo OAuth scopes from deprecated Social Directory API to OIDC
The Yahoo OAuth provider was using deprecated Social Directory API scopes
('sdct-r' and 'sdpp-w') which are no longer valid and causing authentication
failures with the error: invalid_scope

Changes:
- Replace deprecated scopes 'sdct-r' (Social Directory Contacts Read) and
  'sdpp-w' (Social Directory Profile Write) with standard OIDC scopes
- Add 'openid' scope for OpenID Connect authentication
- Add 'profile' scope for basic profile information
- Add 'email' scope for email address access

These new scopes align with Yahoo's OpenID Connect implementation and are
listed in their discovery document at:
https://api.login.yahoo.com/.well-known/openid-configuration

The Yahoo adapter already uses the OIDC userinfo endpoint
(https://api.login.yahoo.com/openid/v1/userinfo), so these scopes are the
correct choice for authentication.

Custom scopes passed via the API are still supported and will be merged
with these defaults via the base OAuth2 class constructor.

Fixes: Yahoo OAuth authentication returning 'invalid_scope' error
2026-04-01 13:15:22 +04:00
Chirag AggarwalandGitHub a5b0378138 Merge pull request #11737 from appwrite/codex/phpstan-baseline-part-2
[codex] Fix PHPStan baseline cleanup issues (part 2)
2026-04-01 13:23:15 +05:30
Chirag AggarwalandGitHub 320068a576 Merge pull request #11740 from appwrite/speed-up-phpstan
Speed up PHPStan analysis with result caching
2026-04-01 13:01:07 +05:30
Chirag Aggarwal 3cd90ae629 fix analyze 2026-04-01 12:59:51 +05:30
Chirag Aggarwal 44f33473bd Use composer analyze in CI to stay in sync with local workflow 2026-04-01 12:04:31 +05:30
Chirag Aggarwal 358f1b78a8 Speed up PHPStan analysis with result caching
Configure a project-local result cache directory so PHPStan only
re-analyses files that changed. In CI, persist the cache across
runs with actions/cache and suppress progress output.
2026-04-01 12:00:32 +05:30
Jake BarnbyandGitHub 1f6b9d94bf Merge pull request #11739 from appwrite/readme-update-1.9.0-installation 2026-04-01 05:53:38 +00:00
Aditya OberaiandGitHub a734c8cd46 Update installation commands in readme for 1.9.0 to include self-hosted wizard 2026-04-01 11:20:45 +05:30
Chirag Aggarwal 1788e1bd6c Address PR review feedback 2026-04-01 11:15:59 +05:30
Chirag Aggarwal 983adf3ffd Fix analyze regressions in PHPStan cleanup 2026-04-01 11:00:26 +05:30
Chirag Aggarwal f2ea0b9b48 Fix PHPStan baseline cleanup issues (part 2) 2026-04-01 10:20:20 +05:30
eldadfux 34f23bdc9f Merge branch '1.8.x' into feat-disposable-emails
Made-with: Cursor

# Conflicts:
#	app/controllers/api/projects.php
2026-03-21 19:32:07 +01:00
eldadfux f74a60dba4 Merge origin/1.8.x into feat-disposable-emails
Made-with: Cursor
2026-03-20 07:43:55 +01:00
eldadfux 5e04480cc0 Merge branch '1.8.x' of origin into feat-disposable-emails
Made-with: Cursor
2026-03-15 19:53:04 +01:00
eldadfux ea2b1519d5 Updated deps 2026-03-15 09:26:37 +01:00
eldadfux e5385f7512 Removed old validator 2026-03-15 08:54:11 +01:00
eldadfux d8bf4b9f89 Implement email validation rules for disposable, canonical, and free emails in user account creation and project settings. Update error handling for invalid email types and adjust related configurations in the console and project models. 2026-03-14 09:21:22 +01:00
eldadfux 2d2c55e07c Merge remote-tracking branch 'origin/1.8.x' into feat-disposable-emails
Made-with: Cursor

# Conflicts:
#	app/controllers/api/account.php
#	app/controllers/api/messaging.php
#	app/controllers/api/projects.php
#	app/controllers/api/teams.php
#	app/controllers/api/users.php
#	composer.lock
#	src/Appwrite/GraphQL/Types/Mapper.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php
#	src/Appwrite/SDK/Specification/Format/OpenAPI3.php
#	src/Appwrite/SDK/Specification/Format/Swagger2.php
2026-03-13 21:56:58 +01:00
Eldad Fux da6dff2d17 Merge branch 'feat-disposable-emails' of github.com:appwrite/appwrite into feat-disposable-emails 2025-10-19 22:22:13 +01:00
Eldad Fux b4aab6134e Merge remote-tracking branch 'origin/1.8.x' into feat-disposable-emails 2025-10-19 22:20:31 +01:00
Eldad Fux 1c465a8cda refactor: standardize email validation across API controllers
- Reintroduced the Utopia Emails validator in multiple API controllers to ensure consistent email validation practices.
- Removed deprecated email validation references, streamlining the codebase and aligning with recent updates to the Utopia Emails library.
2025-10-19 22:16:03 +01:00
Eldad A. FuxandGitHub 8ab6c2d83b Merge branch '1.8.x' into feat-disposable-emails 2025-10-19 22:13:31 +01:00
Eldad Fux c89d075200 refactor: streamline disposable email validation in account API
- Replaced the previous disposable email validation logic with the EmailNotDisposable validator from the Utopia Emails library for consistency and improved maintainability.
- Removed unnecessary configuration loading related to disposable emails, aligning with recent updates to the email validation process.
2025-10-19 22:09:26 +01:00
Eldad Fux 6c6c71484d refactor: replace disposable email validation logic with Utopia Emails library
- Introduced the EmailNotDisposable validator from the Utopia Emails library for improved disposable email validation in the account API.
- Removed the deprecated disposable emails configuration loading from the config files to streamline the codebase.
2025-10-19 22:01:27 +01:00
Eldad Fux 18a66bae4e chore: update Utopia Emails dependency to version 0.5 and remove deprecated email validator
- Updated the Utopia Emails dependency version from 0.4.* to 0.5.* in composer.json and composer.lock.
- Removed the custom Email validator class and its associated tests as it is no longer needed with the new Utopia Emails library.
2025-10-19 21:55:52 +01:00
Eldad Fux 68b72f3162 feat: integrate Utopia Emails library for email validation
- Added Utopia Emails library as a dependency in composer.json.
- Updated email validation references across multiple API controllers to use the new Utopia Emails validator.
- Removed the deprecated disposable emails configuration file.
- Updated composer.lock to reflect the new library and version changes for existing dependencies.
2025-10-19 21:44:22 +01:00
Eldad A. FuxandGitHub f6438065e3 Merge branch '1.8.x' into feat-disposable-emails 2025-10-15 00:14:06 +01:00
4c6961f7d6 Update app/controllers/api/users.php
Co-authored-by: Jake Barnby <jakeb994@gmail.com>
2025-10-14 07:58:30 +01:00
0bd27c4182 Update app/controllers/api/users.php
Co-authored-by: Jake Barnby <jakeb994@gmail.com>
2025-10-14 07:58:24 +01:00
Eldad Fux ddcfac4f0f feat: enhance account handling with plan support for disposable email validation
- Added 'plan' injection to account-related API endpoints.
- Updated logic to check for disposable email validation support based on the plan.
- Improved handling of disposable email checks during account creation and email updates.
2025-10-14 00:03:11 +01:00
Eldad Fux 3e58014dbd fixes 2025-10-13 23:48:50 +01:00
Eldad Fux ff06920e24 feat: add support for blocking disposable email addresses
- Introduced a new exception for disposable email addresses.
- Updated user account creation and email handling to check against a list of disposable email domains.
- Added a new API endpoint to enable or disable disposable email checks for projects.
- Updated project model to include disposable email settings.
- Configured loading of disposable email domains from a separate configuration file.
2025-10-13 23:43:54 +01:00
108 changed files with 974 additions and 1915 deletions
+9 -1
View File
@@ -150,8 +150,16 @@ jobs:
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Cache PHPStan result cache
uses: actions/cache@v4
with:
path: .phpstan-cache
key: phpstan-${{ github.sha }}
restore-keys: |
phpstan-
- name: Run PHPStan
run: composer analyze
run: composer analyze -- --no-progress
locale:
name: Checks / Locale
+1
View File
@@ -21,6 +21,7 @@ appwrite.config.json
/app/config/specs/
/docs/examples/
.phpunit.cache
.phpstan-cache
playwright-report
test-results
docker-compose.web-installer.yml
+3
View File
@@ -72,6 +72,7 @@ Before running the installation command, make sure you have [Docker](https://www
```bash
docker run -it --rm \
--publish 20080:20080 \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
@@ -84,6 +85,7 @@ docker run -it --rm \
```cmd
docker run -it --rm ^
--publish 20080:20080 ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
@@ -94,6 +96,7 @@ docker run -it --rm ^
```powershell
docker run -it --rm `
--publish 20080:20080 `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
+4
View File
@@ -39,6 +39,10 @@ $console = [
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
// For email configuration, false means feature is disabled; false means these emails are allowed during sign-ups
'disposableEmails' => false,
'canonicalEmails' => false,
'freeEmails' => false,
'invalidateSessions' => true
],
'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
+15
View File
@@ -226,6 +226,21 @@ return [
'description' => 'A user with the same email already exists in the current project.',
'code' => 409,
],
Exception::USER_EMAIL_DISPOSABLE => [
'name' => Exception::USER_EMAIL_DISPOSABLE,
'description' => 'Disposable email addresses are not allowed. Please use a permanent email address.',
'code' => 400,
],
Exception::USER_EMAIL_FREE => [
'name' => Exception::USER_EMAIL_FREE,
'description' => 'Free email addresses are not allowed. Please use a business or custom-domain email address.',
'code' => 400,
],
Exception::USER_EMAIL_NOT_CANONICAL => [
'name' => Exception::USER_EMAIL_NOT_CANONICAL,
'description' => 'This email address must already be in its canonical form. Please remove aliases, tags, or provider-specific variations and try again.',
'code' => 400,
],
Exception::USER_PASSWORD_MISMATCH => [
'name' => Exception::USER_PASSWORD_MISMATCH,
'description' => 'Passwords do not match. Please check the password and confirm password.',
-3
View File
@@ -31,9 +31,6 @@ class FunctionUseCases
public const DEV_TOOLS = 'dev-tools';
public const AUTH = 'auth';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
+3 -6
View File
@@ -25,9 +25,6 @@ class SiteUseCases
public const FORMS = 'forms';
public const DASHBOARD = 'dashboard';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
@@ -252,7 +249,7 @@ return [
'frameworks' => [
getFramework('VITE', [
'providerRootDirectory' => './vite/vitepress',
'outputDirectory' => '404.html',
'fallbackFile' => '404.html',
'installCommand' => 'npm i vitepress && npm install',
'buildCommand' => 'npm run docs:build',
'outputDirectory' => './.vitepress/dist',
@@ -275,7 +272,7 @@ return [
'frameworks' => [
getFramework('VUE', [
'providerRootDirectory' => './vue/vuepress',
'outputDirectory' => '404.html',
'fallbackFile' => '404.html',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './src/.vuepress/dist',
@@ -298,7 +295,7 @@ return [
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/docusaurus',
'outputDirectory' => '404.html',
'fallbackFile' => '404.html',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './build',
+239 -62
View File
@@ -403,7 +403,8 @@ Http::post('/v1/account')
->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, Authorization $authorization, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks, array $plan) {
$email = \strtolower($email);
if ('console' === $project->getId()) {
@@ -452,11 +453,38 @@ Http::post('/v1/account')
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$proof = new ProofsPassword();
$hash = $proof->hash($password);
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email);
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
try {
@@ -487,11 +515,11 @@ Http::post('/v1/account')
'authenticators' => null,
'search' => implode(' ', [$userId, $email, $name]),
'accessedAt' => DateTime::now(),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
'emailCanonical' => $emailMetadata['emailCanonical'],
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
'emailIsFree' => $emailMetadata['emailIsFree'],
]);
$user->removeAttribute('$sequence');
@@ -695,6 +723,7 @@ Http::delete('/v1/account/sessions')
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
$currentSession = null;
foreach ($sessions as $session) {/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
@@ -716,6 +745,7 @@ Http::delete('/v1/account/sessions')
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
// Use current session for events.
$currentSession = $session;
$queueForEvents
->setPayload($response->output($session, Response::MODEL_SESSION));
@@ -728,9 +758,11 @@ Http::delete('/v1/account/sessions')
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId());
if ($currentSession instanceof Document) {
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $currentSession->getId());
}
$response->noContent();
});
@@ -776,7 +808,8 @@ Http::get('/v1/account/sessions/:sessionId')
->setAttribute('secret', $session->getAttribute('secret', ''))
;
return $response->dynamic($session, Response::MODEL_SESSION);
$response->dynamic($session, Response::MODEL_SESSION);
return;
}
}
@@ -956,7 +989,7 @@ Http::patch('/v1/account/sessions/:sessionId')
->setPayload($response->output($session, Response::MODEL_SESSION))
;
return $response->dynamic($session, Response::MODEL_SESSION);
$response->dynamic($session, Response::MODEL_SESSION);
});
Http::post('/v1/account/sessions/email')
@@ -1504,8 +1537,9 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('plan')
->inject('authorization')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@@ -1747,10 +1781,38 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
}
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email);
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
try {
@@ -1780,11 +1842,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
'authenticators' => null,
'search' => implode(' ', [$userId, $email, $name]),
'accessedAt' => DateTime::now(),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
'emailCanonical' => $emailMetadata['emailCanonical'],
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
'emailIsFree' => $emailMetadata['emailIsFree'],
]);
$user->removeAttribute('$sequence');
@@ -1859,19 +1921,47 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
}
if (empty($user->getAttribute('email'))) {
$user->setAttribute('email', $oauth2->getUserEmail($accessToken));
$email = $oauth2->getUserEmail($accessToken);
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($user->getAttribute('email'));
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email);
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
$user->setAttribute('emailCanonical', $emailCanonical?->getCanonical());
$user->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported());
$user->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate());
$user->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable());
$user->setAttribute('emailIsFree', $emailCanonical?->isFree());
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
$user->setAttribute('email', $email);
$user->setAttribute('emailCanonical', $emailMetadata['emailCanonical']);
$user->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical']);
$user->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate']);
$user->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable']);
$user->setAttribute('emailIsFree', $emailMetadata['emailIsFree']);
}
if (empty($user->getAttribute('name'))) {
@@ -1988,7 +2078,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
if (isset($sessionUpgrade) && $sessionUpgrade) {
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
foreach ($user->getAttribute('targets', []) as $target) {
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
continue;
@@ -2149,10 +2239,11 @@ Http::post('/v1/account/tokens/magic-url')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->inject('plan')
->inject('proofForPassword')
->inject('platform')
->inject('authorization')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2187,10 +2278,38 @@ Http::post('/v1/account/tokens/magic-url')
$userId = $userId === 'unique()' ? ID::unique() : $userId;
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email);
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
$user->setAttributes([
@@ -2217,11 +2336,11 @@ Http::post('/v1/account/tokens/magic-url')
'authenticators' => null,
'search' => implode(' ', [$userId, $email]),
'accessedAt' => DateTime::now(),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
'emailCanonical' => $emailMetadata['emailCanonical'],
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
'emailIsFree' => $emailMetadata['emailIsFree'],
]);
$user->removeAttribute('$sequence');
@@ -2429,10 +2548,11 @@ Http::post('/v1/account/tokens/email')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->inject('plan')
->inject('proofForPassword')
->inject('proofForCode')
->inject('authorization')
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2465,10 +2585,38 @@ Http::post('/v1/account/tokens/email')
$userId = $userId === 'unique()' ? ID::unique() : $userId;
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email);
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
$user->setAttributes([
@@ -2493,11 +2641,11 @@ Http::post('/v1/account/tokens/email')
'memberships' => null,
'search' => implode(' ', [$userId, $email]),
'accessedAt' => DateTime::now(),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
'emailCanonical' => $emailMetadata['emailCanonical'],
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
'emailIsFree' => $emailMetadata['emailIsFree'],
]);
$user->removeAttribute('$sequence');
@@ -3314,9 +3462,10 @@ Http::patch('/v1/account/email')
->inject('queueForEvents')
->inject('project')
->inject('hooks')
->inject('plan')
->inject('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) {
->inject('authorization')
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, array $plan, ProofsPassword $proofForPassword, Authorization $authorization) {
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user->getAttribute('passwordUpdate');
@@ -3344,20 +3493,48 @@ Http::patch('/v1/account/email')
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email);
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
->setAttribute('emailIsFree', $emailCanonical?->isFree())
->setAttribute('emailCanonical', $emailMetadata['emailCanonical'])
->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical'])
->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate'])
->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable'])
->setAttribute('emailIsFree', $emailMetadata['emailIsFree'])
;
if (empty($passwordUpdate)) {
@@ -4715,5 +4892,5 @@ Http::delete('/v1/account/identities/:identityId')
->setParam('identityId', $identity->getId())
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
return $response->noContent();
$response->noContent();
});
+1
View File
@@ -231,6 +231,7 @@ Http::get('/v1/locale/continents')
->inject('locale')
->action(function (Response $response, Locale $locale) {
$list = array_keys(Config::getParam('locale-continents'));
$output = [];
foreach ($list as $value) {
$output[] = new Document([
+1 -1
View File
@@ -3566,7 +3566,7 @@ Http::post('/v1/messaging/messages/push')
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
$scheduleTime = $scheduledAt;
if (!\is_null($scheduleTime)) {
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
} else {
+1 -1
View File
@@ -1570,7 +1570,7 @@ Http::post('/v1/projects/:projectId/smtp/tests')
->trigger();
}
return $response->noContent();
$response->noContent();
});
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
+104 -41
View File
@@ -73,7 +73,7 @@ use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks, array $plan): Document
{
$name = $name ?? '';
$plaintextPassword = $password;
@@ -110,11 +110,39 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
}
}
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email ?? '');
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
$hashedPassword = null;
$isHashed = !$hash instanceof Plaintext;
@@ -159,11 +187,11 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
'emailCanonical' => $emailMetadata['emailCanonical'],
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
'emailIsFree' => $emailMetadata['emailIsFree'],
]);
if (!$isHashed && !empty($password)) {
@@ -256,10 +284,11 @@ Http::post('/v1/users')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$plaintext = new Plaintext();
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($user, Response::MODEL_USER);
@@ -292,11 +321,12 @@ Http::post('/v1/users/bcrypt')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$bcrypt = new Bcrypt();
$bcrypt->setCost(8); // Default cost
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -330,10 +360,11 @@ Http::post('/v1/users/md5')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$md5 = new MD5();
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -367,10 +398,11 @@ Http::post('/v1/users/argon2')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$argon2 = new Argon2();
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -405,13 +437,14 @@ Http::post('/v1/users/sha')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$sha = new Sha();
if (!empty($passwordVersion)) {
$sha->setVersion($passwordVersion);
}
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -445,10 +478,11 @@ Http::post('/v1/users/phpass')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$phpass = new PHPass();
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -487,7 +521,8 @@ Http::post('/v1/users/scrypt')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$scrypt = new Scrypt();
$scrypt
->setSalt($passwordSalt)
@@ -496,7 +531,7 @@ Http::post('/v1/users/scrypt')
->setParallelCost($passwordParallel)
->setLength($passwordLength);
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -533,14 +568,15 @@ Http::post('/v1/users/scrypt-modified')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$scryptModified = new ScryptModified();
$scryptModified
->setSalt($passwordSalt)
->setSaltSeparator($passwordSaltSeparator)
->setSignerKey($passwordSignerKey);
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -1470,8 +1506,10 @@ Http::patch('/v1/users/:userId/email')
->param('email', '', new EmailValidator(allowEmpty: true), 'User email.')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('plan')
->inject('queueForEvents')
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Document $project, array $plan, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
@@ -1502,20 +1540,47 @@ Http::patch('/v1/users/:userId/email')
$oldEmail = $user->getAttribute('email');
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email);
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
->setAttribute('emailIsFree', $emailCanonical?->isFree())
->setAttribute('emailCanonical', $emailMetadata['emailCanonical'])
->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical'])
->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate'])
->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable'])
->setAttribute('emailIsFree', $emailMetadata['emailIsFree'])
;
try {
@@ -2337,9 +2402,8 @@ Http::post('/v1/users/:userId/sessions')
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($session, Response::MODEL_SESSION);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($session, Response::MODEL_SESSION);
});
Http::post('/v1/users/:userId/tokens')
@@ -2402,9 +2466,8 @@ Http::post('/v1/users/:userId/tokens')
->setParam('tokenId', $token->getId())
->setPayload($response->output($token, Response::MODEL_TOKEN));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($token, Response::MODEL_TOKEN);
});
Http::delete('/v1/users/:userId/sessions/:sessionId')
@@ -2658,7 +2721,7 @@ Http::delete('/v1/users/identities/:identityId')
->setParam('identityId', $identity->getId())
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
return $response->noContent();
$response->noContent();
});
Http::post('/v1/users/:userId/jwts')
+17 -10
View File
@@ -166,14 +166,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
$response->redirect('https://' . $request->getHostname() . $request->getURI());
return false;
}
}
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
/** @var Document $deployment */
if (!empty($rule->getAttribute('deploymentId', ''))) {
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
} else {
@@ -244,6 +244,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($isPreview && $requirePreview) {
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
$authorized = false;
$user = new Document();
// Security checks to mark authorized true
if (!empty($cookie)) {
@@ -273,7 +274,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$membershipExists = false;
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if (!$project->isEmpty() && isset($user)) {
if (!$project->isEmpty() && !$user->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$membership = $user->find('teamId', $teamId, 'memberships');
if (!empty($membership)) {
@@ -379,7 +380,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$executionId = ID::unique();
$headers = \array_merge([], $requestHeaders);
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-user-id'] = '';
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
@@ -459,7 +460,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
]);
@@ -529,6 +530,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
/** Execute function */
$executionResponse = [
'headers' => [],
'body' => '',
];
try {
$version = match ($type) {
'function' => $resource->getAttribute('version', 'v2'),
@@ -734,7 +740,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
$execution->setAttribute('responseHeaders', $headers);
$body = $execution['responseBody'] ?? '';
$body = $execution['responseBody'];
$contentType = 'text/plain';
foreach ($executionResponse['headers'] as $name => $values) {
@@ -865,9 +871,9 @@ Http::init()
Request::setRoute($route);
if ($route === null) {
return $response
->setStatusCode(404)
->send('Not Found');
$response->setStatusCode(404);
$response->send('Not Found');
return;
}
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
@@ -973,7 +979,8 @@ Http::init()
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
$response->redirect('https://' . $request->getHostname() . $request->getURI());
return;
}
}
});
+30 -28
View File
@@ -244,36 +244,38 @@ Http::get('/v1/mock/github/callback')
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
}
if (!empty($providerInstallationId)) {
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => false
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
if (empty($providerInstallationId)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing provider installation ID');
}
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => false
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
$response->json([
'installationId' => $installation->getId(),
]);
+4 -3
View File
@@ -3,6 +3,8 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/init/span.php';
global $register;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Constant;
@@ -31,7 +33,6 @@ use Utopia\Http\Files;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
use Utopia\Pools\Group;
use Utopia\Span\Span;
use Utopia\System\System;
@@ -293,7 +294,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
go(function () use ($register, $app) {
$pools = $register->get('pools');
/** @var Group $pools */
/** @var \Utopia\Pools\Group $pools */
Http::setResource('pools', fn () => $pools);
/** @var array $collections */
@@ -655,7 +656,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
/** @var Table $riskyDomains */
/** @var \Swoole\Table $riskyDomains */
$riskyDomains = $app->getResource('riskyDomains');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
+21 -36
View File
@@ -6,7 +6,6 @@ use Appwrite\Hooks\Hooks;
use Appwrite\PubSub\Adapter\Redis as PubSub;
use Appwrite\URL\URL as AppwriteURL;
use MaxMind\Db\Reader;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Database\PDOProxy;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Config\Config;
@@ -25,6 +24,7 @@ use Utopia\Logger\Adapter\LogOwl;
use Utopia\Logger\Adapter\Raygun;
use Utopia\Logger\Adapter\Sentry;
use Utopia\Logger\Logger;
use Utopia\Messaging\Adapter\Email\SMTP;
use Utopia\Mongo\Client as MongoClient;
use Utopia\Pools\Adapter\Stack as StackPool;
use Utopia\Pools\Adapter\Swoole as SwoolePool;
@@ -56,7 +56,7 @@ $register->set('logger', function () {
}
try {
$loggingProvider = new DSN($providerConfig ?? '');
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
$providerConfig = match ($providerName) {
@@ -76,7 +76,7 @@ $register->set('logger', function () {
};
}
if (empty($providerName) || empty($providerConfig)) {
if (empty($providerName)) {
return;
}
@@ -121,7 +121,7 @@ $register->set('realtimeLogger', function () {
default => ['key' => $loggingProvider->getHost()],
};
if (empty($providerName) || empty($providerConfig)) {
if (empty($providerName)) {
return;
}
@@ -242,8 +242,8 @@ $register->set('pools', function () {
],
];
$maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14);
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
@@ -308,7 +308,7 @@ $register->set('pools', function () {
]);
});
},
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase, $dsn) {
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
try {
$mongo = new MongoClient($dsnDatabase, $dsnHost, (int)$dsnPort, $dsnUser, $dsnPass, false);
@$mongo->connect();
@@ -433,35 +433,20 @@ $register->set('db', function () {
});
$register->set('smtp', function () {
$mail = new PHPMailer(true);
$mail->isSMTP();
$username = System::getEnv('_APP_SMTP_USERNAME');
$password = System::getEnv('_APP_SMTP_PASSWORD');
$mail->XMailer = 'Appwrite Mailer';
$mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp');
$mail->Port = System::getEnv('_APP_SMTP_PORT', 25);
$mail->SMTPAuth = !empty($username) && !empty($password);
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
$mail->SMTPAutoTLS = false;
$mail->SMTPKeepAlive = true;
$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);
$mail->setFrom($email, $from);
$mail->addReplyTo($email, $from);
$mail->isHTML(true);
return $mail;
$username = System::getEnv('_APP_SMTP_USERNAME', '');
$password = System::getEnv('_APP_SMTP_PASSWORD', '');
return new SMTP(
host: System::getEnv('_APP_SMTP_HOST', 'smtp'),
port: (int) System::getEnv('_APP_SMTP_PORT', 25),
username: $username,
password: $password,
smtpSecure: System::getEnv('_APP_SMTP_SECURE', ''),
smtpAutoTLS: false,
xMailer: 'Appwrite Mailer',
timeout: 10,
keepAlive: true,
timelimit: 30,
);
});
$register->set('geodb', function () {
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
+1 -1
View File
@@ -696,7 +696,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$cacheKey = \sprintf(
'%s-cache-%s:%s:%s:project:%s:functions:events',
$dbForProject->getCacheName(),
$hostname ?? '',
$hostname,
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$project->getId()
+6 -7
View File
@@ -237,7 +237,6 @@ if (!function_exists('getTelemetry')) {
if (!function_exists('triggerStats')) {
function triggerStats(array $event, string $projectId): void
{
return;
}
}
@@ -320,14 +319,14 @@ if (!function_exists('logError')) {
$server->error(logError(...));
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
$server->onStart(function () use ($stats, $containerId, &$statsDocument) {
sleep(5); // wait for the initial database schema to be ready
Console::success('Server started successfully');
/**
* Create document for this worker to share stats across Containers.
*/
go(function () use ($register, $containerId, &$statsDocument) {
go(function () use ($containerId, &$statsDocument) {
$attempts = 0;
$database = getConsoleDB();
@@ -357,7 +356,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
*/
// TODO: Remove this if check once it doesn't cause issues for cloud
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
Timer::tick(5000, function () use ($stats, &$statsDocument) {
$payload = [];
foreach ($stats as $projectId => $value) {
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
@@ -396,7 +395,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$attempts = 0;
$start = time();
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
Timer::tick(5000, function () use ($server, $realtime, $stats) {
/**
* Sending current connections to project channels on the console project every 5 seconds.
*/
@@ -798,7 +797,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
}
});
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
$project = null;
$authorization = null;
@@ -810,7 +809,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
// Get authorization from connection (stored during onOpen)
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
if ($authorization === null) {
$authorization = new Authorization('');
$authorization = new Authorization();
}
$database = getConsoleDB();
+3 -4
View File
@@ -1,7 +1,6 @@
<?php
require_once __DIR__ . '/init.php';
use Appwrite\Certificates\LetsEncrypt;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
@@ -280,14 +279,14 @@ Server::setResource('abuseRetention', function () {
Server::setResource('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
Server::setResource('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
});
Server::setResource('cache', function (Registry $register) {
+1 -1
View File
@@ -72,7 +72,7 @@
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
Generated
+8 -8
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b5261855586680e467168f527e0634ae",
"content-hash": "4fe91e67f343fbe6deac1fdc7eda949f",
"packages": [
{
"name": "adhocore/jwt",
@@ -4467,23 +4467,23 @@
},
{
"name": "utopia-php/messaging",
"version": "0.20.1",
"version": "0.22.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0"
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0",
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-openssl": "*",
"giggsey/libphonenumber-for-php-lite": "9.0.23",
"php": ">=8.0.0",
"php": ">=8.1.0",
"phpmailer/phpmailer": "6.9.1"
},
"require-dev": {
@@ -4512,9 +4512,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
"source": "https://github.com/utopia-php/messaging/tree/0.20.1"
"source": "https://github.com/utopia-php/messaging/tree/0.22.0"
},
"time": "2026-02-06T09:56:06+00:00"
"time": "2026-04-02T04:09:19+00:00"
},
{
"name": "utopia-php/migration",
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -1,8 +1,6 @@
includes:
- phpstan-baseline.neon
parameters:
level: 3
tmpDir: .phpstan-cache
paths:
- src
- app
@@ -14,4 +12,3 @@ parameters:
- vendor/swoole/ide-helper
excludePaths:
- tests/resources
+3 -2
View File
@@ -23,8 +23,9 @@ class Yahoo extends OAuth2
* @var array
*/
protected array $scopes = [
'sdct-r',
'sdpp-w',
'openid',
'profile',
'email',
];
/**
+3 -5
View File
@@ -285,7 +285,7 @@ class Event
*
* @param string $key
* @param Document $context
* @return self
* @return static
*/
public function setContext(string $key, Document $context): self
{
@@ -309,7 +309,7 @@ class Event
/**
* Set class used for this event.
* @param string $class
* @return self
* @return static
*/
public function setClass(string $class): self
{
@@ -648,10 +648,8 @@ class Event
*
* @param Event $event
*
* @return self
*
*/
public function from(Event $event): self
public function from(Event $event): static
{
$this->project = $event->getProject();
$this->user = $event->getUser();
+1
View File
@@ -40,6 +40,7 @@ class Usage extends Base
*/
public static function fromArray(array $data): static
{
/** @phpstan-ignore new.static (subclass constructors are backwards-compatible via optional params) */
return new static(
project: new Document($data['project'] ?? []),
metrics: $data['metrics'] ?? [],
+3
View File
@@ -82,6 +82,9 @@ class Exception extends \Exception
public const string USER_PASSWORD_RECENTLY_USED = 'password_recently_used';
public const string USER_PASSWORD_PERSONAL_DATA = 'password_personal_data';
public const string USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists';
public const string USER_EMAIL_DISPOSABLE = 'user_email_disposable';
public const string USER_EMAIL_FREE = 'user_email_free';
public const string USER_EMAIL_NOT_CANONICAL = 'user_email_not_canonical';
public const string USER_PASSWORD_MISMATCH = 'user_password_mismatch';
public const string USER_SESSION_NOT_FOUND = 'user_session_not_found';
public const string USER_IDENTITY_NOT_FOUND = 'user_identity_not_found';
+6 -10
View File
@@ -25,11 +25,7 @@ class Resolvers
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
/** @var Http $utopia */
/** @var Response $response */
/** @var Request $request */
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -96,7 +92,7 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -127,7 +123,7 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -163,7 +159,7 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -195,7 +191,7 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -225,7 +221,7 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
+76 -75
View File
@@ -98,10 +98,9 @@ class Schema
foreach ($routes as $route) {
/** @var Route $route */
/** @var \Appwrite\SDK\Method $sdk */
$sdk = $route->getLabel('sdk', false);
if (empty($sdk)) {
if ($sdk === false) {
continue;
}
@@ -177,7 +176,7 @@ class Schema
$required = $attr['required'];
$default = $attr['default'];
$escapedKey = str_replace('$', '', $key);
$collections[$collectionId][$escapedKey] = [
$collections[$databaseId][$collectionId][$escapedKey] = [
'type' => Mapper::attribute(
$type,
$array,
@@ -187,80 +186,82 @@ 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')
);
$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,
];
$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']),
foreach ($collections as $databaseId => $databaseCollections) {
foreach ($databaseCollections as $collectionId => $attributes) {
$objectType = new ObjectType([
'name' => $collectionId,
'fields' => \array_merge(
["_id" => ['type' => Type::string()]],
$attributes
),
]);
$attributes = \array_merge(
$attributes,
Mapper::args('mutate')
);
$queryFields[$collectionId . 'Get'] = [
'type' => $objectType,
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentGet(
$utopia,
$databaseId,
$collectionId,
$urls['get'],
)
),
'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'],
)
];
];
$queryFields[$collectionId . 'List'] = [
'type' => Type::listOf($objectType),
'args' => Mapper::args('list'),
'resolve' => Resolvers::documentList(
$utopia,
$databaseId,
$collectionId,
$urls['list'],
$params['list'],
),
'complexity' => $complexity,
];
$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'],
)
];
}
}
$offset += $limit;
}
+16 -7
View File
@@ -15,10 +15,13 @@ class Types
*
* @return Json
*/
public static function json(): Type
public static function json(): Json
{
if (Registry::has(Json::class)) {
return Registry::get(Json::class);
$type = Registry::get(Json::class);
if ($type instanceof Json) {
return $type;
}
}
$type = new Json();
Registry::set(Json::class, $type);
@@ -28,12 +31,15 @@ class Types
/**
* Get the JSON type.
*
* @return Json
* @return Assoc
*/
public static function assoc(): Type
public static function assoc(): Assoc
{
if (Registry::has(Assoc::class)) {
return Registry::get(Assoc::class);
$type = Registry::get(Assoc::class);
if ($type instanceof Assoc) {
return $type;
}
}
$type = new Assoc();
Registry::set(Assoc::class, $type);
@@ -45,10 +51,13 @@ class Types
*
* @return InputFile
*/
public static function inputFile(): Type
public static function inputFile(): InputFile
{
if (Registry::has(InputFile::class)) {
return Registry::get(InputFile::class);
$type = Registry::get(InputFile::class);
if ($type instanceof InputFile) {
return $type;
}
}
$type = new InputFile();
Registry::set(InputFile::class, $type);
+1 -3
View File
@@ -273,11 +273,9 @@ class Mapper
case \Appwrite\Auth\Validator\Password::class:
case \Appwrite\Event\Validator\Event::class:
case \Appwrite\Event\Validator\FunctionEvent::class:
case \Appwrite\Network\Validator\CNAME::class:
case \Utopia\Emails\Validator\Email::class:
case \Appwrite\Network\Validator\Redirect::class:
case \Appwrite\Network\Validator\DNS::class:
case \Appwrite\Network\Validator\Origin::class:
case \Appwrite\Task\Validator\Cron::class:
case \Appwrite\Utopia\Database\Validator\CustomId::class:
case \Utopia\Database\Validator\Key::class:
@@ -286,7 +284,7 @@ class Mapper
case \Utopia\Validator\HexColor::class:
case \Utopia\Validator\Host::class:
case \Utopia\Validator\IP::class:
case \Utopia\Validator\Origin::class:
case \Appwrite\Network\Validator\Origin::class:
case \Utopia\Validator\Text::class:
case \Utopia\Validator\URL::class:
case \Utopia\Validator\WhiteList::class:
+25
View File
@@ -13,6 +13,7 @@ use Utopia\Database\Exception\Limit;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\PDO;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
abstract class Migration
@@ -204,6 +205,30 @@ abstract class Migration
}
}
/**
* @param array<Query> $queries
* @return \Generator<int, Document>
* @throws Exception
*/
protected function documentsIterator(string $collection, array $queries = []): \Generator
{
$offset = 0;
do {
$documents = $this->dbForProject->find($collection, [
...$queries,
Query::limit($this->limit),
Query::offset($offset),
]);
foreach ($documents as $document) {
yield $document;
}
$offset += \count($documents);
} while (\count($documents) === $this->limit);
}
/**
* Creates collection from the config collection.
*
+4 -5
View File
@@ -1224,7 +1224,7 @@ class V15 extends Migration
* @param \Utopia\Database\Document $document
* @return \Utopia\Database\Document
*/
protected function fixDocument(Document $document)
protected function fixDocument(Document $document): Document
{
switch ($document->getCollection()) {
case 'cache':
@@ -1234,7 +1234,7 @@ class V15 extends Migration
* skipping migration for 'cache' and 'variables'.
* 'users' already migrated.
*/
return;
return $document;
case '_metadata':
/**
@@ -1480,7 +1480,6 @@ class V15 extends Migration
* Filter from the 'encrypt' filter.
*
* @param string $value
* @return string|false
*/
protected function encryptFilter(string $value): string
{
@@ -1492,8 +1491,8 @@ class V15 extends Migration
'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
'method' => OpenSSL::CIPHER_AES_128_GCM,
'iv' => \bin2hex($iv),
'tag' => \bin2hex($tag ?? ''),
'tag' => \bin2hex($tag),
'version' => '1',
]);
]) ?: '';
}
}
+1 -1
View File
@@ -452,7 +452,7 @@ class V20 extends Migration
Query::equal('period', ['1d']),
]);
$value = $query ?? 0;
$value = $query;
$this->createInfMetric($to, $value);
}
+1 -1
View File
@@ -48,7 +48,7 @@ final class Cors
/**
* Build CORS headers for a given request origin.
*
* @return array<string,string>
* @return array<string, int|string>
*/
public function headers(string $origin): array
{
+1 -1
View File
@@ -18,7 +18,7 @@ class OpenSSL
*
* @return string
*/
public static function encrypt($data, $method, $key, $options = 0, $iv = '', &$tag = null, $aad = '', $tag_length = 16)
public static function encrypt($data, $method, $key, $options = 0, $iv = '', ?string &$tag = null, $aad = '', $tag_length = 16)
{
return \openssl_encrypt($data, $method, $key, $options, $iv, $tag, $aad, $tag_length);
}
@@ -49,7 +49,6 @@ class Action extends PlatformAction
$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
@@ -204,7 +204,6 @@ class Get extends Action
$image = new Image($data);
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
@@ -95,7 +95,6 @@ class Get extends Action
}
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
@@ -90,7 +90,7 @@ class Get extends Action
}
}
$rand = \substr($code, -1);
$rand = (int) \substr((string) $code, -1);
$rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand;
@@ -17,7 +17,7 @@ class Action extends AppwriteAction
return $this->context;
}
public function setHttpPath(string $path): AppwriteAction
public function setHttpPath(string $path): self
{
if (\str_contains($path, '/tablesdb')) {
$this->context = DATABASE_TYPE_TABLESDB;
@@ -28,7 +28,8 @@ class Action extends AppwriteAction
if (\str_contains($path, '/vectorsdb')) {
$this->context = DATABASE_TYPE_VECTORSDB;
}
return parent::setHttpPath($path);
parent::setHttpPath($path);
return $this;
}
/**
@@ -24,7 +24,7 @@ abstract class Action extends DatabasesAction
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): DatabasesAction
public function setHttpPath(string $path): self
{
if (str_contains($path, '/tablesdb/')) {
$this->context = ROWS;
@@ -47,7 +47,8 @@ abstract class Action extends DatabasesAction
],
];
return parent::setHttpPath($path);
parent::setHttpPath($path);
return $this;
}
protected function getDatabasesOperationReadMetric(): string
@@ -406,8 +407,6 @@ abstract class Action extends DatabasesAction
if (\is_array($related)) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} elseif (empty($relations)) {
$document->setAttribute($relationship->getAttribute('key'), null);
}
}
@@ -209,7 +209,7 @@ class Create extends Action
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() . ' with relationship ' . $this->getStructureContext());
}
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) {
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $authorization) {
$allowedPermissions = [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
@@ -122,7 +122,6 @@ class Update extends Action
$dbForDatabases = $getDatabasesDB($database);
// Read permission should not be required for update
/** @var Document $document */
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
if ($transactionId !== null) {
@@ -147,7 +147,7 @@ class XList extends Action
$cacheKeyBase = \sprintf(
'%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s',
$dbForProject->getCacheName(),
$hostname ?? '',
$hostname,
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$collectionId,
@@ -99,8 +99,6 @@ class Update extends Action
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
$enabled ??= $collection->getAttribute('enabled', true);
$collection = $dbForProject->updateDocument(
'database_' . $database->getSequence(),
$collectionId,
@@ -103,6 +103,9 @@ class XList extends Action
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
$output[$i] = new Document([
'event' => $log['event'],
@@ -121,9 +124,9 @@ class XList extends Action
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel'],
'deviceName' => $deviceName,
'deviceBrand' => $deviceBrand,
'deviceModel' => $deviceModel,
]);
$record = $geodb->get($log['ip']);
@@ -33,7 +33,7 @@ abstract class Action extends DatabasesAction
return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_WRITES;
}
public function setHttpPath(string $path): DatabasesAction
public function setHttpPath(string $path): self
{
switch (true) {
case str_contains($path, '/tablesdb'):
@@ -50,7 +50,8 @@ abstract class Action extends DatabasesAction
$this->databaseType = VECTORSDB;
break;
}
return parent::setHttpPath($path);
parent::setHttpPath($path);
return $this;
}
/**
@@ -239,7 +239,7 @@ class Create extends Action
}
}
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) {
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $operations) {
$dbForProject->createDocuments('transactionLogs', $staged);
return $dbForProject->increaseDocumentAttribute(
'transactions',
@@ -105,8 +105,7 @@ class Update extends Action
* @throws Exception
* @throws \Throwable
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Structure
* @throws StructureException
* @throws \Utopia\Http\Exception
*/
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
@@ -183,7 +182,7 @@ class Update extends Action
$dbForDatabases = $getDatabasesDB($databaseDoc);
try {
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'committing',
])));
@@ -97,6 +97,9 @@ class XList extends Action
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
$output[$i] = new Document([
'event' => $log['event'],
@@ -115,9 +118,9 @@ class XList extends Action
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel'],
'deviceName' => $deviceName,
'deviceBrand' => $deviceBrand,
'deviceModel' => $deviceModel,
]);
$record = $geodb->get($log['ip']);
@@ -130,9 +130,11 @@ class Create extends CollectionAction
$indexes[] = new Document($index);
}
try {
// passing null in creates only creates the metadata collection
if (!$dbForDatabases->exists(null, Database::METADATA)) {
$dbForDatabases->create();
try {
$dbForDatabases->create();
} catch (DuplicateException) {
}
}
$dbForDatabases->createCollection(
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
@@ -88,16 +88,11 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
switch ($type) {
case 'output':
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForFunctions;
break;
}
[$path, $device] = match ($type) {
'output' => [$deployment->getAttribute('buildPath', ''), $deviceForBuilds],
'source' => [$deployment->getAttribute('sourcePath', ''), $deviceForFunctions],
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid deployment download type.'),
};
if (!$device->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
@@ -213,7 +213,10 @@ class Create extends Base
$current = new Document();
foreach ($sessions as $session) {
/** @var Utopia\Database\Document $session */
if (!$session instanceof Document) {
continue;
}
if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { // Find most recent active session for user ID and JWT headers
$current = $session;
}
@@ -237,11 +240,11 @@ class Create extends Base
]);
$executionId = ID::unique();
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
$headers['x-appwrite-user-id'] = $user->getId();
$headers['x-appwrite-user-jwt'] = $jwt;
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
@@ -350,16 +353,18 @@ class Create extends Base
}
}
$this->enqueueDeletes(
$project,
$function->getSequence(),
$executionsRetentionCount,
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$queueForDeletes
);
->setProject($project)
->setResource($function->getSequence())
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
return $response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($execution, Response::MODEL_EXECUTION);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($execution, Response::MODEL_EXECUTION);
return;
}
$durationStart = \microtime(true);
@@ -370,7 +375,7 @@ class Create extends Base
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
]);
@@ -537,32 +542,18 @@ class Create extends Base
}
}
$this->enqueueDeletes(
$project,
$function->getSequence(),
$executionsRetentionCount,
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$queueForDeletes
);
->setProject($project)
->setResource($function->getSequence())
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($execution, Response::MODEL_EXECUTION);
}
private function enqueueDeletes(
Document $project,
string $resourceId,
int $executionsRetentionCount,
DeleteEvent $queueForDeletes
): void {
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$queueForDeletes
->setProject($project)
->setResource($resourceId)
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
}
}
@@ -424,8 +424,8 @@ class Create extends Base
/** Trigger Realtime Events */
$queueForRealtime
->from($ruleCreate)
->setSubscribers(['console', $project->getId()])
->from($ruleCreate)
->trigger();
}
}
@@ -170,8 +170,6 @@ class Update extends Base
$runtime = $function->getAttribute('runtime');
}
$enabled ??= $function->getAttribute('enabled', true);
$repositoryId = $function->getAttribute('repositoryId', '');
$repositoryInternalId = $function->getAttribute('repositoryInternalId', '');
@@ -450,7 +450,7 @@ class Builds extends Action
$providerCommitHash = \trim($stdout);
$deployment->setAttribute('providerCommitHash', $providerCommitHash ?? '');
$deployment->setAttribute('providerCommitHash', $providerCommitHash);
$deployment->setAttribute('providerCommitAuthorUrl', APP_VCS_GITHUB_URL);
$deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME);
$deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function");
@@ -862,7 +862,7 @@ class Builds extends Action
if (\str_contains($logs, '{APPWRITE_DETECTION_SEPARATOR_START}')) {
[$logsBefore, $detectionLogsStart] = \explode('{APPWRITE_DETECTION_SEPARATOR_START}', $logs, 2);
[$detectionLogs, $logsAfter] = \explode('{APPWRITE_DETECTION_SEPARATOR_END}', $detectionLogsStart, 2);
$logs = ($logsBefore ?? '') . ($logsAfter ?? '');
$logs = $logsBefore . $logsAfter;
}
$deployment->setAttribute('buildLogs', $logs);
@@ -1203,6 +1203,8 @@ class Builds extends Action
protected function sendUsage(Document $resource, Document $deployment, Document $project, Context $usage, UsagePublisher $publisherForUsage): void
{
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$cpus = (int) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT);
$memory = (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT);
switch ($deployment->getAttribute('status')) {
case 'ready':
@@ -1364,6 +1366,8 @@ class Builds extends Action
Realtime $queueForRealtime,
array $platform
): void {
$deployment = new Document();
try {
if ($resource->getAttribute('providerSilentMode', false) === true) {
return;
@@ -1444,7 +1448,7 @@ class Builds extends Action
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$previewUrl = match ($resource->getCollection()) {
'functions' => '',
'sites' => ! empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
'sites' => !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
default => throw new \Exception('Invalid resource type')
};
@@ -114,6 +114,9 @@ class Create extends Action
'passwordDictionary' => false,
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
'personalDataCheck' => false,
'disposableEmails' => false,
'canonicalEmails' => false,
'freeEmails' => false,
'mockNumbers' => [],
'sessionAlerts' => false,
'membershipsUserName' => false,
@@ -83,7 +83,8 @@ class Update extends Action
// If rule is already verified or in certificate generation state, don't queue for verification again
if ($rule->getAttribute('status') === RULE_STATUS_VERIFIED || $rule->getAttribute('status') === RULE_STATUS_CERTIFICATE_GENERATING) {
return $response->dynamic($rule, Response::MODEL_PROXY_RULE);
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
return;
}
try {
@@ -87,16 +87,11 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
switch ($type) {
case 'output':
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForSites;
break;
}
[$path, $device] = match ($type) {
'output' => [$deployment->getAttribute('buildPath', ''), $deviceForBuilds],
'source' => [$deployment->getAttribute('sourcePath', ''), $deviceForSites],
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid deployment download type.'),
};
if (!$device->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
@@ -172,8 +172,6 @@ class Update extends Base
$framework = $site->getAttribute('framework');
}
$enabled ??= $site->getAttribute('enabled', true);
$repositoryId = $site->getAttribute('repositoryId', '');
$repositoryInternalId = $site->getAttribute('repositoryInternalId', '');
@@ -286,6 +286,8 @@ class Create extends Action
$mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption
$fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption
$data = '';
$iv = '';
$tag = null;
// Compression
$algorithm = $bucket->getAttribute('compression', Compression::NONE);
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) {
@@ -99,12 +99,8 @@ class Update extends Action
$permissions ??= $bucket->getPermissions();
$maximumFileSize ??= $bucket->getAttribute('maximumFileSize', (int) System::getEnv('_APP_STORAGE_LIMIT', 0));
$allowedFileExtensions ??= $bucket->getAttribute('allowedFileExtensions', []);
$enabled ??= $bucket->getAttribute('enabled', true);
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
$transformations ??= $bucket->getAttribute('transformations', true);
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
@@ -102,6 +102,8 @@ class Create extends Action
{
$isAppUser = $user->isApp($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$invitee = new Document();
$hash = '';
if (empty($url)) {
if (! $isAppUser && ! $isPrivilegedUser) {
@@ -145,9 +147,6 @@ class Create extends Action
}
} elseif (! empty($phone)) {
$invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
if (! $invitee->isEmpty() && ! empty($email) && $invitee->getAttribute('email', '') !== $email) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409);
}
}
if ($invitee->isEmpty()) { // Create new user if no user with same email found
@@ -169,14 +168,41 @@ class Create extends Action
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$userId = ID::unique();
$hash = $proofForPassword->hash($proofForPassword->generate());
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email);
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
$hash = $proofForPassword->hash($proofForPassword->generate());
$userId = ID::unique();
$userDocument = new Document([
@@ -209,11 +235,11 @@ class Create extends Action
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
'emailCanonical' => $emailMetadata['emailCanonical'],
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
'emailIsFree' => $emailMetadata['emailIsFree'],
]);
try {
@@ -110,10 +110,7 @@ class Update extends Action
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($providerRepositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -65,6 +65,7 @@ class Get extends Action
}
$state = \json_decode($state, true);
$redirectFailure = $state['failure'] ?? '';
$projectId = $state['projectId'] ?? '';
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -74,10 +75,11 @@ class Get extends Action
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
return;
}
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
@@ -165,10 +167,11 @@ class Get extends Action
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
return;
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
@@ -49,6 +49,8 @@ trait Deployment
) {
$errors = [];
foreach ($repositories as $repository) {
$logBase = 'vcs.github.event.repo.unknown';
try {
$repositoryId = $repository->getId();
$projectId = $repository->getAttribute('projectId');
@@ -107,18 +109,11 @@ trait Deployment
$owner = $github->getOwnerName($providerInstallationId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$repositoryName = $github->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$isAuthorized = !$external;
if (!$isAuthorized && !empty($providerPullRequestId)) {
@@ -291,10 +286,7 @@ trait Deployment
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$repositoryName = $github->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -501,7 +493,7 @@ trait Deployment
$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', '')) : '';
$previewUrl = !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
if (!empty($previewUrl)) {
$comment = new Comment($platform);
@@ -524,10 +516,7 @@ trait Deployment
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$repositoryName = $github->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -83,7 +83,7 @@ class Create extends Action
default => null,
};
return $response->json($parsedPayload);
$response->json($parsedPayload);
}
protected function preprocessEvent(Request $request)
+13 -9
View File
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\ClamAV\Network;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Config\Config;
use Utopia\Console;
@@ -13,6 +12,8 @@ use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Logger\Logger;
use Utopia\Messaging\Adapter\Email as EmailAdapter;
use Utopia\Messaging\Messages\Email as EmailMessage;
use Utopia\Platform\Action;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
@@ -124,7 +125,7 @@ class Doctor extends Action
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG', '');
try {
$loggingProvider = new DSN($providerConfig ?? '');
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
@@ -212,15 +213,18 @@ class Doctor extends Action
}
try {
/* @var PHPMailer $mail */
$mail = $register->get('smtp');
/** @var EmailAdapter $smtp */
$smtp = $register->get('smtp');
$mail->addAddress('demo@example.com', 'Example.com');
$mail->Subject = 'Test SMTP Connection';
$mail->Body = 'Hello World';
$mail->AltBody = 'Hello World';
$emailMessage = new EmailMessage(
to: ['demo@example.com'],
subject: 'Test SMTP Connection',
content: 'Hello World',
fromName: \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')),
fromEmail: System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
);
$mail->send();
$smtp->send($emailMessage);
Console::success('🟢 ' . str_pad("SMTP", 50, '.') . 'connected');
} catch (\Throwable) {
Console::error('🔴 ' . str_pad("SMTP", 47, '.') . 'disconnected');
+1 -1
View File
@@ -227,7 +227,7 @@ class Install extends Action
// Fall back to CLI mode
$enableAssistant = false;
$assistantExistsInOldCompose = false;
if ($existingInstallation && isset($compose)) {
if ($existingInstallation) {
try {
$assistantService = $compose->getService('appwrite-assistant');
$assistantExistsInOldCompose = $assistantService !== null;
+2
View File
@@ -566,6 +566,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$repoBranch = $language['repoBranch'] ?? 'main';
if ($git && !empty($gitUrl)) {
$prUrls = [];
// Generate commit message: use provided message, AI changelog, or fallback
if (! empty($message)) {
$commitMessage = $message;
@@ -60,7 +60,7 @@ class StatsResources extends Action
$interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600');
Console::loop(function () use ($queueForStatsResources, $dbForPlatform) {
Console::loop(function () use ($queueForStatsResources) {
$last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours'));
/**
+1 -4
View File
@@ -6,7 +6,6 @@ use Exception;
use Throwable;
use Utopia\Console;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
@@ -51,13 +50,11 @@ class Audits extends Action
/**
* @param Message $message
* @param callable $getProjectDB
* @param Document $project
* @param callable $getAudit
* @param callable(Document): \Utopia\Audit\Audit $getAudit
* @return Commit|NoCommit
* @throws Throwable
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Structure
*/
public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit
+8 -14
View File
@@ -24,7 +24,6 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
@@ -363,7 +362,6 @@ class Deletes extends Action
/**
* @param Document $project
* @param callable $getProjectDB
* @param Document $target
* @return void
* @throws Exception
*/
@@ -438,7 +436,6 @@ class Deletes extends Action
* @param string $resource
* @param string|null $resourceType
* @return void
* @throws Authorization
* @throws Exception
*/
private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource, ?string $resourceType = null): void
@@ -518,7 +515,6 @@ class Deletes extends Action
}
/**
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $hourlyUsageRetentionDatetime
* @return void
@@ -586,7 +582,6 @@ class Deletes extends Action
* @param Database $dbForPlatform
* @param Document $document
* @return void
* @throws Authorization
* @throws DatabaseException
* @throws Conflict
* @throws Restricted
@@ -623,7 +618,6 @@ class Deletes extends Action
* @param Document $document
* @return void
* @throws Exception
* @throws Authorization
* @throws DatabaseException
*/
protected function deleteProject(Database $dbForPlatform, callable $getProjectDB, callable $getDatabasesDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Document $document): void
@@ -952,7 +946,7 @@ class Deletes extends Action
// fast path, no need to list anything!
$delete($dbForProject, $resourceInternalId, $resourceType);
} else {
$processResource = function (string $type) use ($dbForProject, $delete, $resourceType) {
$processResource = function (string $type) use ($dbForProject, $delete) {
$this->listByGroup(
collection: $type,
queries: [Query::select(['$id', '$sequence'])],
@@ -1109,7 +1103,7 @@ class Deletes extends Action
Query::equal('resourceInternalId', [$siteInternalId]),
Query::equal('resourceType', ['sites']),
Query::orderAsc()
], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
], $dbForProject, function (Document $document) use ($deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds, &$deploymentIds) {
$deploymentInternalIds[] = $document->getSequence();
$deploymentIds[] = $document->getId();
$this->deleteBuildFiles($deviceForBuilds, $document);
@@ -1172,7 +1166,7 @@ class Deletes extends Action
Query::equal('deploymentResourceInternalId', [$functionInternalId]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::orderAsc()
], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) {
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
});
@@ -1196,7 +1190,7 @@ class Deletes extends Action
Query::equal('resourceInternalId', [$functionInternalId]),
Query::equal('resourceType', ['functions']),
Query::orderAsc()
], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
$deploymentInternalIds[] = $document->getSequence();
$this->deleteDeploymentFiles($deviceForFunctions, $document);
$this->deleteBuildFiles($deviceForBuilds, $document);
@@ -1321,7 +1315,7 @@ class Deletes extends Action
/**
* @param Device $device
* @param Document $build
* @param Document $deployment
* @return void
*/
private function deleteBuildFiles(Device $device, Document $deployment): void
@@ -1631,9 +1625,9 @@ class Deletes extends Action
try {
$dbForProject->deleteDocuments('transactions', [
Query::lessThan('expiresAt', DateTime::format(new \DateTime())),
], onNext: function (Document $transaction) use ($dbForProject, $project, &$transactionInternalIds) {
], onNext: function (Document $transaction) use (&$transactionInternalIds) {
$transactionInternalIds[] = $transaction->getSequence();
}, onError: function (Throwable $th) use ($project) {
}, onError: function (Throwable $th) {
// Swallow errors to avoid breaking the cleanup process
});
} catch (Throwable $th) {
@@ -1646,7 +1640,7 @@ class Deletes extends Action
$dbForProject->deleteDocuments('transactionLogs', [
Query::equal('transactionInternalId', $transactionInternalIds),
], onError: function (Throwable $th) use ($project) {
], onError: function (Throwable $th) {
// Swallow errors to avoid breaking the cleanup process
});
}
+9 -6
View File
@@ -33,7 +33,7 @@ class Functions extends Action
}
/**
* @throws Exception
* @throws \Exception
*/
public function __construct()
{
@@ -256,7 +256,7 @@ class Functions extends Action
* @param Document $user
* @param string|null $jwt
* @param string|null $event
* @throws Exception
* @throws \Exception
*/
private function fail(
string $message,
@@ -271,10 +271,10 @@ class Functions extends Action
?string $event = null,
): void {
$executionId = ID::unique();
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-trigger'] = $trigger;
$headers['x-appwrite-event'] = $event ?? '';
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-id'] = $user->getId();
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
$headersFiltered = [];
@@ -458,8 +458,8 @@ class Functions extends Action
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' => $body ?? '',
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_EVENT_DATA' => $body,
'APPWRITE_FUNCTION_EVENT' => $headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
@@ -508,6 +508,9 @@ class Functions extends Action
]);
/** Execute function */
$error = null;
$errorCode = 0;
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];
+53 -68
View File
@@ -4,10 +4,13 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Template\Template;
use Exception;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Runtime;
use Utopia\Database\Document;
use Utopia\Logger\Log;
use Utopia\Messaging\Adapter\Email as EmailAdapter;
use Utopia\Messaging\Adapter\Email\SMTP;
use Utopia\Messaging\Messages\Email as EmailMessage;
use Utopia\Messaging\Messages\Email\Attachment;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Registry\Registry;
@@ -49,9 +52,9 @@ class Mails extends Action
/**
* @param Message $message
* @param Document $project
* @param Registry $register
* @param Log $log
* @throws \PHPMailer\PHPMailer\Exception
* @return void
* @throws Exception
*/
@@ -132,36 +135,38 @@ class Mails extends Action
// render() will return the subject in <p> tags, so use strip_tags() to remove them
$subject = \strip_tags($subjectTemplate->render());
/** @var PHPMailer $mail */
$mail = empty($smtp)
/** @var EmailAdapter $adapter */
$adapter = empty($smtp)
? $register->get('smtp')
: $this->getMailer($smtp);
: new SMTP(
host: $smtp['host'],
port: (int) $smtp['port'],
username: $smtp['username'] ?? '',
password: $smtp['password'] ?? '',
smtpSecure: $smtp['secure'] ?? '',
smtpAutoTLS: false,
xMailer: 'Appwrite Mailer',
timeout: 10,
keepAlive: true,
timelimit: 30,
);
$mail->clearAddresses();
$mail->clearAllRecipients();
$mail->clearReplyTos();
$mail->clearAttachments();
$mail->clearBCCs();
$mail->clearCCs();
$mail->addAddress($recipient, $name);
$mail->Subject = $subject;
$mail->Body = $body;
// Resolve from/replyTo using fallback hierarchy: Custom options > SMTP config > Defaults
$defaultFromEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$defaultFromName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$mail->AltBody = $body;
$mail->AltBody = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $mail->AltBody);
$mail->AltBody = \strip_tags($mail->AltBody);
$mail->AltBody = \trim($mail->AltBody);
$replyTo = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$replyToName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$fromEmail = !empty($smtp) ? ($smtp['senderEmail'] ?? $defaultFromEmail) : $defaultFromEmail;
$fromName = !empty($smtp) ? ($smtp['senderName'] ?? $defaultFromName) : $defaultFromName;
$replyTo = $defaultFromEmail;
$replyToName = $defaultFromName;
$customMailOptions = $payload['customMailOptions'] ?? [];
// fallback hierarchy: Custom options > SMTP config > Defaults.
if (!empty($customMailOptions['senderEmail']) || !empty($customMailOptions['senderName'])) {
$fromEmail = $customMailOptions['senderEmail'] ?? $mail->From;
$fromName = $customMailOptions['senderName'] ?? $mail->FromName;
$mail->setFrom($fromEmail, $fromName);
if (!empty($customMailOptions['senderEmail'])) {
$fromEmail = $customMailOptions['senderEmail'];
}
if (!empty($customMailOptions['senderName'])) {
$fromName = $customMailOptions['senderName'];
}
if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) {
@@ -172,18 +177,32 @@ class Mails extends Action
$replyToName = $smtp['senderName'] ?? $replyToName;
}
$mail->addReplyTo($replyTo, $replyToName);
$attachments = null;
if (!empty($attachment['content'] ?? '')) {
$mail->AddStringAttachment(
base64_decode($attachment['content']),
$attachment['filename'] ?? 'unknown.file',
$attachment['encoding'] ?? PHPMailer::ENCODING_BASE64,
$attachment['type'] ?? 'plain/text'
);
$attachments = [
new Attachment(
name: $attachment['filename'] ?? 'unknown.file',
path: '',
type: $attachment['type'] ?? 'plain/text',
content: \base64_decode($attachment['content']),
),
];
}
$emailMessage = new EmailMessage(
to: [['email' => $recipient, 'name' => $name]],
subject: $subject,
content: $body,
fromName: $fromName,
fromEmail: $fromEmail,
replyToName: $replyToName,
replyToEmail: $replyTo,
attachments: $attachments,
html: true,
);
try {
$mail->send();
$adapter->send($emailMessage);
} catch (\Throwable $error) {
if ($type === 'smtp') {
throw new Exception('Error sending mail: ' . $error->getMessage(), 401);
@@ -191,38 +210,4 @@ class Mails extends Action
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
}
}
/**
* @param array $smtp
* @return PHPMailer
* @throws \PHPMailer\PHPMailer\Exception
*/
protected function getMailer(array $smtp): PHPMailer
{
$mail = new PHPMailer(true);
$mail->isSMTP();
$username = $smtp['username'];
$password = $smtp['password'];
$mail->XMailer = 'Appwrite Mailer';
$mail->Host = $smtp['host'];
$mail->Port = $smtp['port'];
$mail->SMTPAuth = (!empty($username) && !empty($password));
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = $smtp['secure'];
$mail->SMTPAutoTLS = false;
$mail->SMTPKeepAlive = true;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
$mail->setFrom($smtp['senderEmail'], $smtp['senderName']);
$mail->isHTML();
return $mail;
}
}
+2 -2
View File
@@ -285,7 +285,7 @@ class Messaging extends Action
try {
$response = $adapter->send($data);
$deliveredTotal += $response['deliveredTo'];
$deliveredTotal += (int) $response['deliveredTo'];
foreach ($response['results'] as $result) {
if ($result['status'] === 'failure') {
$deliveryErrors[] = "Failed sending to target {$result['recipient']} with error: {$result['error']}";
@@ -380,7 +380,7 @@ class Messaging extends Action
]));
// Delete any attachments that were downloaded to local storage
if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) {
if ($providerType === MESSAGE_TYPE_EMAIL) {
if ($deviceForFiles->getType() === Storage::DEVICE_LOCAL) {
return;
}
+1 -1
View File
@@ -408,6 +408,7 @@ class Migrations extends Action
$tempAPIKey = $this->generateAPIKey($project);
$transfer = $source = $destination = null;
$aggregatedResources = [];
$host = System::getEnv('_APP_MIGRATION_HOST');
if (empty($host)) {
@@ -444,7 +445,6 @@ class Migrations extends Action
$destination
);
$aggregatedResources = [];
/** Start Transfer */
if (empty($source->getErrors())) {
$migration->setAttribute('stage', 'migrating');
@@ -208,7 +208,7 @@ class StatsResources extends Action
{
$totalFiles = 0;
$totalStorage = 0;
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalFiles, &$totalStorage) {
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $region, &$totalFiles, &$totalStorage) {
try {
$files = $dbForProject->count('bucket_' . $bucket->getSequence());
} catch (Throwable $th) {
+4 -4
View File
@@ -140,7 +140,7 @@ class StatsUsage extends Action
/**
* @param Message $message
* @param callable(): Database $getProjectDB
* @param callable(Document): Database $getProjectDB
* @param callable(): Database $getLogsDB
* @param Registry $register
* @return void
@@ -212,7 +212,7 @@ class StatsUsage extends Action
* @param Document $project
* @param Document $document
* @param array $metrics
* @param callable(): Database $getProjectDB
* @param callable(Document): Database $getProjectDB
* @param string $databaseType Database type from context
* @return void
*/
@@ -394,7 +394,7 @@ class StatsUsage extends Action
/**
* Commit stats to DB
* @param callable(): Database $getProjectDB
* @param callable(Document): Database $getProjectDB
* @return void
*/
public function commitToDb(callable $getProjectDB): void
@@ -459,7 +459,7 @@ class StatsUsage extends Action
/**
* Sort by unique index key reduce locks/deadlocks
*/
usort($projectStats['stats'], function ($a, $b) use ($sequence) {
usort($projectStats['stats'], function ($a, $b) {
// Metric DESC
$cmp = strcmp($b['metric'], $a['metric']);
if ($cmp !== 0) {
+1 -1
View File
@@ -233,7 +233,7 @@ class Webhooks extends Action
$template->setParam('{{webhook}}', $webhook->getAttribute('name'));
$template->setParam('{{project}}', $project->getAttribute('name'));
$template->setParam('{{url}}', $webhook->getAttribute('url'));
$template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code');
$template->setParam('{{error}}', 'The server returned ' . $statusCode . ' status code');
$template->setParam('{{path}}', "/console/project-$region-$projectId/settings/webhooks/$webhookId");
$template->setParam('{{attempts}}', $attempts);
+1
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Promises;
/** @phpstan-consistent-constructor */
abstract class Promise
{
protected const STATE_PENDING = 1;
+1 -1
View File
@@ -197,7 +197,7 @@ class Method
public function isHidden(): bool|array
{
return $this->hide ?? false;
return $this->hide;
}
public function isPackaging(): bool
@@ -204,9 +204,6 @@ abstract class Format
*
* Get services value
*
* @param array $services
*
* @return self
*/
public function getServices(): array
{
@@ -79,8 +79,8 @@ class OpenAPI3 extends Format
$output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '<YOUR_API_KEY>'];
}
if (isset($output['securityDefinitions']['JWT'])) {
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
if (isset($output['components']['securitySchemes']['JWT'])) {
$output['components']['securitySchemes']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
}
if (isset($output['components']['securitySchemes']['Locale'])) {
@@ -99,7 +99,7 @@ class OpenAPI3 extends Format
$sdk = $route->getLabel('sdk', false);
if (empty($sdk)) {
if ($sdk === false) {
continue;
}
@@ -125,7 +125,9 @@ class OpenAPI3 extends Format
$namespace = $sdk->getNamespace() ?? 'default';
$desc ??= '';
if ($desc === null) {
$desc = '';
}
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
$temp = [
@@ -163,7 +165,7 @@ class OpenAPI3 extends Format
];
}
if (!empty($additionalMethods)) {
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
$temp['x-appwrite']['methods'] = [];
foreach ($additionalMethods as $methodObj) {
/** @var Method $methodObj */
@@ -329,7 +331,7 @@ class OpenAPI3 extends Format
if (($response->getCode() ?? 500) === 204) {
$temp['responses'][(string)$response->getCode() ?? '500']['description'] = 'No content';
unset($temp['responses'][(string)$response->getCode() ?? '500']['schema']);
unset($temp['responses'][(string)$response->getCode() ?? '500']['content']);
}
}
@@ -383,7 +385,7 @@ class OpenAPI3 extends Format
$validator = $validator->getValidator();
}
$class = !empty($validator)
$class = $validator instanceof Validator
? \get_class($validator)
: '';
@@ -431,7 +433,7 @@ class OpenAPI3 extends Format
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case \Utopia\Database\Validator\DatetimeValidator::class:
case \Utopia\Database\Validator\Datetime::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'datetime';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
@@ -463,7 +465,6 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = ($param['example'] ?? '') ?: 'https://example.com';
break;
case \Utopia\Validator\JSON::class:
case \Utopia\Validator\Mock::class:
case \Utopia\Validator\Assoc::class:
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['type'] = 'object';
@@ -563,12 +564,6 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\Length::class:
$node['schema']['type'] = $validator->getType();
if (!empty($param['example'])) {
$node['schema']['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\WhiteList::class:
if ($array) {
$validator = $validator->getValidator();
@@ -96,10 +96,9 @@ class Swagger2 extends Format
$scope = $route->getLabel('scope', '');
/** @var Method $sdk */
$sdk = $route->getLabel('sdk', false);
if (empty($sdk)) {
if ($sdk === false) {
continue;
}
@@ -127,7 +126,9 @@ class Swagger2 extends Format
$sdkPlatforms = array_values(array_unique($sdkPlatforms));
$namespace = $sdk->getNamespace() ?? 'default';
$desc ??= '';
if ($desc === null) {
$desc = '';
}
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
$temp = [
@@ -171,7 +172,7 @@ class Swagger2 extends Format
$temp['produces'][] = $produces;
}
if (!empty($additionalMethods)) {
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
$temp['x-appwrite']['methods'] = [];
foreach ($additionalMethods as $methodObj) {
/** @var Method $methodObj */
@@ -388,7 +389,7 @@ class Swagger2 extends Format
$validator = $validator->getValidator();
}
$class = !empty($validator)
$class = $validator instanceof Validator
? \get_class($validator)
: '';
@@ -436,7 +437,7 @@ class Swagger2 extends Format
$node['type'] = $validator->getType();
$node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case \Utopia\Database\Validator\DatetimeValidator::class:
case \Utopia\Database\Validator\Datetime::class:
$node['type'] = $validator->getType();
$node['format'] = 'datetime';
$node['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
@@ -479,7 +480,6 @@ class Swagger2 extends Format
}
break;
case \Utopia\Validator\JSON::class:
case \Utopia\Validator\Mock::class:
case \Utopia\Validator\Assoc::class:
$node['type'] = 'object';
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
@@ -552,12 +552,6 @@ class Swagger2 extends Format
$node['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\Length::class:
$node['type'] = $validator->getType();
if (!empty($param['example'])) {
$node['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\WhiteList::class:
if ($array) {
$validator = $validator->getValidator();
@@ -86,7 +86,6 @@ class User extends Document
/**
* Check if user is anonymous.
*
* @param Document $this
* @return bool
*/
public function isAnonymous(): bool
@@ -153,7 +152,6 @@ class User extends Document
/**
* Verify session and check that its not expired.
*
* @param array<Document> $sessions
* @param string $secret
*
* @return bool|string
@@ -137,6 +137,24 @@ class Project extends Model
'default' => false,
'example' => true,
])
->addRule('authDisposableEmails', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether or not to disallow disposable email addresses during signup and email updates.',
'default' => false,
'example' => true,
])
->addRule('authCanonicalEmails', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether or not to require canonical email addresses during signup and email updates.',
'default' => false,
'example' => true,
])
->addRule('authFreeEmails', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether or not to disallow free email addresses during signup and email updates.',
'default' => false,
'example' => true,
])
->addRule('authMockNumbers', [
'type' => Response::MODEL_MOCK_NUMBER,
'description' => 'An array of mock numbers and their corresponding verification codes (OTPs).',
@@ -415,6 +433,9 @@ class Project extends Model
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
$document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false);
$document->setAttribute('authDisposableEmails', $authValues['disposableEmails'] ?? false);
$document->setAttribute('authCanonicalEmails', $authValues['canonicalEmails'] ?? false);
$document->setAttribute('authFreeEmails', $authValues['freeEmails'] ?? false);
$document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []);
$document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false);
$document->setAttribute('authMembershipsUserName', $authValues['membershipsUserName'] ?? true);
+6 -6
View File
@@ -48,38 +48,38 @@ class Rule extends Model
])
->addRule('redirectUrl', [
'type' => self::TYPE_STRING,
'description' => 'URL to redirect to. Used if type is "redirect"',
'description' => 'URL to redirect to. Only applicable if type is "redirect", otherwise empty string.',
'default' => '',
'example' => 'https://appwrite.io/docs',
])
->addRule('redirectStatusCode', [
'type' => self::TYPE_INTEGER,
'description' => 'Status code to apply during redirect. Used if type is "redirect"',
'description' => 'Status code to apply during redirect. Only applicable if type is "redirect", defaults to 301.',
'default' => 301,
'example' => 301,
])
->addRule('deploymentId', [
'type' => self::TYPE_STRING,
'description' => 'ID of deployment. Used if type is "deployment"',
'description' => 'ID of deployment. Only applicable if type is "deployment", otherwise empty string.',
'default' => '',
'example' => 'n3u9feiwmf',
])
->addRule('deploymentResourceType', [
'type' => self::TYPE_ENUM,
'description' => 'Type of deployment. Possible values are "function", "site". Used if rule\'s type is "deployment".',
'description' => 'Type of deployment. Possible values are "function", "site". Only applicable if type is "deployment", otherwise empty string.',
'default' => '',
'example' => 'function',
'enum' => ['function', 'site'],
])
->addRule('deploymentResourceId', [
'type' => self::TYPE_STRING,
'description' => 'ID deployment\'s resource. Used if type is "deployment"',
'description' => 'ID of deployment\'s resource. Only applicable if type is "deployment", otherwise empty string.',
'default' => '',
'example' => 'n3u9feiwmf',
])
->addRule('deploymentVcsProviderBranch', [
'type' => self::TYPE_STRING,
'description' => 'Name of Git branch that updates rule. Used if type is "deployment"',
'description' => 'Name of Git branch that updates rule. Only applicable if type is "deployment", otherwise empty string.',
'default' => '',
'example' => 'main',
])
@@ -29,8 +29,8 @@ class DatabasesStringTypesTest extends Scope
protected function setupDatabaseAndCollection(): array
{
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty(static::$setupCache[$cacheKey])) {
return static::$setupCache[$cacheKey];
if (!empty(self::$setupCache[$cacheKey])) {
return self::$setupCache[$cacheKey];
}
$projectId = $this->getProject()['$id'];
@@ -135,12 +135,12 @@ class DatabasesStringTypesTest extends Scope
// Wait for all attributes to be available
$this->waitForAllAttributes($databaseId, $collectionId);
static::$setupCache[$cacheKey] = [
self::$setupCache[$cacheKey] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
];
return static::$setupCache[$cacheKey];
return self::$setupCache[$cacheKey];
}
public function testCreateDatabase(): void
@@ -45,6 +45,12 @@ trait DatabasesPermissionsBase
return $recordId ? "{$base}/{$recordId}" : $base;
}
protected function getIndexUrl(string $databaseId, string $containerId, string $indexId = ''): string
{
$base = "{$this->getContainerUrl($databaseId, $containerId)}/indexes";
return $indexId ? "{$base}/{$indexId}" : $base;
}
// User Management Methods
public function createUser(string $id, string $email, string $password = 'test123!'): array
{
@@ -414,7 +414,7 @@ class FunctionsCustomClientTest extends Scope
'offset' => 2
]);
$this->assertEquals(200, $templatesOffset['headers']['status-code']);
$this->addToAssertionCount(1, $templatesOffset['body']['templates']);
$this->addToAssertionCount(1);
$this->assertEquals($templates['body']['templates'][2]['id'], $templatesOffset['body']['templates'][0]['id']);
// List templates with filters
@@ -1003,6 +1003,7 @@ class FunctionsCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id']
];
$id = '';
$largeTag = null;
while (!feof($handle)) {
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'large-fx.tar.gz');
$headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size;
@@ -102,7 +102,7 @@ class AbuseTest extends Scope
$maxQueries = System::getEnv('_APP_GRAPHQL_MAX_QUERIES', 10);
$query = [];
for ($i = 0; $i <= $maxQueries + 1; $i++) {
for ($i = 0; $i <= ((int) $maxQueries) + 1; $i++) {
$query[] = ['query' => $this->getQuery(self::LIST_COUNTRIES)];
}
+1 -1
View File
@@ -409,7 +409,7 @@ class MessagingTest extends Scope
$apiKey = $emailDSN->getPassword();
$domain = $emailDSN->getUser();
if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) {
if (empty($to) || empty($fromName) || empty($fromEmail) || empty($apiKey) || empty($domain) || empty($isEuRegion)) {
$this->markTestSkipped('Email provider not configured');
}
@@ -229,6 +229,8 @@ class StorageClientTest extends Scope
], $this->getHeaders()), $gqlPayload);
$this->assertEquals(47218, \strlen($file['body']));
return $file;
}
/**
@@ -291,6 +291,8 @@ class StorageServerTest extends Scope
], $this->getHeaders()), $gqlPayload);
$this->assertEquals(47218, \strlen($file['body']));
return $file;
}
/**
@@ -102,7 +102,7 @@ class AbuseTest extends Scope
$maxQueries = System::getEnv('_APP_GRAPHQL_MAX_QUERIES', 10);
$query = [];
for ($i = 0; $i <= $maxQueries + 1; $i++) {
for ($i = 0; $i <= ((int) $maxQueries) + 1; $i++) {
$query[] = ['query' => $this->getQuery(self::LIST_COUNTRIES)];
}
@@ -2241,7 +2241,7 @@ trait MessagingBase
$authKey = $smsDSN->getPassword();
$templateId = $smsDSN->getParam('templateId');
if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) {
if (empty($to) || empty($senderId) || empty($authKey)) {
$this->markTestSkipped('SMS provider not configured');
}
@@ -64,8 +64,8 @@ trait MigrationsBase
*/
protected function setupMigrationDatabase(): array
{
if (!empty(static::$cachedDatabaseData)) {
return static::$cachedDatabaseData;
if (!empty(self::$cachedDatabaseData)) {
return self::$cachedDatabaseData;
}
$response = $this->client->call(Client::METHOD_POST, '/databases', [
@@ -81,11 +81,11 @@ trait MigrationsBase
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
static::$cachedDatabaseData = [
self::$cachedDatabaseData = [
'databaseId' => $response['body']['$id'],
];
return static::$cachedDatabaseData;
return self::$cachedDatabaseData;
}
/**
@@ -94,8 +94,8 @@ trait MigrationsBase
*/
protected function setupMigrationTable(): array
{
if (!empty(static::$cachedTableData)) {
return static::$cachedTableData;
if (!empty(self::$cachedTableData)) {
return self::$cachedTableData;
}
// Ensure database exists first
@@ -141,12 +141,12 @@ trait MigrationsBase
$this->assertEquals('available', $response['body']['status']);
}, 5000, 500);
static::$cachedTableData = [
self::$cachedTableData = [
'databaseId' => $databaseId,
'tableId' => $tableId,
];
return static::$cachedTableData;
return self::$cachedTableData;
}
public function performMigrationSync(array $body): array
@@ -670,7 +670,7 @@ trait MigrationsBase
]);
// Clear the cache since we cleaned up
static::$cachedDatabaseData = [];
self::$cachedDatabaseData = [];
}
public function testAppwriteMigrationDatabasesRow(): void
@@ -757,8 +757,8 @@ trait MigrationsBase
]);
// Clear the caches since we cleaned up
static::$cachedDatabaseData = [];
static::$cachedTableData = [];
self::$cachedDatabaseData = [];
self::$cachedTableData = [];
}
/**
@@ -1331,7 +1331,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($missingColumn, $databaseId, $tableId) {
$this->assertEventually(function () use ($missingColumn) {
$migrationId = $missingColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1363,7 +1363,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($missingColumn, $databaseId, $tableId) {
$this->assertEventually(function () use ($missingColumn) {
$migrationId = $missingColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1395,7 +1395,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($irrelevantColumn, $databaseId, $tableId) {
$this->assertEventually(function () use ($irrelevantColumn) {
$migrationId = $irrelevantColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1422,7 +1422,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($migration, $databaseId, $tableId) {
$this->assertEventually(function () use ($migration) {
$migrationId = $migration['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1464,7 +1464,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($migration, $databaseId, $tableId) {
$this->assertEventually(function () use ($migration) {
$migrationId = $migration['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -4018,7 +4018,7 @@ trait MigrationsBase
]
);
$this->assertEventually(function () use ($migration, $databaseId, $tableId) {
$this->assertEventually(function () use ($migration) {
$migrationId = $migration['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',

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