diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aa6dbe2bc3..f44d5eedf3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/.gitignore b/.gitignore
index d6e138a382..6846e19aa7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
index 457863d236..ed83252e2f 100644
--- a/README.md
+++ b/README.md
@@ -1,43 +1,28 @@
-> We just announced DB operators for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-db-operators)
-
-> Appwrite Cloud is now Generally Available - [Learn more](https://appwrite.io/cloud-ga)
-
-> [Get started with Appwrite](https://apwr.dev/appcloud)
+
-
-
-
- Appwrite is a best-in-class, developer-first platform that gives builders everything they need to create scalable, stable, and production-ready software, fast.
+
Appwrite
+ Appwrite is an open-source, all-in-one development platform. Use built-in backend infrastructure and web hosting, all from a single place.
-
-
-[](https://appwrite.io/company/careers)
-[](https://hacktoberfest.appwrite.io)
-[](https://appwrite.io/discord?r=Github)
-[](https://github.com/appwrite/appwrite/actions)
-[](https://twitter.com/appwrite)
-
-
-
-
+[](https://appwrite.io/discord)
+[](https://x.com/appwrite)
+[](https://cloud.appwrite.io)
English | [简体中文](README-CN.md)
-Appwrite is an end-to-end platform for building Web, Mobile, Native, or Backend apps, packaged as a set of Docker microservices. It includes both a backend server and a fully integrated hosting solution for deploying static and server-side rendered frontends. Appwrite abstracts the complexity and repetitiveness required to build modern apps from scratch and allows you to build secure, full-stack applications faster.
+Appwrite is an open-source development platform for building web, mobile, and AI applications. It brings together backend infrastructure and web hosting in one place, so teams can build, ship, and scale without stitching together a fragmented stack. Appwrite is available as a managed cloud platform and can also be self-hosted on infrastructure you control.
-Using Appwrite, you can easily integrate your app with user authentication and multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, messaging, and [more services](https://appwrite.io/docs).
+With Appwrite, you can add authentication, databases, storage, functions, messaging, realtime capabilities, and integrated web app hosting through Sites. It is designed to reduce the repetitive backend work required to launch modern products while giving developers secure primitives and flexible APIs to build production-ready applications faster.
-
-
-Find out more at: [https://appwrite.io](https://appwrite.io).
+Find out more at [https://appwrite.io](https://appwrite.io).
Table of Contents:
+- [Products](#products)
- [Installation \& Setup](#installation--setup)
- [Self-Hosting](#self-hosting)
- [Unix](#unix)
@@ -47,17 +32,31 @@ Table of Contents:
- [Upgrade from an Older Version](#upgrade-from-an-older-version)
- [One-Click Setups](#one-click-setups)
- [Getting Started](#getting-started)
- - [Products](#products)
- [SDKs](#sdks)
- [Client](#client)
- [Server](#server)
- - [Community](#community)
- [Architecture](#architecture)
- [Contributing](#contributing)
- [Security](#security)
- [Follow Us](#follow-us)
- [License](#license)
+
+## Products
+
+- **[Appwrite Auth](https://appwrite.io/docs/products/authentication)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
+
+- **[Appwrite Databases](https://appwrite.io/docs/products/databases)** - Scalable structured data storage with support for databases, tables, and rows. Includes querying, pagination, indexing, and relationships to model complex application data.
+
+- **[Appwrite Storage](https://appwrite.io/docs/products/storage)** - Secure file storage with support for uploads, downloads, encryption, compression, and file transformations for media and assets.
+
+- **[Appwrite Functions](https://appwrite.io/docs/products/functions)** - Serverless compute platform to run custom backend logic in isolated runtimes, triggered by events or scheduled jobs.15 runtimes supported.
+
+- **[Appwrite Messaging](https://appwrite.io/docs/products/messaging)** - Multi-channel messaging system for sending emails, SMS, and push notifications to users for engagement, alerts, and transactional workflows.
+
+- **[Appwrite Sites](https://appwrite.io/docs/products/sites)** - Integrated hosting platform to deploy and scale web applications with support for custom domains, SSR, and seamless backend integration. Git integration and previews are supported.
+
+
## Installation & Setup
The easiest way to get started with Appwrite is by [signing up for Appwrite Cloud](https://cloud.appwrite.io/). While Appwrite Cloud is in public beta, you can build with Appwrite completely free, and we won't collect your credit card information.
@@ -72,6 +71,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 +84,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 +95,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" `
@@ -165,51 +167,29 @@ Getting started with Appwrite is as easy as creating a new project, choosing you
| | [Quick start for Kotlin](https://appwrite.io/docs/quick-starts/kotlin) |
| | [Quick start for Swift](https://appwrite.io/docs/quick-starts/swift) |
-### Products
-
-- [**Account**](https://appwrite.io/docs/references/cloud/client-web/account) - Manage current user authentication and account. Track and manage the user sessions, devices, sign-in methods, and security logs.
-- [**Users**](https://appwrite.io/docs/server/users) - Manage and list all project users when building backend integrations with Server SDKs.
-- [**Teams**](https://appwrite.io/docs/references/cloud/client-web/teams) - Manage and group users in teams. Manage memberships, invites, and user roles within a team.
-- [**Databases**](https://appwrite.io/docs/references/cloud/client-web/databases) - Manage databases, collections, and documents. Read, create, update, and delete documents and filter lists of document collections using advanced filters.
-- [**Storage**](https://appwrite.io/docs/references/cloud/client-web/storage) - Manage storage files. Read, create, delete, and preview files. Manipulate the preview of your files to perfectly fit your app. All files are scanned by ClamAV and stored in a secure and encrypted way.
-- [**Functions**](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Customize your Appwrite project by executing your custom code in a secure, isolated environment. You can trigger your code on any Appwrite system event either manually or using a CRON schedule.
-- [**Messaging**](https://appwrite.io/docs/references/cloud/client-web/messaging) - Communicate with your users through push notifications, emails, and SMS text messages using Appwrite Messaging.
-- [**Realtime**](https://appwrite.io/docs/realtime) - Listen to real-time events for any of your Appwrite services including users, storage, functions, databases, and more.
-- [**Locale**](https://appwrite.io/docs/references/cloud/client-web/locale) - Track your user's location and manage your app locale-based data.
-- [**Avatars**](https://appwrite.io/docs/references/cloud/client-web/avatars) - Manage your users' avatars, countries' flags, browser icons, and credit card symbols. Generate QR codes from links or plaintext strings.
-- [**MCP**](https://appwrite.io/docs/tooling/mcp) - Use Appwrite's Model Context Protocol (MCP) server to allow LLMs and AI tools like Claude Desktop, Cursor, and Windsurf Editor to directly interact with your Appwrite project through natural language.
-- [**Sites**](https://appwrite.io/docs/products/sites) - Develop, deploy, and scale your web applications directly from Appwrite, alongside your backend.
-
-For the complete API documentation, visit [https://appwrite.io/docs](https://appwrite.io/docs). For more tutorials, news and announcements check out our [blog](https://medium.com/appwrite-io) and [Discord Server](https://discord.gg/GSeTUeA).
-
### SDKs
Below is a list of currently supported platforms and languages. If you would like to help us add support to your platform of choice, you can go over to our [SDK Generator](https://github.com/appwrite/sdk-generator) project and view our [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md).
#### Client
-- :white_check_mark: [Web](https://github.com/appwrite/sdk-for-web) (Maintained by the Appwrite Team)
-- :white_check_mark: [Flutter](https://github.com/appwrite/sdk-for-flutter) (Maintained by the Appwrite Team)
-- :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple) (Maintained by the Appwrite Team)
-- :white_check_mark: [Android](https://github.com/appwrite/sdk-for-android) (Maintained by the Appwrite Team)
-- :white_check_mark: [React Native](https://github.com/appwrite/sdk-for-react-native) - **Beta** (Maintained by the Appwrite Team)
+- :white_check_mark: [Web](https://github.com/appwrite/sdk-for-web)
+- :white_check_mark: [Flutter](https://github.com/appwrite/sdk-for-flutter)
+- :white_check_mark: [Apple](https://github.com/appwrite/sdk-for-apple)
+- :white_check_mark: [Android](https://github.com/appwrite/sdk-for-android)
+- :white_check_mark: [React Native](https://github.com/appwrite/sdk-for-react-native)
#### Server
-- :white_check_mark: [NodeJS](https://github.com/appwrite/sdk-for-node) (Maintained by the Appwrite Team)
-- :white_check_mark: [PHP](https://github.com/appwrite/sdk-for-php) (Maintained by the Appwrite Team)
-- :white_check_mark: [Dart](https://github.com/appwrite/sdk-for-dart) (Maintained by the Appwrite Team)
-- :white_check_mark: [Deno](https://github.com/appwrite/sdk-for-deno) (Maintained by the Appwrite Team)
-- :white_check_mark: [Ruby](https://github.com/appwrite/sdk-for-ruby) (Maintained by the Appwrite Team)
-- :white_check_mark: [Python](https://github.com/appwrite/sdk-for-python) (Maintained by the Appwrite Team)
-- :white_check_mark: [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team)
-- :white_check_mark: [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team)
-- :white_check_mark: [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team)
-
-#### Community
-
-- :white_check_mark: [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (Maintained by [Michael Gangolf](https://github.com/m1ga/))
-- :white_check_mark: [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (Maintained by [fenix-hub @GodotNuts](https://github.com/fenix-hub))
+- :white_check_mark: [NodeJS](https://github.com/appwrite/sdk-for-node)
+- :white_check_mark: [PHP](https://github.com/appwrite/sdk-for-php)
+- :white_check_mark: [Dart](https://github.com/appwrite/sdk-for-dart)
+- :white_check_mark: [Deno](https://github.com/appwrite/sdk-for-deno)
+- :white_check_mark: [Ruby](https://github.com/appwrite/sdk-for-ruby)
+- :white_check_mark: [Python](https://github.com/appwrite/sdk-for-python)
+- :white_check_mark: [Kotlin](https://github.com/appwrite/sdk-for-kotlin)
+- :white_check_mark: [Swift](https://github.com/appwrite/sdk-for-swift)
+- :white_check_mark: [.NET](https://github.com/appwrite/sdk-for-dotnet)
Looking for more SDKs? - Help us by contributing a pull request to our [SDK Generator](https://github.com/appwrite/sdk-generator)!
diff --git a/app/cli.php b/app/cli.php
index b8721320be..b636707f1c 100644
--- a/app/cli.php
+++ b/app/cli.php
@@ -329,17 +329,20 @@ $setResource('bus', function (Registry $register) use ($cli) {
$setResource('telemetry', fn () => new NoTelemetry(), []);
+$exitCode = 0;
+
$cli
->error()
->inject('error')
->inject('logError')
- ->action(function (Throwable $error, callable $logError) use ($taskName) {
+ ->action(function (Throwable $error, callable $logError) use ($taskName, &$exitCode) {
call_user_func_array($logError, [
$error,
'Task',
$taskName,
]);
+ $exitCode = 1;
Timer::clearAll();
});
@@ -348,3 +351,4 @@ $cli->shutdown()->action(fn () => Timer::clearAll());
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
require_once __DIR__ . '/init/span.php';
run($cli->run(...));
+Console::exit($exitCode);
diff --git a/app/config/console.php b/app/config/console.php
index 24de8a8cbb..0b0d6c5881 100644
--- a/app/config/console.php
+++ b/app/config/console.php
@@ -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)) : [],
diff --git a/app/config/errors.php b/app/config/errors.php
index ec2593d207..03fdc2bcc5 100644
--- a/app/config/errors.php
+++ b/app/config/errors.php
@@ -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.',
diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json
index 8e59c40123..3d667a36ad 100644
--- a/app/config/locale/translations/en.json
+++ b/app/config/locale/translations/en.json
@@ -57,21 +57,21 @@
"emails.recovery.thanks": "Thanks,",
"emails.recovery.buttonText": "Reset password",
"emails.recovery.signature": "{{project}} team",
- "emails.csvExport.success.subject": "Your CSV export is ready",
- "emails.csvExport.success.preview": "Your data export has been completed successfully.",
- "emails.csvExport.success.hello": "Hello {{user}},",
- "emails.csvExport.success.body": "Your CSV export is ready to download. Click the button below to download your data export.",
- "emails.csvExport.success.footer": "This download link will expire in 1 hour.",
- "emails.csvExport.success.thanks": "Thanks,",
- "emails.csvExport.success.buttonText": "Download CSV",
- "emails.csvExport.success.signature": "Appwrite team",
- "emails.csvExport.failure.subject": "Your CSV export failed - file too large",
- "emails.csvExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
- "emails.csvExport.failure.hello": "Hello {{user}},",
- "emails.csvExport.failure.body": "Your CSV export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
- "emails.csvExport.failure.footer": "If you have any questions, please contact our support team.",
- "emails.csvExport.failure.thanks": "Thanks,",
- "emails.csvExport.failure.signature": "{{project}} team",
+ "emails.dataExport.success.subject": "Your {{type}} export is ready",
+ "emails.dataExport.success.preview": "Your data export has been completed successfully.",
+ "emails.dataExport.success.hello": "Hello {{user}},",
+ "emails.dataExport.success.body": "Your {{type}} export is ready to download. Click the button below to download your data export.",
+ "emails.dataExport.success.footer": "This download link will expire in 1 hour.",
+ "emails.dataExport.success.thanks": "Thanks,",
+ "emails.dataExport.success.buttonText": "Download {{type}}",
+ "emails.dataExport.success.signature": "Appwrite team",
+ "emails.dataExport.failure.subject": "Your {{type}} export failed - file too large",
+ "emails.dataExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
+ "emails.dataExport.failure.hello": "Hello {{user}},",
+ "emails.dataExport.failure.body": "Your {{type}} export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
+ "emails.dataExport.failure.footer": "If you have any questions, please contact our support team.",
+ "emails.dataExport.failure.thanks": "Thanks,",
+ "emails.dataExport.failure.signature": "{{project}} team",
"emails.invitation.subject": "Invitation to {{team}} Team at {{project}}",
"emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}",
"emails.invitation.hello": "Hello {{user}},",
diff --git a/app/config/templates/function.php b/app/config/templates/function.php
index 6bdfb5ab80..df3a569705 100644
--- a/app/config/templates/function.php
+++ b/app/config/templates/function.php
@@ -31,9 +31,6 @@ class FunctionUseCases
public const DEV_TOOLS = 'dev-tools';
public const AUTH = 'auth';
- /**
- * @var array
- */
public static function getAll(): array
{
return [
diff --git a/app/config/templates/site.php b/app/config/templates/site.php
index 50a9fb8d5d..26f8e39817 100644
--- a/app/config/templates/site.php
+++ b/app/config/templates/site.php
@@ -25,9 +25,6 @@ class SiteUseCases
public const FORMS = 'forms';
public const DASHBOARD = 'dashboard';
- /**
- * @var array
- */
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',
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index 3d7db8f457..fb968d3972 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -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)) {
@@ -3760,7 +3937,7 @@ Http::post('/v1/account/recovery')
->setParam('userId', $profile->getId())
->setParam('tokenId', $recovery->getId())
->setUser($profile)
- ->setPayload(Response::showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']);
+ ->setPayload($response->showSensitive(fn () => $response->output($recovery, Response::MODEL_TOKEN)), sensitive: ['secret']);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -3861,7 +4038,7 @@ Http::put('/v1/account/recovery')
$queueForEvents
->setParam('userId', $profile->getId())
->setParam('tokenId', $recoveryDocument->getId())
- ->setPayload(Response::showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']);
+ ->setPayload($response->showSensitive(fn () => $response->output($recoveryDocument, Response::MODEL_TOKEN)), sensitive: ['secret']);
$response->dynamic($recoveryDocument, Response::MODEL_TOKEN);
});
@@ -4091,7 +4268,7 @@ Http::post('/v1/account/verifications/email')
$queueForEvents
->setParam('userId', $user->getId())
->setParam('tokenId', $verification->getId())
- ->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
+ ->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -4183,7 +4360,7 @@ Http::put('/v1/account/verifications/email')
$queueForEvents
->setParam('userId', $userId)
->setParam('tokenId', $verification->getId())
- ->setPayload(Response::showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
+ ->setPayload($response->showSensitive(fn () => $response->output($verification, Response::MODEL_TOKEN)), sensitive: ['secret']);
$response->dynamic($verification, Response::MODEL_TOKEN);
});
@@ -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();
});
diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php
index d6b4bb814d..c70102a6f1 100644
--- a/app/controllers/api/locale.php
+++ b/app/controllers/api/locale.php
@@ -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([
diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php
index 1ba5eb1119..95a8afc963 100644
--- a/app/controllers/api/messaging.php
+++ b/app/controllers/api/messaging.php
@@ -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 {
diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php
index 5a87293b49..45a663fb56 100644
--- a/app/controllers/api/migrations.php
+++ b/app/controllers/api/migrations.php
@@ -29,6 +29,7 @@ use Utopia\Migration\Resource;
use Utopia\Migration\Sources\Appwrite;
use Utopia\Migration\Sources\CSV;
use Utopia\Migration\Sources\Firebase;
+use Utopia\Migration\Sources\JSON;
use Utopia\Migration\Sources\NHost;
use Utopia\Migration\Sources\Supabase;
use Utopia\Migration\Transfer;
@@ -53,6 +54,15 @@ function getDatabaseTransferResourceServices(string $databaseType)
};
}
+function getDatabaseResourceType(string $databaseType): string
+{
+ return match($databaseType) {
+ DATABASE_TYPE_VECTORSDB => Resource::TYPE_DATABASE_VECTORSDB,
+ DATABASE_TYPE_DOCUMENTSDB => Resource::TYPE_DATABASE_DOCUMENTSDB,
+ default => Resource::TYPE_DATABASE,
+ };
+}
+
Http::post('/v1/migrations/appwrite')
->groups(['api', 'migrations'])
->desc('Create Appwrite migration')
@@ -447,6 +457,7 @@ Http::post('/v1/migrations/csv/imports')
}
$fileSize = $deviceForMigrations->getFileSize($newPath);
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
+ $resourceType = getDatabaseResourceType($databaseType);
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => $migrationId,
@@ -456,7 +467,7 @@ Http::post('/v1/migrations/csv/imports')
'destination' => Appwrite::getName(),
'resources' => $resources,
'resourceId' => $resourceId,
- 'resourceType' => Resource::TYPE_DATABASE,
+ 'resourceType' => $resourceType,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
@@ -565,16 +576,6 @@ Http::post('/v1/migrations/csv/exports')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
- $validator = new Documents(
- attributes: $collection->getAttribute('attributes', []),
- indexes: $collection->getAttribute('indexes', []),
- idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
- );
-
- if (!$validator->isValid($parsedQueries)) {
- throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
- }
-
// getting databasetype
$resources = explode(':', $resourceId);
$databaseId = $resources[0];
@@ -583,7 +584,23 @@ Http::post('/v1/migrations/csv/exports')
if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
}
+
+ // Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields
+ $isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]);
+
+ $validator = new Documents(
+ attributes: $collection->getAttribute('attributes', []),
+ indexes: $collection->getAttribute('indexes', []),
+ idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
+ supportForAttributes: !$isSchemaless,
+ );
+
+ if (!$validator->isValid($parsedQueries)) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+ }
+
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
+ $resourceType = getDatabaseResourceType($databaseType);
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
@@ -593,7 +610,7 @@ Http::post('/v1/migrations/csv/exports')
'destination' => CSV::getName(),
'resources' => $resources,
'resourceId' => $resourceId,
- 'resourceType' => Resource::TYPE_DATABASE,
+ 'resourceType' => $resourceType,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
@@ -624,6 +641,291 @@ Http::post('/v1/migrations/csv/exports')
->dynamic($migration, Response::MODEL_MIGRATION);
});
+Http::post('/v1/migrations/json/imports')
+ ->groups(['api', 'migrations'])
+ ->desc('Import documents from a JSON')
+ ->label('scope', 'migrations.write')
+ ->label('event', 'migrations.[migrationId].create')
+ ->label('audits.event', 'migration.create')
+ ->label('sdk', new Method(
+ namespace: 'migrations',
+ group: null,
+ name: 'createJSONImport',
+ description: '/docs/references/migrations/migration-json-import.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_ACCEPTED,
+ model: Response::MODEL_MIGRATION,
+ )
+ ]
+ ))
+ ->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
+ ->param('fileId', '', new UID(), 'File ID.')
+ ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.')
+ ->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('dbForPlatform')
+ ->inject('authorization')
+ ->inject('project')
+ ->inject('platform')
+ ->inject('deviceForFiles')
+ ->inject('deviceForMigrations')
+ ->inject('queueForEvents')
+ ->inject('queueForMigrations')
+ ->action(function (
+ string $bucketId,
+ string $fileId,
+ string $resourceId,
+ bool $internalFile,
+ Response $response,
+ Database $dbForProject,
+ Database $dbForPlatform,
+ Authorization $authorization,
+ Document $project,
+ array $platform,
+ Device $deviceForFiles,
+ Device $deviceForMigrations,
+ Event $queueForEvents,
+ Migration $queueForMigrations
+ ) {
+ $bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
+ if ($internalFile) {
+ return $dbForPlatform->getDocument('buckets', 'default');
+ }
+ return $dbForProject->getDocument('buckets', $bucketId);
+ });
+
+ if ($bucket->isEmpty()) {
+ throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
+ }
+
+ $file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
+ if ($file->isEmpty()) {
+ throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
+ }
+
+ $path = $file->getAttribute('path', '');
+ if (!$deviceForFiles->exists($path)) {
+ throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
+ }
+
+ // No encryption or compression on files above 20MB.
+ $hasEncryption = !empty($file->getAttribute('openSSLCipher'));
+ $compression = $file->getAttribute('algorithm', Compression::NONE);
+ $hasCompression = $compression !== Compression::NONE;
+
+ $migrationId = ID::unique();
+ $newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.json');
+
+ if ($hasEncryption || $hasCompression) {
+ $source = $deviceForFiles->read($path);
+
+ if ($hasEncryption) {
+ $source = OpenSSL::decrypt(
+ $source,
+ $file->getAttribute('openSSLCipher'),
+ System::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
+ 0,
+ hex2bin($file->getAttribute('openSSLIV')),
+ hex2bin($file->getAttribute('openSSLTag'))
+ );
+ }
+
+ if ($hasCompression) {
+ switch ($compression) {
+ case Compression::ZSTD:
+ $source = (new Zstd())->decompress($source);
+ break;
+ case Compression::GZIP:
+ $source = (new GZIP())->decompress($source);
+ break;
+ }
+ }
+
+ // Manual write after decryption and/or decompression
+ if (!$deviceForMigrations->write($newPath, $source, 'application/json')) {
+ throw new \Exception('Unable to copy file');
+ }
+ } elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) {
+ throw new \Exception('Unable to copy file');
+ }
+
+ $fileSize = $deviceForMigrations->getFileSize($newPath);
+
+ [$databaseId] = \explode(':', $resourceId, 2);
+ $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
+ if ($database->isEmpty()) {
+ throw new Exception(Exception::DATABASE_NOT_FOUND);
+ }
+ $databaseType = $database->getAttribute('type');
+ $resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
+ $resourceType = getDatabaseResourceType($databaseType);
+
+ $migration = $dbForProject->createDocument('migrations', new Document([
+ '$id' => $migrationId,
+ 'status' => 'pending',
+ 'stage' => 'init',
+ 'source' => JSON::getName(),
+ 'destination' => Appwrite::getName(),
+ 'resources' => $resources,
+ 'resourceId' => $resourceId,
+ 'resourceType' => $resourceType,
+ 'statusCounters' => '{}',
+ 'resourceData' => '{}',
+ 'errors' => [],
+ 'options' => [
+ 'path' => $newPath,
+ 'size' => $fileSize,
+ ],
+ ]));
+
+ $queueForEvents->setParam('migrationId', $migration->getId());
+
+ $queueForMigrations
+ ->setMigration($migration)
+ ->setProject($project)
+ ->setPlatform($platform)
+ ->trigger();
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_ACCEPTED)
+ ->dynamic($migration, Response::MODEL_MIGRATION);
+ });
+
+Http::post('/v1/migrations/json/exports')
+ ->groups(['api', 'migrations'])
+ ->desc('Export documents to JSON')
+ ->label('scope', 'migrations.write')
+ ->label('event', 'migrations.[migrationId].create')
+ ->label('audits.event', 'migration.create')
+ ->label('sdk', new Method(
+ namespace: 'migrations',
+ group: null,
+ name: 'createJSONExport',
+ description: '/docs/references/migrations/migration-json-export.md',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_ACCEPTED,
+ model: Response::MODEL_MIGRATION,
+ )
+ ]
+ ))
+ ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
+ ->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .json extension.')
+ ->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true)
+ ->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
+ ->param('notify', true, new Boolean(), 'Set to true to receive an email when the export is complete. Default is true.', true)
+ ->inject('user')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('dbForPlatform')
+ ->inject('authorization')
+ ->inject('project')
+ ->inject('platform')
+ ->inject('queueForEvents')
+ ->inject('queueForMigrations')
+ ->action(function (
+ string $resourceId,
+ string $filename,
+ array $columns,
+ array $queries,
+ bool $notify,
+ Document $user,
+ Response $response,
+ Database $dbForProject,
+ Database $dbForPlatform,
+ Authorization $authorization,
+ Document $project,
+ array $platform,
+ Event $queueForEvents,
+ Migration $queueForMigrations
+ ) {
+ try {
+ $parsedQueries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
+ $bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
+ if ($bucket->isEmpty()) {
+ throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
+ }
+
+ [$databaseId, $collectionId] = \explode(':', $resourceId, 2);
+ if (empty($databaseId)) {
+ throw new Exception(Exception::DATABASE_NOT_FOUND);
+ }
+ if (empty($collectionId)) {
+ throw new Exception(Exception::COLLECTION_NOT_FOUND);
+ }
+
+ $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
+ if ($database->isEmpty()) {
+ throw new Exception(Exception::DATABASE_NOT_FOUND);
+ }
+
+ $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
+ if ($collection->isEmpty()) {
+ throw new Exception(Exception::COLLECTION_NOT_FOUND);
+ }
+
+ $databaseType = $database->getAttribute('type');
+
+ // Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields
+ $isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]);
+
+ $validator = new Documents(
+ attributes: $collection->getAttribute('attributes', []),
+ indexes: $collection->getAttribute('indexes', []),
+ idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
+ supportForAttributes: !$isSchemaless,
+ );
+
+ if (!$validator->isValid($parsedQueries)) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+ }
+
+ $resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
+ $resourceType = getDatabaseResourceType($databaseType);
+
+ $migration = $dbForProject->createDocument('migrations', new Document([
+ '$id' => ID::unique(),
+ 'status' => 'pending',
+ 'stage' => 'init',
+ 'source' => Appwrite::getName(),
+ 'destination' => JSON::getName(),
+ 'resources' => $resources,
+ 'resourceId' => $resourceId,
+ 'resourceType' => $resourceType,
+ 'statusCounters' => '{}',
+ 'resourceData' => '{}',
+ 'errors' => [],
+ 'options' => [
+ 'bucketId' => 'default', // Always use internal bucket
+ 'filename' => $filename,
+ 'columns' => $columns,
+ 'queries' => $queries,
+ 'notify' => $notify,
+ 'userInternalId' => $user->getSequence(),
+ ],
+ ]));
+
+ $queueForEvents->setParam('migrationId', $migration->getId());
+
+ $queueForMigrations
+ ->setMigration($migration)
+ ->setProject($project)
+ ->setPlatform($platform)
+ ->trigger();
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_ACCEPTED)
+ ->dynamic($migration, Response::MODEL_MIGRATION);
+ });
+
Http::get('/v1/migrations')
->groups(['api', 'migrations'])
->desc('List migrations')
diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php
index 2fc20ba83f..4eb537d923 100644
--- a/app/controllers/api/projects.php
+++ b/app/controllers/api/projects.php
@@ -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')
diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php
index 3b21d4797d..51fc3d03c7 100644
--- a/app/controllers/api/users.php
+++ b/app/controllers/api/users.php
@@ -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')
diff --git a/app/controllers/general.php b/app/controllers/general.php
index 79929816d9..d10ad9a060 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -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) {
@@ -862,12 +868,12 @@ Http::init()
* Request format
*/
$route = $utopia->getRoute();
- Request::setRoute($route);
+ $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;
}
}
});
@@ -1012,7 +1019,7 @@ Http::init()
return;
}
$route = $request->getRoute();
- if ($route->getLabel('origin', false) === '*') {
+ if ($route?->getLabel('origin', false) === '*') {
return;
}
if (!$originValidator->isValid($origin)) {
@@ -1486,6 +1493,19 @@ Http::error()
'type' => $type,
];
+ // Add CORS headers to error responses so browsers can read the error.
+ // Wrapped in try-catch: if the error itself is a DB failure, resolving
+ // the cors resource (which depends on rule -> DB) would cascade.
+ // Uses override:true to avoid duplicate headers if init() already set them.
+ try {
+ $cors = $utopia->getResource('cors');
+ foreach ($cors->headers($request->getOrigin()) as $name => $value) {
+ $response->addHeader($name, $value, override: true);
+ }
+ } catch (Throwable) {
+ // Degrade gracefully - error response without CORS is no worse than before.
+ }
+
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
diff --git a/app/controllers/mock.php b/app/controllers/mock.php
index 712d4b7742..99713af430 100644
--- a/app/controllers/mock.php
+++ b/app/controllers/mock.php
@@ -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(),
]);
diff --git a/app/http.php b/app/http.php
index 1742bc7cdd..4532839727 100644
--- a/app/http.php
+++ b/app/http.php
@@ -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) {
diff --git a/app/init/registers.php b/app/init/registers.php
index 7c2f822fdd..68ccbd2097 100644
--- a/app/init/registers.php
+++ b/app/init/registers.php
@@ -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');
diff --git a/app/init/resources.php b/app/init/resources.php
index 3481e73e0b..8acecb8e3e 100644
--- a/app/init/resources.php
+++ b/app/init/resources.php
@@ -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()
diff --git a/app/realtime.php b/app/realtime.php
index 1c453ce05b..67cfb19e2a 100644
--- a/app/realtime.php
+++ b/app/realtime.php
@@ -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();
diff --git a/app/worker.php b/app/worker.php
index 71446ee94f..d573a60231 100644
--- a/app/worker.php
+++ b/app/worker.php
@@ -1,7 +1,6 @@
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) {
diff --git a/composer.json b/composer.json
index 65838a1615..0f4dcfb8db 100644
--- a/composer.json
+++ b/composer.json
@@ -72,8 +72,8 @@
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
- "utopia-php/messaging": "0.20.*",
- "utopia-php/migration": "1.8.*",
+ "utopia-php/messaging": "0.22.*",
+ "utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
diff --git a/composer.lock b/composer.lock
index 43355afb6e..2acdb20c8b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "f9225f2b580de0ccb796b2fb8c881384",
+ "content-hash": "4fe91e67f343fbe6deac1fdc7eda949f",
"packages": [
{
"name": "adhocore/jwt",
@@ -161,16 +161,16 @@
},
{
"name": "appwrite/php-runtimes",
- "version": "0.19.4",
+ "version": "0.19.5",
"source": {
"type": "git",
"url": "https://github.com/appwrite/runtimes.git",
- "reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b"
+ "reference": "aa2f7760cd0493c0880209b92df812c9386b3546"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/runtimes/zipball/eea9d1b3ca2540eab623b419c8afde09ef406c0b",
- "reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b",
+ "url": "https://api.github.com/repos/appwrite/runtimes/zipball/aa2f7760cd0493c0880209b92df812c9386b3546",
+ "reference": "aa2f7760cd0493c0880209b92df812c9386b3546",
"shasum": ""
},
"require": {
@@ -210,9 +210,9 @@
],
"support": {
"issues": "https://github.com/appwrite/runtimes/issues",
- "source": "https://github.com/appwrite/runtimes/tree/0.19.4"
+ "source": "https://github.com/appwrite/runtimes/tree/0.19.5"
},
- "time": "2026-02-17T10:04:39+00:00"
+ "time": "2026-04-01T01:39:23+00:00"
},
{
"name": "brick/math",
@@ -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,22 +4512,22 @@
],
"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",
- "version": "1.8.3",
+ "version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
- "reference": "8633523b3343d492427331b6eec53f020f6ab7a7"
+ "reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/migration/zipball/8633523b3343d492427331b6eec53f020f6ab7a7",
- "reference": "8633523b3343d492427331b6eec53f020f6ab7a7",
+ "url": "https://api.github.com/repos/utopia-php/migration/zipball/7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
+ "reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
"shasum": ""
},
"require": {
@@ -4567,9 +4567,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
- "source": "https://github.com/utopia-php/migration/tree/1.8.3"
+ "source": "https://github.com/utopia-php/migration/tree/1.9.1"
},
- "time": "2026-03-19T09:18:47+00:00"
+ "time": "2026-03-25T07:05:27+00:00"
},
{
"name": "utopia-php/mongo",
diff --git a/docs/references/health/get-queue-audits.md b/docs/references/health/get-queue-audits.md
index 75010cc2f4..bac075581f 100644
--- a/docs/references/health/get-queue-audits.md
+++ b/docs/references/health/get-queue-audits.md
@@ -1 +1 @@
-Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server.
\ No newline at end of file
+Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server.
diff --git a/docs/references/migrations/migration-json-export.md b/docs/references/migrations/migration-json-export.md
new file mode 100644
index 0000000000..8a955c5990
--- /dev/null
+++ b/docs/references/migrations/migration-json-export.md
@@ -0,0 +1 @@
+Export documents to a JSON file from your Appwrite database. This endpoint allows you to export documents to a JSON file stored in a secure internal bucket. You'll receive an email with a download link when the export is complete.
diff --git a/docs/references/migrations/migration-json-import.md b/docs/references/migrations/migration-json-import.md
new file mode 100644
index 0000000000..2eeeaf5619
--- /dev/null
+++ b/docs/references/migrations/migration-json-import.md
@@ -0,0 +1 @@
+Import documents from a JSON file into your Appwrite database. This endpoint allows you to import documents from a JSON file uploaded to Appwrite Storage bucket.
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
deleted file mode 100644
index 37e2579644..0000000000
--- a/phpstan-baseline.neon
+++ /dev/null
@@ -1,1750 +0,0 @@
-parameters:
- ignoreErrors:
- -
- message: '#^PHPDoc tag @var above a method has no effect\.$#'
- identifier: varTag.misplaced
- count: 1
- path: app/config/templates/function.php
-
- -
- message: '#^Array has 2 duplicate keys with value ''outputDirectory'' \(''outputDirectory'', ''outputDirectory''\)\.$#'
- identifier: array.duplicateKey
- count: 3
- path: app/config/templates/site.php
-
- -
- message: '#^PHPDoc tag @var above a method has no effect\.$#'
- identifier: varTag.misplaced
- count: 1
- path: app/config/templates/site.php
-
- -
- message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 2
- path: app/controllers/api/account.php
-
- -
- message: '#^Result of method Utopia\\Http\\Response\:\:noContent\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 1
- path: app/controllers/api/account.php
-
- -
- message: '#^Variable \$session might not be defined\.$#'
- identifier: variable.undefined
- count: 3
- path: app/controllers/api/account.php
-
- -
- message: '#^Variable \$output might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: app/controllers/api/locale.php
-
- -
- message: '#^Variable \$currentScheduledAt on left side of \?\? is never defined\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: app/controllers/api/messaging.php
-
- -
- message: '#^Result of method Utopia\\Http\\Response\:\:noContent\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 1
- path: app/controllers/api/projects.php
-
- -
- message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 2
- path: app/controllers/api/users.php
-
- -
- message: '#^Result of method Utopia\\Http\\Response\:\:noContent\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 1
- path: app/controllers/api/users.php
-
- -
- message: '#^Result of method Utopia\\Http\\Response\:\:redirect\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 2
- path: app/controllers/general.php
-
- -
- message: '#^Result of method Utopia\\Http\\Response\:\:send\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 1
- path: app/controllers/general.php
-
- -
- message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: app/controllers/general.php
-
- -
- message: '#^Variable \$deployment in PHPDoc tag @var does not exist\.$#'
- identifier: varTag.variableNotFound
- count: 1
- path: app/controllers/general.php
-
- -
- message: '#^Variable \$executionId on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: app/controllers/general.php
-
- -
- message: '#^Variable \$executionResponse might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: app/controllers/general.php
-
- -
- message: '#^Variable \$user might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: app/controllers/general.php
-
- -
- message: '#^Variable \$installation might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: app/controllers/mock.php
-
- -
- message: '#^Variable \$register might not be defined\.$#'
- identifier: variable.undefined
- count: 3
- path: app/http.php
-
- -
- message: '#^Variable \$tag on left side of \?\? always exists and is always null\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: app/init/database/filters.php
-
- -
- message: '#^Anonymous function has an unused use \$dsn\.$#'
- identifier: closure.unusedUse
- count: 1
- path: app/init/registers.php
-
- -
- message: '#^Binary operation "/" between string and string results in an error\.$#'
- identifier: binaryOp.invalid
- count: 1
- path: app/init/registers.php
-
- -
- message: '#^Property PHPMailer\\PHPMailer\\PHPMailer\:\:\$Port \(int\) does not accept string\.$#'
- identifier: assign.propertyType
- count: 1
- path: app/init/registers.php
-
- -
- message: '#^Variable \$providerConfig in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 2
- path: app/init/registers.php
-
- -
- message: '#^Variable \$providerConfig on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: app/init/registers.php
-
- -
- message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: app/init/resources.php
-
- -
- message: '#^Anonymous function has an unused use \$register\.$#'
- identifier: closure.unusedUse
- count: 4
- path: app/realtime.php
-
- -
- message: '#^Class Utopia\\Database\\Validator\\Authorization does not have a constructor and must be instantiated without any parameters\.$#'
- identifier: new.noConstructor
- count: 1
- path: app/realtime.php
-
- -
- message: '#^Function triggerStats\(\) returns void but does not have any side effects\.$#'
- identifier: void.pure
- count: 1
- path: app/realtime.php
-
- -
- message: '#^Binary operation "\*" between \-1 and string results in an error\.$#'
- identifier: binaryOp.invalid
- count: 3
- path: app/worker.php
-
- -
- message: '#^PHPDoc tag @return with type string is incompatible with native type int\.$#'
- identifier: return.phpDocType
- count: 1
- path: src/Appwrite/Auth/OAuth2.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$token$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Auth/OAuth2/Disqus.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$value$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Auth/Validator/PersonalData.php
-
- -
- message: '#^PHPDoc tag @param has invalid value \(DeviceDetector\)\: Unexpected token "\\n ", expected variable at offset 32 on line 2$#'
- identifier: phpDoc.parseError
- count: 1
- path: src/Appwrite/Detector/Detector.php
-
- -
- message: '#^PHPDoc tag @param has invalid value \(string\)\: Unexpected token "\\n ", expected variable at offset 24 on line 2$#'
- identifier: phpDoc.parseError
- count: 1
- path: src/Appwrite/Detector/Detector.php
-
- -
- message: '#^PHPDoc tag @var above a method has no effect\.$#'
- identifier: varTag.misplaced
- count: 1
- path: src/Appwrite/Docker/Compose.php
-
- -
- message: '#^PHPDoc tag @var above a method has no effect\.$#'
- identifier: varTag.misplaced
- count: 1
- path: src/Appwrite/Docker/Compose/Service.php
-
- -
- message: '#^PHPDoc tag @var above a method has no effect\.$#'
- identifier: varTag.misplaced
- count: 1
- path: src/Appwrite/Docker/Env.php
-
- -
- message: '#^PHPDoc tag @param has invalid value \(int port\)\: Unexpected token "port", expected variable at offset 50 on line 4$#'
- identifier: phpDoc.parseError
- count: 1
- path: src/Appwrite/Event/Mail.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$password$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Event/Mail.php
-
- -
- message: '#^PHPDoc tag @return with type string is incompatible with native type Appwrite\\Event\\Mail\.$#'
- identifier: return.phpDocType
- count: 1
- path: src/Appwrite/Event/Mail.php
-
- -
- message: '#^Method Appwrite\\Event\\Message\\Usage\:\:fromArray\(\) should return static\(Appwrite\\Event\\Message\\Usage\) but returns Appwrite\\Event\\Message\\Usage\.$#'
- identifier: return.type
- count: 1
- path: src/Appwrite/Event/Message/Usage.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$message$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Event/Messaging.php
-
- -
- message: '#^PHPDoc tag @return with type string is incompatible with native type Utopia\\Database\\Document\.$#'
- identifier: return.phpDocType
- count: 1
- path: src/Appwrite/Event/Messaging.php
-
- -
- message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getFunctionsEvents\(\) should return array\ but returns array\\>\.$#'
- identifier: return.type
- count: 1
- path: src/Appwrite/Functions/EventProcessor.php
-
- -
- message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getWebhooksEvents\(\) should return array\ but returns array\\>\.$#'
- identifier: return.type
- count: 1
- path: src/Appwrite/Functions/EventProcessor.php
-
- -
- message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Functions/EventProcessor.php
-
- -
- message: '#^Anonymous function has an unused use \$context\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/GraphQL/Resolvers.php
-
- -
- message: '#^Anonymous function has an unused use \$info\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/GraphQL/Resolvers.php
-
- -
- message: '#^Anonymous function has an unused use \$type\.$#'
- identifier: closure.unusedUse
- count: 5
- path: src/Appwrite/GraphQL/Resolvers.php
-
- -
- message: '#^Variable \$request in PHPDoc tag @var does not exist\.$#'
- identifier: varTag.variableNotFound
- count: 1
- path: src/Appwrite/GraphQL/Resolvers.php
-
- -
- message: '#^Variable \$response in PHPDoc tag @var does not exist\.$#'
- identifier: varTag.variableNotFound
- count: 1
- path: src/Appwrite/GraphQL/Resolvers.php
-
- -
- message: '#^Variable \$databaseId might not be defined\.$#'
- identifier: variable.undefined
- count: 5
- path: src/Appwrite/GraphQL/Schema.php
-
- -
- message: '#^Variable \$sdk in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/GraphQL/Schema.php
-
- -
- message: '#^Method Appwrite\\GraphQL\\Types\:\:assoc\(\) should return Appwrite\\GraphQL\\Types\\Json but returns GraphQL\\Type\\Definition\\Type\.$#'
- identifier: return.type
- count: 1
- path: src/Appwrite/GraphQL/Types.php
-
- -
- message: '#^Method Appwrite\\GraphQL\\Types\:\:inputFile\(\) should return Appwrite\\GraphQL\\Types\\InputFile but returns GraphQL\\Type\\Definition\\Type\.$#'
- identifier: return.type
- count: 1
- path: src/Appwrite/GraphQL/Types.php
-
- -
- message: '#^Method Appwrite\\GraphQL\\Types\:\:json\(\) should return Appwrite\\GraphQL\\Types\\Json but returns GraphQL\\Type\\Definition\\Type\.$#'
- identifier: return.type
- count: 1
- path: src/Appwrite/GraphQL/Types.php
-
- -
- message: '#^Class Appwrite\\Network\\Validator\\CNAME not found\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/GraphQL/Types/Mapper.php
-
- -
- message: '#^Class Utopia\\Validator\\Origin not found\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/GraphQL/Types/Mapper.php
-
- -
- message: '#^Unsafe access to private property Appwrite\\GraphQL\\Types\\Mapper\:\:\$models through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: src/Appwrite/GraphQL/Types/Mapper.php
-
- -
- message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getColumnImplementation\(\) through static\:\:\.$#'
- identifier: staticClassAccess.privateMethod
- count: 2
- path: src/Appwrite/GraphQL/Types/Mapper.php
-
- -
- message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getHashOptionsImplementation\(\) through static\:\:\.$#'
- identifier: staticClassAccess.privateMethod
- count: 1
- path: src/Appwrite/GraphQL/Types/Mapper.php
-
- -
- message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getUnionImplementation\(\) through static\:\:\.$#'
- identifier: staticClassAccess.privateMethod
- count: 1
- path: src/Appwrite/GraphQL/Types/Mapper.php
-
- -
- message: '#^Call to an undefined method Appwrite\\Migration\\Version\\V15\:\:documentsIterator\(\)\.$#'
- identifier: method.notFound
- count: 7
- path: src/Appwrite/Migration/Version/V15.php
-
- -
- message: '#^Method Appwrite\\Migration\\Version\\V15\:\:fixDocument\(\) should return Utopia\\Database\\Document but empty return statement found\.$#'
- identifier: return.empty
- count: 1
- path: src/Appwrite/Migration/Version/V15.php
-
- -
- message: '#^PHPDoc tag @return with type string\|false is not subtype of native type string\.$#'
- identifier: return.phpDocType
- count: 1
- path: src/Appwrite/Migration/Version/V15.php
-
- -
- message: '#^Variable \$tag on left side of \?\? always exists and is always null\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Migration/Version/V15.php
-
- -
- message: '#^Call to an undefined method Appwrite\\Migration\\Version\\V17\:\:documentsIterator\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/Appwrite/Migration/Version/V17.php
-
- -
- message: '#^Call to an undefined method Appwrite\\Migration\\Version\\V18\:\:documentsIterator\(\)\.$#'
- identifier: method.notFound
- count: 2
- path: src/Appwrite/Migration/Version/V18.php
-
- -
- message: '#^Call to an undefined method Appwrite\\Migration\\Version\\V19\:\:documentsIterator\(\)\.$#'
- identifier: method.notFound
- count: 4
- path: src/Appwrite/Migration/Version/V19.php
-
- -
- message: '#^Call to an undefined method Appwrite\\Migration\\Version\\V20\:\:documentsIterator\(\)\.$#'
- identifier: method.notFound
- count: 6
- path: src/Appwrite/Migration/Version/V20.php
-
- -
- message: '#^Variable \$query on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Migration/Version/V20.php
-
- -
- message: '#^Method Appwrite\\Network\\Cors\:\:headers\(\) should return array\ but returns array\\.$#'
- identifier: return.type
- count: 5
- path: src/Appwrite/Network/Cors.php
-
- -
- message: '#^Parameter &\$tag by\-ref type of method Appwrite\\OpenSSL\\OpenSSL\:\:encrypt\(\) expects null, string\|null given\.$#'
- identifier: parameterByRef.type
- count: 1
- path: src/Appwrite/OpenSSL/OpenSSL.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$projectId$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Platform/Action.php
-
- -
- message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Avatars/Http/Action.php
-
- -
- message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php
-
- -
- message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php
-
- -
- message: '#^Binary operation "%%" between string and 5 results in an error\.$#'
- identifier: binaryOp.invalid
- count: 1
- path: src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php
-
- -
- message: '#^Method Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Action\:\:setHttpPath\(\) should return Appwrite\\Platform\\Action but returns Utopia\\Platform\\Action\.$#'
- identifier: return.type
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php
-
- -
- message: '#^Method Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Collections\\Documents\\Action\:\:setHttpPath\(\) should return Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Action but returns Appwrite\\Platform\\Action\.$#'
- identifier: return.type
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
-
- -
- message: '#^Variable \$relations in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
-
- -
- message: '#^Anonymous function has an unused use \$dbForProject\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
-
- -
- message: '#^Variable \$document in PHPDoc tag @var does not match assigned variable \$collectionTableId\.$#'
- identifier: varTag.differentVariable
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
-
- -
- message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
-
- -
- message: '#^Variable \$enabled on left side of \?\?\= always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php
-
- -
- message: '#^Offset ''deviceBrand'' does not exist on int\.$#'
- identifier: offsetAccess.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php
-
- -
- message: '#^Offset ''deviceModel'' does not exist on int\.$#'
- identifier: offsetAccess.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php
-
- -
- message: '#^Offset ''deviceName'' does not exist on int\.$#'
- identifier: offsetAccess.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php
-
- -
- message: '#^Method Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Transactions\\Action\:\:setHttpPath\(\) should return Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Action but returns Appwrite\\Platform\\Action\.$#'
- identifier: return.type
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php
-
- -
- message: '#^Anonymous function has an unused use \$existing\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php
-
- -
- message: '#^Anonymous function has an unused use \$queueForEvents\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
-
- -
- message: '#^Anonymous function has an unused use \$queueForFunctions\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
-
- -
- message: '#^Anonymous function has an unused use \$queueForRealtime\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
-
- -
- message: '#^Anonymous function has an unused use \$queueForWebhooks\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
-
- -
- message: '#^Anonymous function has an unused use \$usage\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
-
- -
- message: '#^PHPDoc tag @throws with type Appwrite\\Platform\\Modules\\Databases\\Http\\Databases\\Transactions\\Structure\|Throwable\|Utopia\\Database\\Validator\\Authorization is not subtype of Throwable$#'
- identifier: throws.notThrowable
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
-
- -
- message: '#^Offset ''deviceBrand'' does not exist on int\.$#'
- identifier: offsetAccess.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
-
- -
- message: '#^Offset ''deviceModel'' does not exist on int\.$#'
- identifier: offsetAccess.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
-
- -
- message: '#^Offset ''deviceName'' does not exist on int\.$#'
- identifier: offsetAccess.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
-
- -
- message: '#^Variable \$device might not be defined\.$#'
- identifier: variable.undefined
- count: 5
- path: src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php
-
- -
- message: '#^Variable \$path might not be defined\.$#'
- identifier: variable.undefined
- count: 5
- path: src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php
-
- -
- message: '#^Call to method getAttribute\(\) on an unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
-
- -
- message: '#^Call to method getId\(\) on an unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
-
- -
- message: '#^Call to method isEmpty\(\) on an unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
-
- -
- message: '#^Method Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Create\:\:enqueueDeletes\(\) returns void but does not have any side effects\.$#'
- identifier: void.pure
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
-
- -
- message: '#^PHPDoc tag @var for variable \$session contains unknown class Appwrite\\Platform\\Modules\\Functions\\Http\\Executions\\Utopia\\Database\\Document\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
-
- -
- message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
-
- -
- message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
-
- -
- message: '#^Variable \$executionId on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
-
- -
- message: '#^Variable \$jwt on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
-
- -
- message: '#^Call to an undefined method Appwrite\\Event\\Event\:\:setSubscribers\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
-
- -
- message: '#^Variable \$enabled on left side of \?\?\= always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
-
- -
- message: '#^Undefined variable\: \$cpus$#'
- identifier: variable.undefined
- count: 3
- path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
-
- -
- message: '#^Undefined variable\: \$memory$#'
- identifier: variable.undefined
- count: 3
- path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
-
- -
- message: '#^Variable \$deployment might not be defined\.$#'
- identifier: variable.undefined
- count: 4
- path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
-
- -
- message: '#^Variable \$logsAfter on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
-
- -
- message: '#^Variable \$logsBefore on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
-
- -
- message: '#^Variable \$providerCommitHash on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
-
- -
- message: '#^Variable \$rule in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
-
- -
- message: '#^Result of method Appwrite\\Utopia\\Response\:\:dynamic\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 1
- path: src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php
-
- -
- message: '#^Variable \$device might not be defined\.$#'
- identifier: variable.undefined
- count: 5
- path: src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php
-
- -
- message: '#^Variable \$path might not be defined\.$#'
- identifier: variable.undefined
- count: 5
- path: src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php
-
- -
- message: '#^Variable \$enabled on left side of \?\?\= always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
-
- -
- message: '#^Variable \$iv might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php
-
- -
- message: '#^Variable \$tag might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php
-
- -
- message: '#^Variable \$allowedFileExtensions on left side of \?\?\= always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php
-
- -
- message: '#^Variable \$antivirus on left side of \?\?\= always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php
-
- -
- message: '#^Variable \$enabled on left side of \?\?\= always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php
-
- -
- message: '#^Variable \$transformations on left side of \?\?\= always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php
-
- -
- message: '#^Caught class Appwrite\\Platform\\Modules\\Teams\\Http\\Memberships\\Throwable not found\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
-
- -
- message: '#^Variable \$email in empty\(\) always exists and is always falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
-
- -
- message: '#^Variable \$hash might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
-
- -
- message: '#^Variable \$invitee might not be defined\.$#'
- identifier: variable.undefined
- count: 14
- path: src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
-
- -
- message: '#^Variable \$logBase might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
-
- -
- message: '#^Variable \$previewUrl in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
-
- -
- message: '#^Variable \$repositoryName in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
-
- -
- message: '#^Variable \$rule in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
-
- -
- message: '#^Result of method Utopia\\Http\\Response\:\:redirect\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 2
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php
-
- -
- message: '#^Undefined variable\: \$redirectFailure$#'
- identifier: variable.undefined
- count: 2
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php
-
- -
- message: '#^Variable \$redirectFailure in empty\(\) is never defined\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php
-
- -
- message: '#^Result of method Appwrite\\Utopia\\Response\:\:json\(\) \(void\) is used\.$#'
- identifier: method.void
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
-
- -
- message: '#^Variable \$logBase might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
-
- -
- message: '#^Variable \$previewUrl in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
-
- -
- message: '#^Variable \$repositoryName in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
-
- -
- message: '#^Variable \$rule in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
-
- -
- message: '#^Variable \$providerConfig on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Tasks/Doctor.php
-
- -
- message: '#^Variable \$compose in isset\(\) always exists and is not nullable\.$#'
- identifier: isset.variable
- count: 1
- path: src/Appwrite/Platform/Tasks/Install.php
-
- -
- message: '#^Variable \$prUrls might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: src/Appwrite/Platform/Tasks/SDKs.php
-
- -
- message: '#^Anonymous function has an unused use \$dbForPlatform\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Tasks/StatsResources.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$getProjectDB$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Platform/Workers/Audits.php
-
- -
- message: '#^Anonymous function has an unused use \$certificates\.$#'
- identifier: closure.unusedUse
- count: 2
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^Anonymous function has an unused use \$dbForPlatform\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^Anonymous function has an unused use \$dbForProject\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^Anonymous function has an unused use \$project\.$#'
- identifier: closure.unusedUse
- count: 6
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^Anonymous function has an unused use \$resourceType\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$build$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$dbForPlatform$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$target$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^PHPDoc tag @throws with type Appwrite\\Extend\\Exception\|Utopia\\Database\\Exception\|Utopia\\Database\\Validator\\Authorization is not subtype of Throwable$#'
- identifier: throws.notThrowable
- count: 2
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^PHPDoc tag @throws with type Appwrite\\Extend\\Exception\|Utopia\\Database\\Validator\\Authorization is not subtype of Throwable$#'
- identifier: throws.notThrowable
- count: 1
- path: src/Appwrite/Platform/Workers/Deletes.php
-
- -
- message: '#^PHPDoc tag @throws with type Appwrite\\Platform\\Workers\\Exception is not subtype of Throwable$#'
- identifier: throws.notThrowable
- count: 2
- path: src/Appwrite/Platform/Workers/Functions.php
-
- -
- message: '#^Variable \$body on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 2
- path: src/Appwrite/Platform/Workers/Functions.php
-
- -
- message: '#^Variable \$errorCode might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: src/Appwrite/Platform/Workers/Functions.php
-
- -
- message: '#^Variable \$executionId on left side of \?\? always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Workers/Functions.php
-
- -
- message: '#^Binary operation "\+\=" between 0 and array\\|int\|string\>\|int\|string results in an error\.$#'
- identifier: assignOp.invalid
- count: 1
- path: src/Appwrite/Platform/Workers/Messaging.php
-
- -
- message: '#^Variable \$provider might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: src/Appwrite/Platform/Workers/Messaging.php
-
- -
- message: '#^Variable \$aggregatedResources might not be defined\.$#'
- identifier: variable.undefined
- count: 1
- path: src/Appwrite/Platform/Workers/Migrations.php
-
- -
- message: '#^Anonymous function has an unused use \$dbForLogs\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Workers/StatsResources.php
-
- -
- message: '#^Anonymous function has an unused use \$sequence\.$#'
- identifier: closure.unusedUse
- count: 1
- path: src/Appwrite/Platform/Workers/StatsUsage.php
-
- -
- message: '#^Callable callable\(\)\: Utopia\\Database\\Database invoked with 1 parameter, 0 required\.$#'
- identifier: arguments.count
- count: 2
- path: src/Appwrite/Platform/Workers/StatsUsage.php
-
- -
- message: '#^Variable \$curlError on left side of \?\? is never defined\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/Platform/Workers/Webhooks.php
-
- -
- message: '#^Unsafe usage of new static\(\)\.$#'
- identifier: new.static
- count: 3
- path: src/Appwrite/Promises/Promise.php
-
- -
- message: '#^Property Appwrite\\SDK\\Method\:\:\$hide on left side of \?\? is not nullable nor uninitialized\.$#'
- identifier: nullCoalesce.initializedProperty
- count: 1
- path: src/Appwrite/SDK/Method.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$services$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/SDK/Specification/Format.php
-
- -
- message: '#^PHPDoc tag @return with type Appwrite\\SDK\\Specification\\Format is incompatible with native type array\.$#'
- identifier: return.phpDocType
- count: 1
- path: src/Appwrite/SDK/Specification/Format.php
-
- -
- message: '#^Cannot unset offset ''schema'' on array\{description\: ''No content'', content\?\: non\-empty\-array\<''''\|''\*/\*''\|''application/json''\|''image/\*''\|''image/png''\|''multipart/form\-data''\|''text/html''\|''text/plain'', array\{schema\: array\{''\$ref''\: non\-falsy\-string\}\}\|array\{schema\: array\{oneOf\: array\\}\}\>\}\.$#'
- identifier: unset.offset
- count: 1
- path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php
-
- -
- message: '#^Class Utopia\\Database\\Validator\\DatetimeValidator not found\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php
-
- -
- message: '#^Class Utopia\\Validator\\Length not found\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php
-
- -
- message: '#^Class Utopia\\Validator\\Mock not found\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php
-
- -
- message: '#^Offset ''securityDefinitions'' on array\{openapi\: ''3\.0\.0'', info\: array\{version\: string, title\: string, description\: string, termsOfService\: string, contact\: array\{name\: string, url\: string, email\: string\}, license\: array\{name\: ''BSD\-3\-Clause'', url\: ''https\://raw…''\}\}, servers\: array\{array\{url\: string\}, array\{url\: string\}\}, paths\: array\{\}, tags\: array, components\: array\{schemas\: array\{\}, securitySchemes\: array\}, externalDocs\: array\{description\: string, url\: string\}\} in isset\(\) does not exist\.$#'
- identifier: isset.offset
- count: 1
- path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php
-
- -
- message: '#^Variable \$desc on left side of \?\?\= always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php
-
- -
- message: '#^Variable \$validator in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/SDK/Specification/Format/OpenAPI3.php
-
- -
- message: '#^Class Utopia\\Database\\Validator\\DatetimeValidator not found\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/SDK/Specification/Format/Swagger2.php
-
- -
- message: '#^Class Utopia\\Validator\\Length not found\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/SDK/Specification/Format/Swagger2.php
-
- -
- message: '#^Class Utopia\\Validator\\Mock not found\.$#'
- identifier: class.notFound
- count: 1
- path: src/Appwrite/SDK/Specification/Format/Swagger2.php
-
- -
- message: '#^PHPDoc tag @var with type Appwrite\\SDK\\Method is not subtype of native type \*NEVER\*\.$#'
- identifier: varTag.nativeType
- count: 1
- path: src/Appwrite/SDK/Specification/Format/Swagger2.php
-
- -
- message: '#^Variable \$additionalMethods in empty\(\) always exists and is always falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/SDK/Specification/Format/Swagger2.php
-
- -
- message: '#^Variable \$desc on left side of \?\?\= always exists and is not nullable\.$#'
- identifier: nullCoalesce.variable
- count: 1
- path: src/Appwrite/SDK/Specification/Format/Swagger2.php
-
- -
- message: '#^Variable \$sdk in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/SDK/Specification/Format/Swagger2.php
-
- -
- message: '#^Variable \$validator in empty\(\) always exists and is not falsy\.$#'
- identifier: empty.variable
- count: 1
- path: src/Appwrite/SDK/Specification/Format/Swagger2.php
-
- -
- message: '#^PHPDoc tag @var above a method has no effect\.$#'
- identifier: varTag.misplaced
- count: 2
- path: src/Appwrite/Template/Template.php
-
- -
- message: '#^PHPDoc tag @param has invalid value \(Document \$this\)\: Unexpected token "\$this", expected variable at offset 69 on line 4$#'
- identifier: phpDoc.parseError
- count: 1
- path: src/Appwrite/Utopia/Database/Documents/User.php
-
- -
- message: '#^PHPDoc tag @param references unknown parameter\: \$sessions$#'
- identifier: parameter.notFound
- count: 1
- path: src/Appwrite/Utopia/Database/Documents/User.php
-
- -
- message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:appendSymbol\(\) through static\:\:\.$#'
- identifier: staticClassAccess.privateMethod
- count: 4
- path: src/Appwrite/Utopia/Request/Filters/V17.php
-
- -
- message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:isSpecialChar\(\) through static\:\:\.$#'
- identifier: staticClassAccess.privateMethod
- count: 1
- path: src/Appwrite/Utopia/Request/Filters/V17.php
-
- -
- message: '#^PHPDoc tag @param has invalid value \(callable The callback to show sensitive information for\)\: Unexpected token "The", expected variable at offset 91 on line 4$#'
- identifier: phpDoc.parseError
- count: 1
- path: src/Appwrite/Utopia/Response.php
-
- -
- message: '#^PHPDoc tag @return with type Appwrite\\Utopia\\Response\\Filter is incompatible with native type array\.$#'
- identifier: return.phpDocType
- count: 1
- path: src/Appwrite/Utopia/Response.php
-
- -
- message: '#^PHPDoc tag @return with type string is incompatible with native type Utopia\\Database\\Document\.$#'
- identifier: return.phpDocType
- count: 1
- path: src/Appwrite/Utopia/Response/Model/User.php
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Databases\\Legacy\\DatabasesStringTypesTest\:\:\$setupCache through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/Databases/Legacy/DatabasesStringTypesTest.php
- -
- message: '#^Call to an undefined method Tests\\E2E\\Services\\Databases\\Permissions\\LegacyPermissionsGuestTest\:\:getIndexUrl\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: tests/e2e/Services/Databases/Permissions/LegacyPermissionsGuestTest.php
-
- -
- message: '#^Call to an undefined method Tests\\E2E\\Services\\Databases\\Permissions\\LegacyPermissionsMemberTest\:\:getIndexUrl\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: tests/e2e/Services/Databases/Permissions/LegacyPermissionsMemberTest.php
-
- -
- message: '#^Call to an undefined method Tests\\E2E\\Services\\Databases\\Permissions\\LegacyPermissionsTeamTest\:\:getIndexUrl\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: tests/e2e/Services/Databases/Permissions/LegacyPermissionsTeamTest.php
-
- -
- message: '#^Method PHPUnit\\Framework\\TestCase\:\:addToAssertionCount\(\) invoked with 2 parameters, 1 required\.$#'
- identifier: arguments.count
- count: 1
- path: tests/e2e/Services/Functions/FunctionsCustomClientTest.php
-
- -
- message: '#^Variable \$largeTag might not be defined\.$#'
- identifier: variable.undefined
- count: 8
- path: tests/e2e/Services/Functions/FunctionsCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedDeployment through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/GraphQL/FunctionsClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedExecution through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/FunctionsClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedFunction through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/GraphQL/FunctionsClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedDeployment through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/FunctionsServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedExecution through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/FunctionsServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedFunction through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/FunctionsServerTest.php
-
- -
- message: '#^Binary operation "\+" between string and 1 results in an error\.$#'
- identifier: binaryOp.invalid
- count: 1
- path: tests/e2e/Services/GraphQL/Legacy/AbuseTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$bulkData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$collection through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$database through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$document through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedEmail through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 2
- path: tests/e2e/Services/GraphQL/MessagingTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedProviders through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 8
- path: tests/e2e/Services/GraphQL/MessagingTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedPush through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/MessagingTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedSms through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/MessagingTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedSubscriber through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/MessagingTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedTopic through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 9
- path: tests/e2e/Services/GraphQL/MessagingTest.php
-
- -
- message: '#^Variable \$from in empty\(\) is never defined\.$#'
- identifier: empty.variable
- count: 1
- path: tests/e2e/Services/GraphQL/MessagingTest.php
-
- -
- message: '#^Method Tests\\E2E\\Services\\GraphQL\\StorageClientTest\:\:testGetFileDownload\(\) should return array but return statement is missing\.$#'
- identifier: return.missing
- count: 1
- path: tests/e2e/Services/GraphQL/StorageClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageClientTest\:\:\$cachedBucket through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/GraphQL/StorageClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageClientTest\:\:\$cachedFile through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/StorageClientTest.php
-
- -
- message: '#^Method Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:testGetFileDownload\(\) should return array but return statement is missing\.$#'
- identifier: return.missing
- count: 1
- path: tests/e2e/Services/GraphQL/StorageServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:\$cachedBucket through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/StorageServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:\$cachedFile through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 6
- path: tests/e2e/Services/GraphQL/StorageServerTest.php
-
- -
- message: '#^Binary operation "\+" between string and 1 results in an error\.$#'
- identifier: binaryOp.invalid
- count: 1
- path: tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedBooleanColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedBulkData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedDatabase through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedDatetimeColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedEmailColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedEnumColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedFloatColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIPColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIndexData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 7
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIntegerColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedRelationshipColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedRowData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedStringColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedTableData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedURLColumnData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsClientTest\:\:\$cachedMembership through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/TeamsClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsClientTest\:\:\$cachedTeam through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/GraphQL/TeamsClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedMembership through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/GraphQL/TeamsServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedTeam through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/GraphQL/TeamsServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedTeamWithPrefs through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/GraphQL/TeamsServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\UsersTest\:\:\$cachedUser through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/GraphQL/UsersTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\UsersTest\:\:\$cachedUserTarget through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 5
- path: tests/e2e/Services/GraphQL/UsersTest.php
-
- -
- message: '#^Variable \$from in empty\(\) is never defined\.$#'
- identifier: empty.variable
- count: 1
- path: tests/e2e/Services/Messaging/MessagingConsoleClientTest.php
-
- -
- message: '#^Variable \$from in empty\(\) is never defined\.$#'
- identifier: empty.variable
- count: 1
- path: tests/e2e/Services/Messaging/MessagingCustomClientTest.php
-
- -
- message: '#^Variable \$from in empty\(\) is never defined\.$#'
- identifier: empty.variable
- count: 1
- path: tests/e2e/Services/Messaging/MessagingCustomServerTest.php
-
- -
- message: '#^Anonymous function has an unused use \$databaseId\.$#'
- identifier: closure.unusedUse
- count: 5
- path: tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php
-
- -
- message: '#^Anonymous function has an unused use \$tableId\.$#'
- identifier: closure.unusedUse
- count: 5
- path: tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php
-
- -
- message: '#^Variable \$largeFile might not be defined\.$#'
- identifier: variable.undefined
- count: 8
- path: tests/e2e/Services/Storage/StorageConsoleClientTest.php
-
- -
- message: '#^Variable \$largeFile might not be defined\.$#'
- identifier: variable.undefined
- count: 8
- path: tests/e2e/Services/Storage/StorageCustomClientTest.php
-
- -
- message: '#^Variable \$largeFile might not be defined\.$#'
- identifier: variable.undefined
- count: 8
- path: tests/e2e/Services/Storage/StorageCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\TablesDB\\DatabasesStringTypesTest\:\:\$setupCache through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/TablesDB/DatabasesStringTypesTest.php
-
- -
- message: '#^Call to an undefined method Tests\\E2E\\Services\\TablesDB\\Permissions\\TablesDBPermissionsGuestTest\:\:getIndexUrl\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: tests/e2e/Services/TablesDB/Permissions/TablesDBPermissionsGuestTest.php
-
- -
- message: '#^Call to an undefined method Tests\\E2E\\Services\\TablesDB\\Permissions\\TablesDBPermissionsMemberTest\:\:getIndexUrl\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: tests/e2e/Services/TablesDB/Permissions/TablesDBPermissionsMemberTest.php
-
- -
- message: '#^Call to an undefined method Tests\\E2E\\Services\\TablesDB\\Permissions\\TablesDBPermissionsTeamTest\:\:getIndexUrl\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: tests/e2e/Services/TablesDB/Permissions/TablesDBPermissionsTeamTest.php
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$bucketAndFileData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/Tokens/TokensConsoleClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$tokenData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/Tokens/TokensConsoleClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomClientTest\:\:\$bucketAndFileData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/Tokens/TokensCustomClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomServerTest\:\:\$bucketAndFileData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/Tokens/TokensCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomServerTest\:\:\$tokenData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/Tokens/TokensCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Users\\UsersCustomServerTest\:\:\$cachedHashedPasswordUsers through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 2
- path: tests/e2e/Services/Users/UsersCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Users\\UsersCustomServerTest\:\:\$cachedUser through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 7
- path: tests/e2e/Services/Users/UsersCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Users\\UsersCustomServerTest\:\:\$cachedUserTarget through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 7
- path: tests/e2e/Services/Users/UsersCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Users\\UsersCustomServerTest\:\:\$userEmailUpdated through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/Users/UsersCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Users\\UsersCustomServerTest\:\:\$userNameUpdated through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/Users/UsersCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\Users\\UsersCustomServerTest\:\:\$userNumberUpdated through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 3
- path: tests/e2e/Services/Users/UsersCustomServerTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\VCS\\VCSConsoleClientTest\:\:\$cachedFunctionData through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/VCS/VCSConsoleClientTest.php
-
- -
- message: '#^Unsafe access to private property Tests\\E2E\\Services\\VCS\\VCSConsoleClientTest\:\:\$cachedInstallationId through static\:\:\.$#'
- identifier: staticClassAccess.privateProperty
- count: 4
- path: tests/e2e/Services/VCS/VCSConsoleClientTest.php
-
- -
- message: '#^Unsafe call to private method Tests\\Unit\\Auth\\KeyTest\:\:generateKey\(\) through static\:\:\.$#'
- identifier: staticClassAccess.privateMethod
- count: 3
- path: tests/unit/Auth/KeyTest.php
-
- -
- message: '#^Call to an undefined method Utopia\\Queue\\Publisher\:\:getEvents\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: tests/unit/Event/EventTest.php
-
- -
- message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
- identifier: class.notFound
- count: 6
- path: tests/unit/Utopia/Response/Filters/V16Test.php
-
- -
- message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V16Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V16\.$#'
- identifier: assign.propertyType
- count: 1
- path: tests/unit/Utopia/Response/Filters/V16Test.php
-
- -
- message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V16Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
- identifier: class.notFound
- count: 1
- path: tests/unit/Utopia/Response/Filters/V16Test.php
-
- -
- message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
- identifier: class.notFound
- count: 5
- path: tests/unit/Utopia/Response/Filters/V17Test.php
-
- -
- message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V17Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V17\.$#'
- identifier: assign.propertyType
- count: 1
- path: tests/unit/Utopia/Response/Filters/V17Test.php
-
- -
- message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V17Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
- identifier: class.notFound
- count: 1
- path: tests/unit/Utopia/Response/Filters/V17Test.php
-
- -
- message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
- identifier: class.notFound
- count: 4
- path: tests/unit/Utopia/Response/Filters/V18Test.php
-
- -
- message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V18Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V18\.$#'
- identifier: assign.propertyType
- count: 1
- path: tests/unit/Utopia/Response/Filters/V18Test.php
-
- -
- message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V18Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
- identifier: class.notFound
- count: 1
- path: tests/unit/Utopia/Response/Filters/V18Test.php
-
- -
- message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
- identifier: class.notFound
- count: 11
- path: tests/unit/Utopia/Response/Filters/V19Test.php
-
- -
- message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V19Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V19\.$#'
- identifier: assign.propertyType
- count: 1
- path: tests/unit/Utopia/Response/Filters/V19Test.php
-
- -
- message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V19Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
- identifier: class.notFound
- count: 1
- path: tests/unit/Utopia/Response/Filters/V19Test.php
diff --git a/phpstan.neon b/phpstan.neon
index b87ad46eca..85d18fd44d 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -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
-
diff --git a/src/Appwrite/Auth/OAuth2.php b/src/Appwrite/Auth/OAuth2.php
index 9358c89547..a8a2d175b5 100644
--- a/src/Appwrite/Auth/OAuth2.php
+++ b/src/Appwrite/Auth/OAuth2.php
@@ -155,7 +155,7 @@ abstract class OAuth2
/**
* @param string $code
*
- * @return string
+ * @return int
*/
public function getAccessTokenExpiry(string $code): int
{
diff --git a/src/Appwrite/Auth/OAuth2/Disqus.php b/src/Appwrite/Auth/OAuth2/Disqus.php
index 58b7f48914..738c6d503e 100644
--- a/src/Appwrite/Auth/OAuth2/Disqus.php
+++ b/src/Appwrite/Auth/OAuth2/Disqus.php
@@ -108,7 +108,7 @@ class Disqus extends OAuth2
}
/**
- * @param string $token
+ * @param string $accessToken
*
* @return string
*/
diff --git a/src/Appwrite/Auth/OAuth2/Yahoo.php b/src/Appwrite/Auth/OAuth2/Yahoo.php
index c70a2fb6c9..1a6c6b860d 100644
--- a/src/Appwrite/Auth/OAuth2/Yahoo.php
+++ b/src/Appwrite/Auth/OAuth2/Yahoo.php
@@ -23,8 +23,9 @@ class Yahoo extends OAuth2
* @var array
*/
protected array $scopes = [
- 'sdct-r',
- 'sdpp-w',
+ 'openid',
+ 'profile',
+ 'email',
];
/**
diff --git a/src/Appwrite/Auth/Validator/PersonalData.php b/src/Appwrite/Auth/Validator/PersonalData.php
index 8eaae002f6..3b09839bd1 100644
--- a/src/Appwrite/Auth/Validator/PersonalData.php
+++ b/src/Appwrite/Auth/Validator/PersonalData.php
@@ -33,7 +33,7 @@ class PersonalData extends Password
/**
* Is valid.
*
- * @param mixed $value
+ * @param mixed $password
*
* @return bool
*/
diff --git a/src/Appwrite/Detector/Detector.php b/src/Appwrite/Detector/Detector.php
index 61286835f5..73259673dd 100644
--- a/src/Appwrite/Detector/Detector.php
+++ b/src/Appwrite/Detector/Detector.php
@@ -6,14 +6,8 @@ use DeviceDetector\DeviceDetector;
class Detector
{
- /**
- * @param string
- */
protected $userAgent = '';
- /**
- * @param DeviceDetector
- */
protected $detctor;
/**
diff --git a/src/Appwrite/Docker/Compose.php b/src/Appwrite/Docker/Compose.php
index 241e281ed8..9ea6420d2d 100644
--- a/src/Appwrite/Docker/Compose.php
+++ b/src/Appwrite/Docker/Compose.php
@@ -12,9 +12,6 @@ class Compose
*/
protected $compose = [];
- /**
- * @var string $data
- */
public function __construct(string $data)
{
$this->compose = yaml_parse($data);
diff --git a/src/Appwrite/Docker/Compose/Service.php b/src/Appwrite/Docker/Compose/Service.php
index a3f9c91253..87699aaeba 100644
--- a/src/Appwrite/Docker/Compose/Service.php
+++ b/src/Appwrite/Docker/Compose/Service.php
@@ -11,9 +11,6 @@ class Service
*/
protected $service = [];
- /**
- * @var string $path
- */
public function __construct(array $service)
{
$this->service = $service;
diff --git a/src/Appwrite/Docker/Env.php b/src/Appwrite/Docker/Env.php
index 3bf6fb2d50..af5e4f11e2 100644
--- a/src/Appwrite/Docker/Env.php
+++ b/src/Appwrite/Docker/Env.php
@@ -9,9 +9,6 @@ class Env
*/
protected $vars = [];
- /**
- * @var string $data
- */
public function __construct(string $data)
{
$data = explode("\n", $data);
diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php
index bf6339f8a0..ae75e3924f 100644
--- a/src/Appwrite/Event/Event.php
+++ b/src/Appwrite/Event/Event.php
@@ -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();
diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php
index 2d12aa542c..38d7a27c11 100644
--- a/src/Appwrite/Event/Mail.php
+++ b/src/Appwrite/Event/Mail.php
@@ -101,7 +101,8 @@ class Mail extends Event
/**
* Sets preview for the mail event.
*
- * @return string
+ * @param string $preview
+ * @return self
*/
public function setPreview(string $preview): self
{
@@ -115,7 +116,7 @@ class Mail extends Event
*
* @return string
*/
- public function getPreview(string $preview): string
+ public function getPreview(): string
{
return $this->preview;
}
@@ -181,7 +182,7 @@ class Mail extends Event
/**
* Set SMTP port
*
- * @param int port
+ * @param int $port
* @return self
*/
public function setSmtpPort(int $port): self
@@ -217,7 +218,7 @@ class Mail extends Event
/**
* Set SMTP secure
*
- * @param string $password
+ * @param string $secure
* @return self
*/
public function setSmtpSecure(string $secure): self
diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php
index 776188d5b5..c72bc8ae2a 100644
--- a/src/Appwrite/Event/Message/Usage.php
+++ b/src/Appwrite/Event/Message/Usage.php
@@ -40,7 +40,8 @@ class Usage extends Base
*/
public static function fromArray(array $data): static
{
- return new self(
+ /** @phpstan-ignore new.static (subclass constructors are backwards-compatible via optional params) */
+ return new static(
project: new Document($data['project'] ?? []),
metrics: $data['metrics'] ?? [],
reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []),
diff --git a/src/Appwrite/Event/Messaging.php b/src/Appwrite/Event/Messaging.php
index 8c13185e0b..9895d52ec2 100644
--- a/src/Appwrite/Event/Messaging.php
+++ b/src/Appwrite/Event/Messaging.php
@@ -86,7 +86,7 @@ class Messaging extends Event
/**
* Returns message document for the messaging event.
*
- * @return string
+ * @return Document
*/
public function getMessage(): Document
{
@@ -96,7 +96,7 @@ class Messaging extends Event
/**
* Sets message ID for the messaging event.
*
- * @param string $message
+ * @param string $messageId
* @return self
*/
public function setMessageId(string $messageId): self
diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php
index f7c76d3800..c015d2fdcb 100644
--- a/src/Appwrite/Extend/Exception.php
+++ b/src/Appwrite/Extend/Exception.php
@@ -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';
diff --git a/src/Appwrite/Functions/EventProcessor.php b/src/Appwrite/Functions/EventProcessor.php
index e9c3b7241a..d41ee56c5d 100644
--- a/src/Appwrite/Functions/EventProcessor.php
+++ b/src/Appwrite/Functions/EventProcessor.php
@@ -8,6 +8,18 @@ use Utopia\Database\Query;
class EventProcessor
{
+ /**
+ * @param array $events
+ * @return array
+ */
+ private function getEventMap(array $events): array
+ {
+ return \array_fill_keys(
+ \array_map('strval', \array_unique($events)),
+ true
+ );
+ }
+
/**
* Get function events for a project, using Redis cache
* @param Document|null $project
@@ -26,7 +38,7 @@ class EventProcessor
$cacheKey = \sprintf(
'%s-cache-%s:%s:%s:project:%s:functions:events',
$dbForProject->getCacheName(),
- $hostname ?? '',
+ $hostname,
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$project->getId()
@@ -36,7 +48,9 @@ class EventProcessor
$cachedFunctionEvents = $dbForProject->getCache()->load($cacheKey, $ttl);
if ($cachedFunctionEvents !== false) {
- return \json_decode($cachedFunctionEvents, true) ?? [];
+ $decoded = \json_decode($cachedFunctionEvents, true);
+
+ return \is_array($decoded) ? $this->getEventMap(\array_keys($decoded)) : [];
}
$events = [];
@@ -63,7 +77,7 @@ class EventProcessor
}
}
- $uniqueEvents = \array_flip(\array_unique($events));
+ $uniqueEvents = $this->getEventMap($events);
$dbForProject->getCache()->save($cacheKey, \json_encode($uniqueEvents));
return $uniqueEvents;
@@ -97,6 +111,6 @@ class EventProcessor
}
}
- return \array_flip(\array_unique($events));
+ return $this->getEventMap($events);
}
}
diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php
index 484cafb0ab..e422bcbf96 100644
--- a/src/Appwrite/GraphQL/Resolvers.php
+++ b/src/Appwrite/GraphQL/Resolvers.php
@@ -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);
diff --git a/src/Appwrite/GraphQL/Schema.php b/src/Appwrite/GraphQL/Schema.php
index 57115ff027..aa68fd28a1 100644
--- a/src/Appwrite/GraphQL/Schema.php
+++ b/src/Appwrite/GraphQL/Schema.php
@@ -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;
}
diff --git a/src/Appwrite/GraphQL/Types.php b/src/Appwrite/GraphQL/Types.php
index 279cac2068..3d5979dc18 100644
--- a/src/Appwrite/GraphQL/Types.php
+++ b/src/Appwrite/GraphQL/Types.php
@@ -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);
diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php
index 037f80bcf7..53474b855a 100644
--- a/src/Appwrite/GraphQL/Types/Mapper.php
+++ b/src/Appwrite/GraphQL/Types/Mapper.php
@@ -101,16 +101,16 @@ class Mapper
if (\is_array($modelName)) {
foreach ($modelName as $name) {
- $models[] = static::$models[$name];
+ $models[] = self::$models[$name];
}
} else {
- $models[] = static::$models[$modelName];
+ $models[] = self::$models[$modelName];
}
}
} else {
// If single response, get its model and wrap in array
$modelName = $responses->getModel();
- $models = [static::$models[$modelName]];
+ $models = [self::$models[$modelName]];
}
foreach ($models as $model) {
@@ -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:
@@ -425,7 +423,7 @@ class Mapper
'name' => $unionName,
'types' => $types,
'resolveType' => static function ($object) use ($unionName) {
- return static::getUnionImplementation($unionName, $object);
+ return self::getUnionImplementation($unionName, $object);
},
]);
@@ -440,11 +438,11 @@ class Mapper
switch ($name) {
case 'Attributes':
- return static::getColumnImplementation($object);
+ return self::getColumnImplementation($object);
case 'Columns':
- return static::getColumnImplementation($object, true);
+ return self::getColumnImplementation($object, true);
case 'HashOptions':
- return static::getHashOptionsImplementation($object);
+ return self::getHashOptionsImplementation($object);
}
throw new Exception('Unknown union type: ' . $name);
diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php
index a4f73eb5f2..e481eebf6e 100644
--- a/src/Appwrite/Migration/Migration.php
+++ b/src/Appwrite/Migration/Migration.php
@@ -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 $queries
+ * @return \Generator
+ * @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.
*
diff --git a/src/Appwrite/Migration/Version/V15.php b/src/Appwrite/Migration/Version/V15.php
index 66037660c0..eefa84ec22 100644
--- a/src/Appwrite/Migration/Version/V15.php
+++ b/src/Appwrite/Migration/Version/V15.php
@@ -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',
- ]);
+ ]) ?: '';
}
}
diff --git a/src/Appwrite/Migration/Version/V20.php b/src/Appwrite/Migration/Version/V20.php
index 3c13815949..e3458c815e 100644
--- a/src/Appwrite/Migration/Version/V20.php
+++ b/src/Appwrite/Migration/Version/V20.php
@@ -452,7 +452,7 @@ class V20 extends Migration
Query::equal('period', ['1d']),
]);
- $value = $query ?? 0;
+ $value = $query;
$this->createInfMetric($to, $value);
}
diff --git a/src/Appwrite/Migration/Version/V24.php b/src/Appwrite/Migration/Version/V24.php
index dc9ce9d196..a2d9d7907b 100644
--- a/src/Appwrite/Migration/Version/V24.php
+++ b/src/Appwrite/Migration/Version/V24.php
@@ -187,6 +187,20 @@ class V24 extends Migration
$this->dbForProject->purgeCachedCollection($id);
break;
+ case 'users':
+ try {
+ $this->createAttributeFromCollection($this->dbForProject, $id, 'impersonator');
+ } catch (Throwable $th) {
+ Console::warning("Failed to create attribute \"impersonator\" in collection {$id}: {$th->getMessage()}");
+ }
+ try {
+ $this->createIndexFromCollection($this->dbForProject, $id, 'impersonator');
+ } catch (Throwable $th) {
+ Console::warning("Failed to create index \"impersonator\" from {$id}: {$th->getMessage()}");
+ }
+ $this->dbForProject->purgeCachedCollection($id);
+ break;
+
case 'teams':
try {
$this->createAttributeFromCollection($this->dbForProject, $id, 'labels');
diff --git a/src/Appwrite/Network/Cors.php b/src/Appwrite/Network/Cors.php
index 9fc47c5808..593c82ab24 100644
--- a/src/Appwrite/Network/Cors.php
+++ b/src/Appwrite/Network/Cors.php
@@ -48,7 +48,7 @@ final class Cors
/**
* Build CORS headers for a given request origin.
*
- * @return array
+ * @return array
*/
public function headers(string $origin): array
{
diff --git a/src/Appwrite/OpenSSL/OpenSSL.php b/src/Appwrite/OpenSSL/OpenSSL.php
index 1965a3c858..787feb0904 100644
--- a/src/Appwrite/OpenSSL/OpenSSL.php
+++ b/src/Appwrite/OpenSSL/OpenSSL.php
@@ -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);
}
diff --git a/src/Appwrite/Platform/Action.php b/src/Appwrite/Platform/Action.php
index 01ac92a45c..0aa0da7149 100644
--- a/src/Appwrite/Platform/Action.php
+++ b/src/Appwrite/Platform/Action.php
@@ -37,7 +37,7 @@ class Action extends UtopiaAction
* Foreach Document
* Call provided callback for each document in the collection
*
- * @param string $projectId
+ * @param Database $database
* @param string $collection
* @param array $queries
* @param callable $callback
diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Action.php b/src/Appwrite/Platform/Modules/Avatars/Http/Action.php
index bf7d01764f..d6b58b33dd 100644
--- a/src/Appwrite/Platform/Modules/Avatars/Http/Action.php
+++ b/src/Appwrite/Platform/Modules/Avatars/Http/Action.php
@@ -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
diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php
index 0a4d652d0e..b6cc408dde 100644
--- a/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php
+++ b/src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php
@@ -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
diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php
index eb56ddf0b2..77ff9dd8ce 100644
--- a/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php
+++ b/src/Appwrite/Platform/Modules/Avatars/Http/Image/Get.php
@@ -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
diff --git a/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php b/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php
index 8278a43ea3..c73d20fb0c 100644
--- a/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php
+++ b/src/Appwrite/Platform/Modules/Avatars/Http/Initials/Get.php
@@ -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;
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php
index b2417871ed..60449aeab6 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Action.php
@@ -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;
}
/**
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
index 0bd4a2e080..91dd9c603c 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
@@ -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);
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
index 7f2e895228..38c84c4ae1 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
@@ -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,
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
index 27ccaafc71..b86d934ffb 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
@@ -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) {
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
index 744a4fd922..b3046fe22d 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
@@ -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,
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php
index 5d9d425d71..1142f38aa9 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Update.php
@@ -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,
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php
index 0c0f5f1273..4763a1611d 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php
@@ -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']);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php
index f3edf010d4..91bc1a3ccf 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Action.php
@@ -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;
}
/**
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php
index 30c9b7cb30..f06feccdee 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php
@@ -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',
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
index a5d96d5768..0c8c6a8520 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
@@ -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',
])));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
index 6754179425..23541db146 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Logs/XList.php
@@ -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']);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php
index b85a8b30b4..58433c7deb 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php
@@ -130,9 +130,26 @@ 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();
+ // Bootstrap the database metadata without a separate existence
+ // check to avoid races when multiple first collections are created
+ // concurrently for the same VectorsDB database.
+ for ($attempt = 0; $attempt < 5; $attempt++) {
+ try {
+ $dbForDatabases->create();
+ break;
+ } catch (DuplicateException) {
+ break;
+ } catch (\Throwable $e) {
+ if ($dbForDatabases->exists(null, Database::METADATA)) {
+ break;
+ }
+
+ if ($attempt === 4) {
+ throw $e;
+ }
+
+ \usleep(100_000);
+ }
}
$dbForDatabases->createCollection(
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php
index e9c665ea4b..50c901e4c8 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php
@@ -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);
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
index c6d25a15fc..37292ce984 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
@@ -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();
- }
- }
}
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
index d281c64414..8d4ad5d403 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
@@ -424,8 +424,8 @@ class Create extends Base
/** Trigger Realtime Events */
$queueForRealtime
- ->from($ruleCreate)
->setSubscribers(['console', $project->getId()])
+ ->from($ruleCreate)
->trigger();
}
}
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
index a627bae9dd..71fc99a30e 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
@@ -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', '');
diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
index bcedeee764..c6c4a0b38c 100644
--- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
+++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
@@ -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')
};
diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
index eb71e5a02f..9070962e7d 100644
--- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
+++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
@@ -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,
diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php
index fb83cb34c5..8a0d341132 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php
@@ -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 {
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php
index 600135e152..339d059365 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php
@@ -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);
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
index 6510e505ae..dd9bedffb5 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
@@ -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', '');
diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php
index c67601dae9..c5f4f3dccd 100644
--- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php
+++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php
@@ -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) {
diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php
index e4827a6354..406f8e4f49 100644
--- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php
+++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Update.php
@@ -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);
diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
index 777184f2f2..5edc69f445 100644
--- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
+++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
@@ -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 {
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
index 4a34ffd36a..8b320535e9 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
@@ -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);
}
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php
index 914bcaa93e..69da270e19 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php
@@ -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);
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php
index 04e2dbd406..6e1db12c28 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php
@@ -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');
@@ -70,6 +72,8 @@ trait Deployment
throw new Exception(Exception::PROJECT_NOT_FOUND, 'Repository references non-existent project');
}
+ $this->beforeCreateGitDeployment($project, $repository, $dbForPlatform, $authorization);
+
try {
$dsn = new DSN($project->getAttribute('database'));
$databaseName = $dsn->getHost();
@@ -105,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)) {
@@ -289,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);
}
@@ -499,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);
@@ -522,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);
}
@@ -561,6 +552,10 @@ trait Deployment
}
}
+ protected function beforeCreateGitDeployment(Document $project, Document $repository, Database $dbForPlatform, Authorization $authorization): void
+ {
+ }
+
protected function getBuildQueueName(Document $project, Database $dbForPlatform, Authorization $authorization): string
{
return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME);
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
index a2fa44c613..e3dbcfa0e9 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
@@ -83,7 +83,7 @@ class Create extends Action
default => null,
};
- return $response->json($parsedPayload);
+ $response->json($parsedPayload);
}
protected function preprocessEvent(Request $request)
diff --git a/src/Appwrite/Platform/Tasks/Doctor.php b/src/Appwrite/Platform/Tasks/Doctor.php
index 9a9c2fdf73..3eeaa95e64 100644
--- a/src/Appwrite/Platform/Tasks/Doctor.php
+++ b/src/Appwrite/Platform/Tasks/Doctor.php
@@ -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');
diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php
index 1be10f537f..dd7bed0137 100644
--- a/src/Appwrite/Platform/Tasks/Install.php
+++ b/src/Appwrite/Platform/Tasks/Install.php
@@ -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;
@@ -1343,7 +1343,7 @@ class Install extends Action
{
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $arg) {
- if (\str_starts_with($arg, '--') && !\str_starts_with($arg, '--interactive')) {
+ if (\str_starts_with($arg, '--')) {
return true;
}
}
diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php
index a36959af33..e8a69afddb 100644
--- a/src/Appwrite/Platform/Tasks/SDKs.php
+++ b/src/Appwrite/Platform/Tasks/SDKs.php
@@ -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;
diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php
index 88725a190a..69f105652c 100644
--- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php
+++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php
@@ -61,7 +61,7 @@ class ScheduleFunctions extends ScheduleBase
$nextDate = $cron->getNextRunDate();
$next = DateTime::format($nextDate);
- $currentTick = $next < $timeFrame;
+ $currentTick = $next <= $timeFrame;
if (!$currentTick) {
continue;
@@ -88,7 +88,7 @@ class ScheduleFunctions extends ScheduleBase
$scheduleKey = $delayConfig['key'];
// Ensure schedule was not deleted
if (!\array_key_exists($scheduleKey, $this->schedules)) {
- return;
+ continue;
}
$schedule = $this->schedules[$scheduleKey];
diff --git a/src/Appwrite/Platform/Tasks/StatsResources.php b/src/Appwrite/Platform/Tasks/StatsResources.php
index 6c0e8da48b..220e377619 100644
--- a/src/Appwrite/Platform/Tasks/StatsResources.php
+++ b/src/Appwrite/Platform/Tasks/StatsResources.php
@@ -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'));
/**
diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php
index 6ef9c7134d..f49674896e 100644
--- a/src/Appwrite/Platform/Tasks/Upgrade.php
+++ b/src/Appwrite/Platform/Tasks/Upgrade.php
@@ -30,7 +30,7 @@ class Upgrade extends Install
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
->param('no-start', false, new Boolean(true), 'Run an interactive session', true)
->param('database', 'mongodb', new Text(length: 0), 'Database to use (mongodb|mariadb|postgresql)', true)
- ->param('migrate', true, new Boolean(true), 'Run database migration after upgrade', true)
+ ->param('migrate', false, new Boolean(true), 'Run database migration after upgrade', true)
->callback($this->action(...));
}
@@ -42,7 +42,7 @@ class Upgrade extends Install
string $interactive,
bool $noStart,
string $database,
- bool $migrate = true,
+ bool $migrate = false,
): void {
$this->isUpgrade = true;
$this->migrate = $migrate;
diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php
index 55ec39026b..6bcc85bc36 100644
--- a/src/Appwrite/Platform/Workers/Audits.php
+++ b/src/Appwrite/Platform/Workers/Audits.php
@@ -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
diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php
index 716969e67a..c420444112 100644
--- a/src/Appwrite/Platform/Workers/Deletes.php
+++ b/src/Appwrite/Platform/Workers/Deletes.php
@@ -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
});
}
diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php
index 3f19abdf22..bed28dad1c 100644
--- a/src/Appwrite/Platform/Workers/Functions.php
+++ b/src/Appwrite/Platform/Workers/Functions.php
@@ -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'];
diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php
index f144c58e1b..32de1e50d6 100644
--- a/src/Appwrite/Platform/Workers/Mails.php
+++ b/src/Appwrite/Platform/Workers/Mails.php
@@ -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 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('/