mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d0950b8bc | ||
|
|
647b4bc632 | ||
|
|
d38c921781 | ||
|
|
32728ae4a0 | ||
|
|
168166b9c1 |
+17
-40
@@ -150,16 +150,8 @@ 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 -- --no-progress
|
||||
run: composer analyze
|
||||
|
||||
locale:
|
||||
name: Checks / Locale
|
||||
@@ -188,38 +180,11 @@ jobs:
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
|
||||
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
|
||||
const allDatabases = ['MongoDB'];
|
||||
const allModes = ['shared_v1', 'shared_v2'];
|
||||
|
||||
const defaultDatabases = ['MongoDB'];
|
||||
const defaultModes = ['dedicated'];
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
if (!pr) {
|
||||
core.setOutput('databases', JSON.stringify(allDatabases));
|
||||
core.setOutput('modes', JSON.stringify(allModes));
|
||||
return;
|
||||
}
|
||||
|
||||
const getContent = (ref) => github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: 'composer.lock',
|
||||
ref,
|
||||
});
|
||||
|
||||
const getDbVersion = (lock) => lock.packages?.find(p => p.name === 'utopia-php/database')?.version;
|
||||
|
||||
const [{ data: base }, { data: head }] = await Promise.all([
|
||||
getContent(pr.base.sha),
|
||||
getContent(pr.head.sha),
|
||||
]);
|
||||
|
||||
const decode = (content) => JSON.parse(Buffer.from(content, 'base64').toString());
|
||||
const databaseChanged = getDbVersion(decode(base.content)) !== getDbVersion(decode(head.content));
|
||||
|
||||
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
|
||||
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
|
||||
core.setOutput('databases', JSON.stringify(allDatabases));
|
||||
core.setOutput('modes', JSON.stringify(allModes));
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -459,6 +424,10 @@ jobs:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -533,6 +502,10 @@ jobs:
|
||||
_APP_OPTIONS_ABUSE: enabled
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -590,6 +563,10 @@ jobs:
|
||||
env:
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
|
||||
@@ -21,7 +21,6 @@ appwrite.config.json
|
||||
/app/config/specs/
|
||||
/docs/examples/
|
||||
.phpunit.cache
|
||||
.phpstan-cache
|
||||
playwright-report
|
||||
test-results
|
||||
docker-compose.web-installer.yml
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/55a81268-4ecc-46cd-bdf5-73f7e8662fee" />
|
||||
> 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)
|
||||
|
||||
<br />
|
||||
<p align="center">
|
||||
<h1>Appwrite</h1>
|
||||
<b>Appwrite is an open-source, all-in-one development platform. Use built-in backend infrastructure and web hosting, all from a single place.</b>
|
||||
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "The Developer's Cloud"></a>
|
||||
<br />
|
||||
<br />
|
||||
<b>Appwrite is a best-in-class, developer-first platform that gives builders everything they need to create scalable, stable, and production-ready software, fast.</b>
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
|
||||
[](https://appwrite.io/discord)
|
||||
[](https://x.com/appwrite)
|
||||
[](https://cloud.appwrite.io)
|
||||
<!-- [](https://travis-ci.com/appwrite/appwrite) -->
|
||||
|
||||
[](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://hub.docker.com/r/appwrite/appwrite) -->
|
||||
<!-- [](docs/tutorials/add-translations.md) -->
|
||||
<!-- [](https://store.appwrite.io) -->
|
||||
|
||||
English | [简体中文](README-CN.md)
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
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)
|
||||
@@ -32,31 +47,17 @@ 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.
|
||||
@@ -71,7 +72,6 @@ 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,7 +84,6 @@ 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" ^
|
||||
@@ -95,7 +94,6 @@ 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" `
|
||||
@@ -167,29 +165,51 @@ 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)
|
||||
- :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)
|
||||
- :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)
|
||||
|
||||
#### Server
|
||||
|
||||
- :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)
|
||||
- :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))
|
||||
|
||||
Looking for more SDKs? - Help us by contributing a pull request to our [SDK Generator](https://github.com/appwrite/sdk-generator)!
|
||||
|
||||
|
||||
+1
-5
@@ -329,20 +329,17 @@ $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, &$exitCode) {
|
||||
->action(function (Throwable $error, callable $logError) use ($taskName) {
|
||||
call_user_func_array($logError, [
|
||||
$error,
|
||||
'Task',
|
||||
$taskName,
|
||||
]);
|
||||
|
||||
$exitCode = 1;
|
||||
Timer::clearAll();
|
||||
});
|
||||
|
||||
@@ -351,4 +348,3 @@ $cli->shutdown()->action(fn () => Timer::clearAll());
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
run($cli->run(...));
|
||||
Console::exit($exitCode);
|
||||
|
||||
@@ -39,10 +39,6 @@ $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)) : [],
|
||||
|
||||
@@ -226,21 +226,6 @@ 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.',
|
||||
|
||||
@@ -31,6 +31,9 @@ class FunctionUseCases
|
||||
public const DEV_TOOLS = 'dev-tools';
|
||||
public const AUTH = 'auth';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -25,6 +25,9 @@ class SiteUseCases
|
||||
public const FORMS = 'forms';
|
||||
public const DASHBOARD = 'dashboard';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return [
|
||||
@@ -249,7 +252,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('VITE', [
|
||||
'providerRootDirectory' => './vite/vitepress',
|
||||
'fallbackFile' => '404.html',
|
||||
'outputDirectory' => '404.html',
|
||||
'installCommand' => 'npm i vitepress && npm install',
|
||||
'buildCommand' => 'npm run docs:build',
|
||||
'outputDirectory' => './.vitepress/dist',
|
||||
@@ -272,7 +275,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('VUE', [
|
||||
'providerRootDirectory' => './vue/vuepress',
|
||||
'fallbackFile' => '404.html',
|
||||
'outputDirectory' => '404.html',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './src/.vuepress/dist',
|
||||
@@ -295,7 +298,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/docusaurus',
|
||||
'fallbackFile' => '404.html',
|
||||
'outputDirectory' => '404.html',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './build',
|
||||
|
||||
+66
-243
@@ -403,8 +403,7 @@ Http::post('/v1/account')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('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) {
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Authorization $authorization, Hooks $hooks) {
|
||||
|
||||
$email = \strtolower($email);
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -453,38 +452,11 @@ 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 {
|
||||
$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);
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -515,11 +487,11 @@ Http::post('/v1/account')
|
||||
'authenticators' => null,
|
||||
'search' => implode(' ', [$userId, $email, $name]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -723,7 +695,6 @@ 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());
|
||||
@@ -745,7 +716,6 @@ 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));
|
||||
|
||||
@@ -758,11 +728,9 @@ Http::delete('/v1/account/sessions')
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
if ($currentSession instanceof Document) {
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $currentSession->getId());
|
||||
}
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $session->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
@@ -808,8 +776,7 @@ Http::get('/v1/account/sessions/:sessionId')
|
||||
->setAttribute('secret', $session->getAttribute('secret', ''))
|
||||
;
|
||||
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
return;
|
||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -989,7 +956,7 @@ Http::patch('/v1/account/sessions/:sessionId')
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION))
|
||||
;
|
||||
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
Http::post('/v1/account/sessions/email')
|
||||
@@ -1537,9 +1504,8 @@ 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, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
@@ -1781,38 +1747,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
}
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$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);
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1842,11 +1780,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
'authenticators' => null,
|
||||
'search' => implode(' ', [$userId, $email, $name]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -1921,47 +1859,19 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
|
||||
if (empty($user->getAttribute('email'))) {
|
||||
$email = $oauth2->getUserEmail($accessToken);
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
$user->setAttribute('email', $oauth2->getUserEmail($accessToken));
|
||||
|
||||
try {
|
||||
$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);
|
||||
$emailCanonical = new Email($user->getAttribute('email'));
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
|
||||
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']);
|
||||
$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 (empty($user->getAttribute('name'))) {
|
||||
@@ -2078,7 +1988,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 && isset($session)) {
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade) {
|
||||
foreach ($user->getAttribute('targets', []) as $target) {
|
||||
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
|
||||
continue;
|
||||
@@ -2239,11 +2149,10 @@ 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, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2278,38 +2187,10 @@ 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 {
|
||||
$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);
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
|
||||
$user->setAttributes([
|
||||
@@ -2336,11 +2217,11 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
'authenticators' => null,
|
||||
'search' => implode(' ', [$userId, $email]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -2548,11 +2429,10 @@ 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, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2585,38 +2465,10 @@ Http::post('/v1/account/tokens/email')
|
||||
|
||||
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$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);
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
|
||||
$user->setAttributes([
|
||||
@@ -2641,11 +2493,11 @@ Http::post('/v1/account/tokens/email')
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
]);
|
||||
|
||||
$user->removeAttribute('$sequence');
|
||||
@@ -3462,10 +3314,9 @@ 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, array $plan, 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, ProofsPassword $proofForPassword, Authorization $authorization) {
|
||||
// passwordUpdate will be empty if the user has never set a password
|
||||
$passwordUpdate = $user->getAttribute('passwordUpdate');
|
||||
|
||||
@@ -3493,48 +3344,20 @@ 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 {
|
||||
$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);
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('email', $email)
|
||||
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
|
||||
->setAttribute('emailCanonical', $emailMetadata['emailCanonical'])
|
||||
->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical'])
|
||||
->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate'])
|
||||
->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable'])
|
||||
->setAttribute('emailIsFree', $emailMetadata['emailIsFree'])
|
||||
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
|
||||
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
|
||||
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
|
||||
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
|
||||
->setAttribute('emailIsFree', $emailCanonical?->isFree())
|
||||
;
|
||||
|
||||
if (empty($passwordUpdate)) {
|
||||
@@ -3937,7 +3760,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)
|
||||
@@ -4038,7 +3861,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);
|
||||
});
|
||||
@@ -4268,7 +4091,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)
|
||||
@@ -4360,7 +4183,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);
|
||||
});
|
||||
@@ -4892,5 +4715,5 @@ Http::delete('/v1/account/identities/:identityId')
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
$response->noContent();
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
@@ -231,7 +231,6 @@ 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([
|
||||
|
||||
@@ -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 = $scheduledAt;
|
||||
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
|
||||
if (!\is_null($scheduleTime)) {
|
||||
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
|
||||
} else {
|
||||
|
||||
@@ -1570,7 +1570,7 @@ Http::post('/v1/projects/:projectId/smtp/tests')
|
||||
->trigger();
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
|
||||
+41
-104
@@ -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, array $plan): Document
|
||||
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
|
||||
{
|
||||
$name = $name ?? '';
|
||||
$plaintextPassword = $password;
|
||||
@@ -110,39 +110,11 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
|
||||
}
|
||||
}
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$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) {
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -187,11 +159,11 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
|
||||
'tokens' => null,
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email, $phone, $name]),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
]);
|
||||
|
||||
if (!$isHashed && !empty($password)) {
|
||||
@@ -284,11 +256,10 @@ Http::post('/v1/users')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('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) {
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$plaintext = new Plaintext();
|
||||
|
||||
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($user, Response::MODEL_USER);
|
||||
@@ -321,12 +292,11 @@ Http::post('/v1/users/bcrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$bcrypt = new Bcrypt();
|
||||
$bcrypt->setCost(8); // Default cost
|
||||
|
||||
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -360,11 +330,10 @@ Http::post('/v1/users/md5')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$md5 = new MD5();
|
||||
|
||||
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -398,11 +367,10 @@ Http::post('/v1/users/argon2')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$argon2 = new Argon2();
|
||||
|
||||
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -437,14 +405,13 @@ Http::post('/v1/users/sha')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('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) {
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$sha = new Sha();
|
||||
if (!empty($passwordVersion)) {
|
||||
$sha->setVersion($passwordVersion);
|
||||
}
|
||||
|
||||
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -478,11 +445,10 @@ Http::post('/v1/users/phpass')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->inject('plan')
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$phpass = new PHPass();
|
||||
|
||||
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -521,8 +487,7 @@ Http::post('/v1/users/scrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('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) {
|
||||
->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) {
|
||||
$scrypt = new Scrypt();
|
||||
$scrypt
|
||||
->setSalt($passwordSalt)
|
||||
@@ -531,7 +496,7 @@ Http::post('/v1/users/scrypt')
|
||||
->setParallelCost($passwordParallel)
|
||||
->setLength($passwordLength);
|
||||
|
||||
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -568,15 +533,14 @@ Http::post('/v1/users/scrypt-modified')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('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) {
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$scryptModified = new ScryptModified();
|
||||
$scryptModified
|
||||
->setSalt($passwordSalt)
|
||||
->setSaltSeparator($passwordSaltSeparator)
|
||||
->setSignerKey($passwordSignerKey);
|
||||
|
||||
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
|
||||
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -1506,10 +1470,8 @@ 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, Document $project, array $plan, Event $queueForEvents) {
|
||||
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
@@ -1540,47 +1502,20 @@ Http::patch('/v1/users/:userId/email')
|
||||
|
||||
$oldEmail = $user->getAttribute('email');
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$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);
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('email', $email)
|
||||
->setAttribute('emailVerification', false)
|
||||
->setAttribute('emailCanonical', $emailMetadata['emailCanonical'])
|
||||
->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical'])
|
||||
->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate'])
|
||||
->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable'])
|
||||
->setAttribute('emailIsFree', $emailMetadata['emailIsFree'])
|
||||
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
|
||||
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
|
||||
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
|
||||
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
|
||||
->setAttribute('emailIsFree', $emailCanonical?->isFree())
|
||||
;
|
||||
|
||||
try {
|
||||
@@ -2402,8 +2337,9 @@ Http::post('/v1/users/:userId/sessions')
|
||||
->setParam('sessionId', $session->getId())
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION));
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
Http::post('/v1/users/:userId/tokens')
|
||||
@@ -2466,8 +2402,9 @@ Http::post('/v1/users/:userId/tokens')
|
||||
->setParam('tokenId', $token->getId())
|
||||
->setPayload($response->output($token, Response::MODEL_TOKEN));
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($token, Response::MODEL_TOKEN);
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($token, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
Http::delete('/v1/users/:userId/sessions/:sessionId')
|
||||
@@ -2721,7 +2658,7 @@ Http::delete('/v1/users/identities/:identityId')
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
$response->noContent();
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
Http::post('/v1/users/:userId/jwts')
|
||||
|
||||
+12
-32
@@ -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);
|
||||
}
|
||||
$response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
return false;
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
}
|
||||
}
|
||||
|
||||
/** @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,7 +244,6 @@ 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)) {
|
||||
@@ -274,7 +273,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
|
||||
$membershipExists = false;
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
if (!$project->isEmpty() && !$user->isEmpty()) {
|
||||
if (!$project->isEmpty() && isset($user)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
$membership = $user->find('teamId', $teamId, 'memberships');
|
||||
if (!empty($membership)) {
|
||||
@@ -380,7 +379,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'] = '';
|
||||
@@ -460,7 +459,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'] ?? ''
|
||||
]);
|
||||
@@ -530,11 +529,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
|
||||
/** Execute function */
|
||||
$executionResponse = [
|
||||
'headers' => [],
|
||||
'body' => '',
|
||||
];
|
||||
|
||||
try {
|
||||
$version = match ($type) {
|
||||
'function' => $resource->getAttribute('version', 'v2'),
|
||||
@@ -740,7 +734,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) {
|
||||
@@ -868,12 +862,12 @@ Http::init()
|
||||
* Request format
|
||||
*/
|
||||
$route = $utopia->getRoute();
|
||||
$request->setRoute($route);
|
||||
Request::setRoute($route);
|
||||
|
||||
if ($route === null) {
|
||||
$response->setStatusCode(404);
|
||||
$response->send('Not Found');
|
||||
return;
|
||||
return $response
|
||||
->setStatusCode(404)
|
||||
->send('Not Found');
|
||||
}
|
||||
|
||||
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
@@ -979,8 +973,7 @@ Http::init()
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
|
||||
}
|
||||
|
||||
$response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
return;
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1019,7 +1012,7 @@ Http::init()
|
||||
return;
|
||||
}
|
||||
$route = $request->getRoute();
|
||||
if ($route?->getLabel('origin', false) === '*') {
|
||||
if ($route->getLabel('origin', false) === '*') {
|
||||
return;
|
||||
}
|
||||
if (!$originValidator->isValid($origin)) {
|
||||
@@ -1493,19 +1486,6 @@ 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')
|
||||
|
||||
+28
-30
@@ -244,38 +244,36 @@ Http::get('/v1/mock/github/callback')
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
|
||||
}
|
||||
|
||||
if (empty($providerInstallationId)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing provider installation ID');
|
||||
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);
|
||||
}
|
||||
|
||||
$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(),
|
||||
]);
|
||||
|
||||
+3
-4
@@ -3,8 +3,6 @@
|
||||
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;
|
||||
@@ -33,6 +31,7 @@ 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;
|
||||
|
||||
@@ -294,7 +293,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
|
||||
|
||||
go(function () use ($register, $app) {
|
||||
$pools = $register->get('pools');
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
/** @var Group $pools */
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
|
||||
/** @var array $collections */
|
||||
@@ -656,7 +655,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
$dbForPlatform = $app->getResource('dbForPlatform');
|
||||
|
||||
/** @var \Swoole\Table $riskyDomains */
|
||||
/** @var Table $riskyDomains */
|
||||
$riskyDomains = $app->getResource('riskyDomains');
|
||||
|
||||
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
|
||||
|
||||
+36
-21
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -24,7 +25,6 @@ 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)) {
|
||||
if (empty($providerName) || empty($providerConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ $register->set('realtimeLogger', function () {
|
||||
default => ['key' => $loggingProvider->getHost()],
|
||||
};
|
||||
|
||||
if (empty($providerName)) {
|
||||
if (empty($providerName) || empty($providerConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,8 +242,8 @@ $register->set('pools', function () {
|
||||
],
|
||||
];
|
||||
|
||||
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
$maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / 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) {
|
||||
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase, $dsn) {
|
||||
try {
|
||||
$mongo = new MongoClient($dsnDatabase, $dsnHost, (int)$dsnPort, $dsnUser, $dsnPass, false);
|
||||
@$mongo->connect();
|
||||
@@ -433,20 +433,35 @@ $register->set('db', function () {
|
||||
});
|
||||
|
||||
$register->set('smtp', function () {
|
||||
$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,
|
||||
);
|
||||
$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;
|
||||
});
|
||||
$register->set('geodb', function () {
|
||||
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
|
||||
|
||||
+28
-6
@@ -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()
|
||||
@@ -888,8 +888,9 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
|
||||
$initializedPools = [];
|
||||
|
||||
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
|
||||
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization, &$initializedPools): Database {
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
@@ -907,11 +908,12 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
@@ -922,10 +924,30 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
// inside pools authorization needs to be set first
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
|
||||
// When the database uses a separate pool (e.g. vectorsdb on PostgreSQL),
|
||||
// always use dedicated mode with namespace isolation. Shared tables mode
|
||||
// can't be used across different engines (e.g. MongoDB UUID tenants are
|
||||
// incompatible with PostgreSQL's integer _tenant column).
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
|
||||
$poolKey = $databaseHost . ':' . $database->getNamespace();
|
||||
if (!isset($initializedPools[$poolKey])) {
|
||||
try {
|
||||
$database->create();
|
||||
} catch (\Utopia\Database\Exception\Duplicate) {
|
||||
// Schema already exists
|
||||
}
|
||||
$initializedPools[$poolKey] = true;
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
|
||||
+7
-25
@@ -237,6 +237,7 @@ if (!function_exists('getTelemetry')) {
|
||||
if (!function_exists('triggerStats')) {
|
||||
function triggerStats(array $event, string $projectId): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,14 +320,14 @@ if (!function_exists('logError')) {
|
||||
|
||||
$server->error(logError(...));
|
||||
|
||||
$server->onStart(function () use ($stats, $containerId, &$statsDocument) {
|
||||
$server->onStart(function () use ($stats, $register, $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 ($containerId, &$statsDocument) {
|
||||
go(function () use ($register, $containerId, &$statsDocument) {
|
||||
$attempts = 0;
|
||||
$database = getConsoleDB();
|
||||
|
||||
@@ -356,7 +357,7 @@ $server->onStart(function () use ($stats, $containerId, &$statsDocument) {
|
||||
*/
|
||||
// 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 ($stats, &$statsDocument) {
|
||||
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
|
||||
$payload = [];
|
||||
foreach ($stats as $projectId => $value) {
|
||||
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
|
||||
@@ -383,22 +384,6 @@ $server->onStart(function () use ($stats, $containerId, &$statsDocument) {
|
||||
}
|
||||
});
|
||||
|
||||
function cloudRealtimeLogConnectionHostnames(Http $app, Document $project, Request $request): void
|
||||
{
|
||||
try {
|
||||
/** @var array<int, string> $allowed */
|
||||
$allowed = $app->getResource('allowedHostnames');
|
||||
Console::info(sprintf(
|
||||
'[Realtime] project=%s origin=%s allowedHostnames=%s',
|
||||
$project->getId(),
|
||||
$request->getOrigin(),
|
||||
json_encode(array_values($allowed))
|
||||
));
|
||||
} catch (Throwable $e) {
|
||||
Console::error('[Realtime] allowedHostnames log failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
|
||||
Console::success('Worker ' . $workerId . ' started successfully');
|
||||
|
||||
@@ -411,7 +396,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
|
||||
Timer::tick(5000, function () use ($server, $realtime, $stats) {
|
||||
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
|
||||
/**
|
||||
* Sending current connections to project channels on the console project every 5 seconds.
|
||||
*/
|
||||
@@ -704,9 +689,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
*/
|
||||
$origin = $request->getOrigin();
|
||||
$originValidator = $app->getResource('originValidator');
|
||||
|
||||
|
||||
cloudRealtimeLogConnectionHostnames($app, $project, $request);
|
||||
|
||||
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
|
||||
@@ -816,7 +798,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
}
|
||||
});
|
||||
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
|
||||
$project = null;
|
||||
$authorization = null;
|
||||
|
||||
@@ -828,7 +810,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();
|
||||
|
||||
+30
-8
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
use Appwrite\Certificates\LetsEncrypt;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
@@ -220,7 +221,9 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
|
||||
$initializedPools = [];
|
||||
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization, &$initializedPools): Database {
|
||||
$projectDocument ??= $project;
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
@@ -245,7 +248,8 @@ Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
@@ -254,12 +258,30 @@ Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
// When using a separate pool, always use dedicated mode with namespace isolation.
|
||||
// Shared tables mode can't be used across different engines (e.g. MongoDB UUID
|
||||
// tenants are incompatible with PostgreSQL's integer _tenant column).
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
|
||||
$poolKey = $databaseHost . ':' . $database->getNamespace();
|
||||
if (!isset($initializedPools[$poolKey])) {
|
||||
try {
|
||||
$database->create();
|
||||
} catch (\Utopia\Database\Exception\Duplicate) {
|
||||
// Schema already exists
|
||||
}
|
||||
$initializedPools[$poolKey] = true;
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int) $projectDocument->getSequence())
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -279,14 +301,14 @@ Server::setResource('abuseRetention', function () {
|
||||
|
||||
Server::setResource('auditRetention', function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
}
|
||||
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
}, ['project']);
|
||||
|
||||
Server::setResource('executionRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
});
|
||||
|
||||
Server::setResource('cache', function (Registry $register) {
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@
|
||||
"utopia-php/image": "0.8.*",
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
|
||||
Generated
+14
-14
@@ -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": "4fe91e67f343fbe6deac1fdc7eda949f",
|
||||
"content-hash": "b5261855586680e467168f527e0634ae",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.17",
|
||||
"version": "5.3.19",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993"
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3903,9 +3903,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.17"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.19"
|
||||
},
|
||||
"time": "2026-03-20T01:18:52+00:00"
|
||||
"time": "2026-03-31T15:52:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4467,23 +4467,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/messaging",
|
||||
"version": "0.22.0",
|
||||
"version": "0.20.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/messaging.git",
|
||||
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030"
|
||||
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
|
||||
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0",
|
||||
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-openssl": "*",
|
||||
"giggsey/libphonenumber-for-php-lite": "9.0.23",
|
||||
"php": ">=8.1.0",
|
||||
"php": ">=8.0.0",
|
||||
"phpmailer/phpmailer": "6.9.1"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -4512,9 +4512,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/messaging/issues",
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.22.0"
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.20.1"
|
||||
},
|
||||
"time": "2026-04-02T04:09:19+00:00"
|
||||
"time": "2026-02-06T09:56:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -1,6 +1,8 @@
|
||||
includes:
|
||||
- phpstan-baseline.neon
|
||||
|
||||
parameters:
|
||||
level: 3
|
||||
tmpDir: .phpstan-cache
|
||||
paths:
|
||||
- src
|
||||
- app
|
||||
@@ -12,3 +14,4 @@ parameters:
|
||||
- vendor/swoole/ide-helper
|
||||
excludePaths:
|
||||
- tests/resources
|
||||
|
||||
|
||||
@@ -23,9 +23,8 @@ class Yahoo extends OAuth2
|
||||
* @var array
|
||||
*/
|
||||
protected array $scopes = [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'sdct-r',
|
||||
'sdpp-w',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -285,7 +285,7 @@ class Event
|
||||
*
|
||||
* @param string $key
|
||||
* @param Document $context
|
||||
* @return static
|
||||
* @return self
|
||||
*/
|
||||
public function setContext(string $key, Document $context): self
|
||||
{
|
||||
@@ -309,7 +309,7 @@ class Event
|
||||
/**
|
||||
* Set class used for this event.
|
||||
* @param string $class
|
||||
* @return static
|
||||
* @return self
|
||||
*/
|
||||
public function setClass(string $class): self
|
||||
{
|
||||
@@ -648,8 +648,10 @@ class Event
|
||||
*
|
||||
* @param Event $event
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
*/
|
||||
public function from(Event $event): static
|
||||
public function from(Event $event): self
|
||||
{
|
||||
$this->project = $event->getProject();
|
||||
$this->user = $event->getUser();
|
||||
|
||||
@@ -40,7 +40,6 @@ class Usage extends Base
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
/** @phpstan-ignore new.static (subclass constructors are backwards-compatible via optional params) */
|
||||
return new static(
|
||||
project: new Document($data['project'] ?? []),
|
||||
metrics: $data['metrics'] ?? [],
|
||||
|
||||
@@ -82,9 +82,6 @@ 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';
|
||||
|
||||
@@ -25,7 +25,11 @@ class Resolvers
|
||||
?Route $route,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
|
||||
/** @var Http $utopia */
|
||||
/** @var Response $response */
|
||||
/** @var Request $request */
|
||||
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -92,7 +96,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, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -123,7 +127,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, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -159,7 +163,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, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -191,7 +195,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, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -221,7 +225,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, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
@@ -98,9 +98,10 @@ class Schema
|
||||
foreach ($routes as $route) {
|
||||
/** @var Route $route */
|
||||
|
||||
/** @var \Appwrite\SDK\Method $sdk */
|
||||
$sdk = $route->getLabel('sdk', false);
|
||||
|
||||
if ($sdk === false) {
|
||||
if (empty($sdk)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -176,7 +177,7 @@ class Schema
|
||||
$required = $attr['required'];
|
||||
$default = $attr['default'];
|
||||
$escapedKey = str_replace('$', '', $key);
|
||||
$collections[$databaseId][$collectionId][$escapedKey] = [
|
||||
$collections[$collectionId][$escapedKey] = [
|
||||
'type' => Mapper::attribute(
|
||||
$type,
|
||||
$array,
|
||||
@@ -186,82 +187,80 @@ class Schema
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($collections as $databaseId => $databaseCollections) {
|
||||
foreach ($databaseCollections as $collectionId => $attributes) {
|
||||
$objectType = new ObjectType([
|
||||
'name' => $collectionId,
|
||||
'fields' => \array_merge(
|
||||
["_id" => ['type' => Type::string()]],
|
||||
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']),
|
||||
$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']),
|
||||
$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'],
|
||||
)
|
||||
];
|
||||
}
|
||||
),
|
||||
'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;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,10 @@ class Types
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
public static function json(): Json
|
||||
public static function json(): Type
|
||||
{
|
||||
if (Registry::has(Json::class)) {
|
||||
$type = Registry::get(Json::class);
|
||||
if ($type instanceof Json) {
|
||||
return $type;
|
||||
}
|
||||
return Registry::get(Json::class);
|
||||
}
|
||||
$type = new Json();
|
||||
Registry::set(Json::class, $type);
|
||||
@@ -31,15 +28,12 @@ class Types
|
||||
/**
|
||||
* Get the JSON type.
|
||||
*
|
||||
* @return Assoc
|
||||
* @return Json
|
||||
*/
|
||||
public static function assoc(): Assoc
|
||||
public static function assoc(): Type
|
||||
{
|
||||
if (Registry::has(Assoc::class)) {
|
||||
$type = Registry::get(Assoc::class);
|
||||
if ($type instanceof Assoc) {
|
||||
return $type;
|
||||
}
|
||||
return Registry::get(Assoc::class);
|
||||
}
|
||||
$type = new Assoc();
|
||||
Registry::set(Assoc::class, $type);
|
||||
@@ -51,13 +45,10 @@ class Types
|
||||
*
|
||||
* @return InputFile
|
||||
*/
|
||||
public static function inputFile(): InputFile
|
||||
public static function inputFile(): Type
|
||||
{
|
||||
if (Registry::has(InputFile::class)) {
|
||||
$type = Registry::get(InputFile::class);
|
||||
if ($type instanceof InputFile) {
|
||||
return $type;
|
||||
}
|
||||
return Registry::get(InputFile::class);
|
||||
}
|
||||
$type = new InputFile();
|
||||
Registry::set(InputFile::class, $type);
|
||||
|
||||
@@ -273,9 +273,11 @@ 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:
|
||||
@@ -284,7 +286,7 @@ class Mapper
|
||||
case \Utopia\Validator\HexColor::class:
|
||||
case \Utopia\Validator\Host::class:
|
||||
case \Utopia\Validator\IP::class:
|
||||
case \Appwrite\Network\Validator\Origin::class:
|
||||
case \Utopia\Validator\Origin::class:
|
||||
case \Utopia\Validator\Text::class:
|
||||
case \Utopia\Validator\URL::class:
|
||||
case \Utopia\Validator\WhiteList::class:
|
||||
|
||||
@@ -13,7 +13,6 @@ 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
|
||||
@@ -205,30 +204,6 @@ abstract class Migration
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Query> $queries
|
||||
* @return \Generator<int, Document>
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function documentsIterator(string $collection, array $queries = []): \Generator
|
||||
{
|
||||
$offset = 0;
|
||||
|
||||
do {
|
||||
$documents = $this->dbForProject->find($collection, [
|
||||
...$queries,
|
||||
Query::limit($this->limit),
|
||||
Query::offset($offset),
|
||||
]);
|
||||
|
||||
foreach ($documents as $document) {
|
||||
yield $document;
|
||||
}
|
||||
|
||||
$offset += \count($documents);
|
||||
} while (\count($documents) === $this->limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates collection from the config collection.
|
||||
*
|
||||
|
||||
@@ -1224,7 +1224,7 @@ class V15 extends Migration
|
||||
* @param \Utopia\Database\Document $document
|
||||
* @return \Utopia\Database\Document
|
||||
*/
|
||||
protected function fixDocument(Document $document): Document
|
||||
protected function fixDocument(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 $document;
|
||||
return;
|
||||
|
||||
case '_metadata':
|
||||
/**
|
||||
@@ -1480,6 +1480,7 @@ class V15 extends Migration
|
||||
* Filter from the 'encrypt' filter.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string|false
|
||||
*/
|
||||
protected function encryptFilter(string $value): string
|
||||
{
|
||||
@@ -1491,8 +1492,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',
|
||||
]) ?: '';
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ class V20 extends Migration
|
||||
Query::equal('period', ['1d']),
|
||||
]);
|
||||
|
||||
$value = $query;
|
||||
$value = $query ?? 0;
|
||||
$this->createInfMetric($to, $value);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ final class Cors
|
||||
/**
|
||||
* Build CORS headers for a given request origin.
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public function headers(string $origin): array
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ class OpenSSL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function encrypt($data, $method, $key, $options = 0, $iv = '', ?string &$tag = null, $aad = '', $tag_length = 16)
|
||||
public static function encrypt($data, $method, $key, $options = 0, $iv = '', &$tag = null, $aad = '', $tag_length = 16)
|
||||
{
|
||||
return \openssl_encrypt($data, $method, $key, $options, $iv, $tag, $aad, $tag_length);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ class Action extends PlatformAction
|
||||
|
||||
$image = new Image(\file_get_contents($path));
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
|
||||
@@ -204,6 +204,7 @@ class Get extends Action
|
||||
|
||||
$image = new Image($data);
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$response
|
||||
|
||||
@@ -95,6 +95,7 @@ class Get extends Action
|
||||
}
|
||||
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$response
|
||||
|
||||
@@ -90,7 +90,7 @@ class Get extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$rand = (int) \substr((string) $code, -1);
|
||||
$rand = \substr($code, -1);
|
||||
|
||||
$rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Action extends AppwriteAction
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
public function setHttpPath(string $path): self
|
||||
public function setHttpPath(string $path): AppwriteAction
|
||||
{
|
||||
if (\str_contains($path, '/tablesdb')) {
|
||||
$this->context = DATABASE_TYPE_TABLESDB;
|
||||
@@ -28,8 +28,7 @@ class Action extends AppwriteAction
|
||||
if (\str_contains($path, '/vectorsdb')) {
|
||||
$this->context = DATABASE_TYPE_VECTORSDB;
|
||||
}
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
return parent::setHttpPath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-3
@@ -24,7 +24,7 @@ abstract class Action extends DatabasesAction
|
||||
*/
|
||||
abstract protected function getResponseModel(): string;
|
||||
|
||||
public function setHttpPath(string $path): self
|
||||
public function setHttpPath(string $path): DatabasesAction
|
||||
{
|
||||
if (str_contains($path, '/tablesdb/')) {
|
||||
$this->context = ROWS;
|
||||
@@ -47,8 +47,7 @@ abstract class Action extends DatabasesAction
|
||||
],
|
||||
];
|
||||
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
return parent::setHttpPath($path);
|
||||
}
|
||||
|
||||
protected function getDatabasesOperationReadMetric(): string
|
||||
@@ -407,6 +406,8 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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, $authorization) {
|
||||
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) {
|
||||
$allowedPermissions = [
|
||||
Database::PERMISSION_READ,
|
||||
Database::PERMISSION_UPDATE,
|
||||
|
||||
+1
@@ -122,6 +122,7 @@ 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) {
|
||||
|
||||
+1
-1
@@ -147,7 +147,7 @@ class XList extends Action
|
||||
$cacheKeyBase = \sprintf(
|
||||
'%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname,
|
||||
$hostname ?? '',
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$collectionId,
|
||||
|
||||
@@ -99,6 +99,8 @@ 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,
|
||||
|
||||
@@ -61,16 +61,16 @@ class Create extends Action
|
||||
$databaseKeys = System::getEnv('_APP_DATABASE_DOCUMENTSDB_KEYS', '');
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb');
|
||||
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
|
||||
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')));
|
||||
break;
|
||||
case VECTORSDB:
|
||||
$databases = Config::getParam('pools-vectorsdb', []);
|
||||
$databaseKeys = System::getEnv('_APP_DATABASE_VECTORSDB_KEYS', '');
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql');
|
||||
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
|
||||
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')));
|
||||
break;
|
||||
default:
|
||||
// legacy/tablesdb
|
||||
@@ -108,7 +108,7 @@ class Create extends Action
|
||||
if ($index !== false) {
|
||||
$selectedDsn = $databases[$index];
|
||||
} else {
|
||||
if (!empty($dsn)) {
|
||||
if (!empty($dsn) && !empty($databaseSharedTables)) {
|
||||
$beforeFilter = \array_values($databases);
|
||||
if ($isSharedTablesV1) {
|
||||
$databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1));
|
||||
@@ -118,7 +118,10 @@ class Create extends Action
|
||||
$databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables));
|
||||
}
|
||||
}
|
||||
$selectedDsn = !empty($databases) ? $databases[array_rand($databases)] : '';
|
||||
if (empty($databases)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "No {$databasetype} database pool available for the current shared-tables mode");
|
||||
}
|
||||
$selectedDsn = $databases[array_rand($databases)];
|
||||
}
|
||||
|
||||
if (\in_array($selectedDsn, $databaseSharedTables)) {
|
||||
|
||||
@@ -103,9 +103,6 @@ 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'],
|
||||
@@ -124,9 +121,9 @@ class XList extends Action
|
||||
'clientVersion' => $client['clientVersion'],
|
||||
'clientEngine' => $client['clientEngine'],
|
||||
'clientEngineVersion' => $client['clientEngineVersion'],
|
||||
'deviceName' => $deviceName,
|
||||
'deviceBrand' => $deviceBrand,
|
||||
'deviceModel' => $deviceModel,
|
||||
'deviceName' => $device['deviceName'],
|
||||
'deviceBrand' => $device['deviceBrand'],
|
||||
'deviceModel' => $device['deviceModel'],
|
||||
]);
|
||||
|
||||
$record = $geodb->get($log['ip']);
|
||||
|
||||
@@ -33,7 +33,7 @@ abstract class Action extends DatabasesAction
|
||||
return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_WRITES;
|
||||
}
|
||||
|
||||
public function setHttpPath(string $path): self
|
||||
public function setHttpPath(string $path): DatabasesAction
|
||||
{
|
||||
switch (true) {
|
||||
case str_contains($path, '/tablesdb'):
|
||||
@@ -50,8 +50,7 @@ abstract class Action extends DatabasesAction
|
||||
$this->databaseType = VECTORSDB;
|
||||
break;
|
||||
}
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
return parent::setHttpPath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -239,7 +239,7 @@ class Create extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $operations) {
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) {
|
||||
$dbForProject->createDocuments('transactionLogs', $staged);
|
||||
return $dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
|
||||
@@ -105,7 +105,8 @@ class Update extends Action
|
||||
* @throws Exception
|
||||
* @throws \Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws StructureException
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
* @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
|
||||
@@ -182,7 +183,7 @@ class Update extends Action
|
||||
$dbForDatabases = $getDatabasesDB($databaseDoc);
|
||||
|
||||
try {
|
||||
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) {
|
||||
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'committing',
|
||||
])));
|
||||
|
||||
@@ -97,9 +97,6 @@ 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'],
|
||||
@@ -118,9 +115,9 @@ class XList extends Action
|
||||
'clientVersion' => $client['clientVersion'],
|
||||
'clientEngine' => $client['clientEngine'],
|
||||
'clientEngineVersion' => $client['clientEngineVersion'],
|
||||
'deviceName' => $deviceName,
|
||||
'deviceBrand' => $deviceBrand,
|
||||
'deviceModel' => $deviceModel,
|
||||
'deviceName' => $device['deviceName'],
|
||||
'deviceBrand' => $device['deviceBrand'],
|
||||
'deviceModel' => $device['deviceModel'],
|
||||
]);
|
||||
|
||||
$record = $geodb->get($log['ip']);
|
||||
|
||||
@@ -130,26 +130,9 @@ class Create extends CollectionAction
|
||||
$indexes[] = new Document($index);
|
||||
}
|
||||
try {
|
||||
// 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);
|
||||
}
|
||||
// passing null in creates only creates the metadata collection
|
||||
if (!$dbForDatabases->exists(null, Database::METADATA)) {
|
||||
$dbForDatabases->create();
|
||||
}
|
||||
$dbForDatabases->createCollection(
|
||||
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
|
||||
|
||||
@@ -88,11 +88,16 @@ class Get extends Action
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
[$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.'),
|
||||
};
|
||||
switch ($type) {
|
||||
case 'output':
|
||||
$path = $deployment->getAttribute('buildPath', '');
|
||||
$device = $deviceForBuilds;
|
||||
break;
|
||||
case 'source':
|
||||
$path = $deployment->getAttribute('sourcePath', '');
|
||||
$device = $deviceForFunctions;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$device->exists($path)) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
|
||||
@@ -213,10 +213,7 @@ class Create extends Base
|
||||
$current = new Document();
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
if (!$session instanceof Document) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var Utopia\Database\Document $session */
|
||||
if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { // Find most recent active session for user ID and JWT headers
|
||||
$current = $session;
|
||||
}
|
||||
@@ -240,11 +237,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';
|
||||
@@ -353,18 +350,16 @@ class Create extends Base
|
||||
}
|
||||
}
|
||||
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
$this->enqueueDeletes(
|
||||
$project,
|
||||
$function->getSequence(),
|
||||
$executionsRetentionCount,
|
||||
$queueForDeletes
|
||||
->setProject($project)
|
||||
->setResource($function->getSequence())
|
||||
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
|
||||
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
|
||||
->trigger();
|
||||
}
|
||||
);
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
|
||||
$response->dynamic($execution, Response::MODEL_EXECUTION);
|
||||
return;
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($execution, Response::MODEL_EXECUTION);
|
||||
}
|
||||
|
||||
$durationStart = \microtime(true);
|
||||
@@ -375,7 +370,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'] ?? ''
|
||||
]);
|
||||
@@ -542,18 +537,32 @@ class Create extends Base
|
||||
}
|
||||
}
|
||||
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
$this->enqueueDeletes(
|
||||
$project,
|
||||
$function->getSequence(),
|
||||
$executionsRetentionCount,
|
||||
$queueForDeletes
|
||||
->setProject($project)
|
||||
->setResource($function->getSequence())
|
||||
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
|
||||
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
|
||||
->trigger();
|
||||
}
|
||||
);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($execution, Response::MODEL_EXECUTION);
|
||||
}
|
||||
|
||||
private function enqueueDeletes(
|
||||
Document $project,
|
||||
string $resourceId,
|
||||
int $executionsRetentionCount,
|
||||
DeleteEvent $queueForDeletes
|
||||
): void {
|
||||
/* cleanup */
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
$queueForDeletes
|
||||
->setProject($project)
|
||||
->setResource($resourceId)
|
||||
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
|
||||
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,8 +424,8 @@ class Create extends Base
|
||||
|
||||
/** Trigger Realtime Events */
|
||||
$queueForRealtime
|
||||
->setSubscribers(['console', $project->getId()])
|
||||
->from($ruleCreate)
|
||||
->setSubscribers(['console', $project->getId()])
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,8 @@ class Update extends Base
|
||||
$runtime = $function->getAttribute('runtime');
|
||||
}
|
||||
|
||||
$enabled ??= $function->getAttribute('enabled', true);
|
||||
|
||||
$repositoryId = $function->getAttribute('repositoryId', '');
|
||||
$repositoryInternalId = $function->getAttribute('repositoryInternalId', '');
|
||||
|
||||
|
||||
@@ -450,7 +450,7 @@ class Builds extends Action
|
||||
|
||||
$providerCommitHash = \trim($stdout);
|
||||
|
||||
$deployment->setAttribute('providerCommitHash', $providerCommitHash);
|
||||
$deployment->setAttribute('providerCommitHash', $providerCommitHash ?? '');
|
||||
$deployment->setAttribute('providerCommitAuthorUrl', APP_VCS_GITHUB_URL);
|
||||
$deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME);
|
||||
$deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function");
|
||||
@@ -862,7 +862,7 @@ class Builds extends Action
|
||||
if (\str_contains($logs, '{APPWRITE_DETECTION_SEPARATOR_START}')) {
|
||||
[$logsBefore, $detectionLogsStart] = \explode('{APPWRITE_DETECTION_SEPARATOR_START}', $logs, 2);
|
||||
[$detectionLogs, $logsAfter] = \explode('{APPWRITE_DETECTION_SEPARATOR_END}', $detectionLogsStart, 2);
|
||||
$logs = $logsBefore . $logsAfter;
|
||||
$logs = ($logsBefore ?? '') . ($logsAfter ?? '');
|
||||
}
|
||||
|
||||
$deployment->setAttribute('buildLogs', $logs);
|
||||
@@ -1203,8 +1203,6 @@ 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':
|
||||
@@ -1366,8 +1364,6 @@ class Builds extends Action
|
||||
Realtime $queueForRealtime,
|
||||
array $platform
|
||||
): void {
|
||||
$deployment = new Document();
|
||||
|
||||
try {
|
||||
if ($resource->getAttribute('providerSilentMode', false) === true) {
|
||||
return;
|
||||
@@ -1448,7 +1444,7 @@ class Builds extends Action
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = match ($resource->getCollection()) {
|
||||
'functions' => '',
|
||||
'sites' => !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
|
||||
'sites' => ! empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
|
||||
@@ -114,9 +114,6 @@ class Create extends Action
|
||||
'passwordDictionary' => false,
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'personalDataCheck' => false,
|
||||
'disposableEmails' => false,
|
||||
'canonicalEmails' => false,
|
||||
'freeEmails' => false,
|
||||
'mockNumbers' => [],
|
||||
'sessionAlerts' => false,
|
||||
'membershipsUserName' => false,
|
||||
|
||||
@@ -83,8 +83,7 @@ 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) {
|
||||
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
return;
|
||||
return $response->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -87,11 +87,16 @@ class Get extends Action
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
[$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.'),
|
||||
};
|
||||
switch ($type) {
|
||||
case 'output':
|
||||
$path = $deployment->getAttribute('buildPath', '');
|
||||
$device = $deviceForBuilds;
|
||||
break;
|
||||
case 'source':
|
||||
$path = $deployment->getAttribute('sourcePath', '');
|
||||
$device = $deviceForSites;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$device->exists($path)) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
|
||||
@@ -172,6 +172,8 @@ class Update extends Base
|
||||
$framework = $site->getAttribute('framework');
|
||||
}
|
||||
|
||||
$enabled ??= $site->getAttribute('enabled', true);
|
||||
|
||||
$repositoryId = $site->getAttribute('repositoryId', '');
|
||||
$repositoryInternalId = $site->getAttribute('repositoryInternalId', '');
|
||||
|
||||
|
||||
@@ -286,8 +286,6 @@ class Create extends Action
|
||||
$mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption
|
||||
$fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption
|
||||
$data = '';
|
||||
$iv = '';
|
||||
$tag = null;
|
||||
// Compression
|
||||
$algorithm = $bucket->getAttribute('compression', Compression::NONE);
|
||||
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) {
|
||||
|
||||
@@ -99,8 +99,12 @@ class Update extends Action
|
||||
|
||||
$permissions ??= $bucket->getPermissions();
|
||||
$maximumFileSize ??= $bucket->getAttribute('maximumFileSize', (int) System::getEnv('_APP_STORAGE_LIMIT', 0));
|
||||
$allowedFileExtensions ??= $bucket->getAttribute('allowedFileExtensions', []);
|
||||
$enabled ??= $bucket->getAttribute('enabled', true);
|
||||
$encryption ??= $bucket->getAttribute('encryption', true);
|
||||
$antivirus ??= $bucket->getAttribute('antivirus', true);
|
||||
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
|
||||
$transformations ??= $bucket->getAttribute('transformations', true);
|
||||
|
||||
// Map aggregate permissions into the multiple permissions they represent.
|
||||
$permissions = Permission::aggregate($permissions);
|
||||
|
||||
@@ -102,8 +102,6 @@ class Create extends Action
|
||||
{
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$invitee = new Document();
|
||||
$hash = '';
|
||||
|
||||
if (empty($url)) {
|
||||
if (! $isAppUser && ! $isPrivilegedUser) {
|
||||
@@ -147,6 +145,9 @@ 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
|
||||
@@ -168,41 +169,14 @@ class Create extends Action
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$emailMetadata = [
|
||||
'emailCanonical' => null,
|
||||
'emailIsCanonical' => null,
|
||||
'emailIsCorporate' => null,
|
||||
'emailIsDisposable' => null,
|
||||
'emailIsFree' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$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) {
|
||||
$userId = ID::unique();
|
||||
$hash = $proofForPassword->hash($proofForPassword->generate());
|
||||
$emailCanonical = new Email($email);
|
||||
} catch (Throwable) {
|
||||
$emailCanonical = null;
|
||||
}
|
||||
|
||||
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([
|
||||
@@ -235,11 +209,11 @@ class Create extends Action
|
||||
'tokens' => null,
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email, $name]),
|
||||
'emailCanonical' => $emailMetadata['emailCanonical'],
|
||||
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
|
||||
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
|
||||
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
|
||||
'emailIsFree' => $emailMetadata['emailIsFree'],
|
||||
'emailCanonical' => $emailCanonical?->getCanonical(),
|
||||
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
|
||||
'emailIsCorporate' => $emailCanonical?->isCorporate(),
|
||||
'emailIsDisposable' => $emailCanonical?->isDisposable(),
|
||||
'emailIsFree' => $emailCanonical?->isFree(),
|
||||
]);
|
||||
|
||||
try {
|
||||
|
||||
+4
-1
@@ -110,7 +110,10 @@ class Update extends Action
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
|
||||
try {
|
||||
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($providerRepositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ class Get extends Action
|
||||
}
|
||||
|
||||
$state = \json_decode($state, true);
|
||||
$redirectFailure = $state['failure'] ?? '';
|
||||
$projectId = $state['projectId'] ?? '';
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
@@ -75,11 +74,10 @@ class Get extends Action
|
||||
|
||||
if (!empty($redirectFailure)) {
|
||||
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
|
||||
$response
|
||||
return $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);
|
||||
@@ -167,11 +165,10 @@ class Get extends Action
|
||||
|
||||
if (!empty($redirectFailure)) {
|
||||
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
|
||||
$response
|
||||
return $response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
|
||||
|
||||
@@ -49,8 +49,6 @@ trait Deployment
|
||||
) {
|
||||
$errors = [];
|
||||
foreach ($repositories as $repository) {
|
||||
$logBase = 'vcs.github.event.repo.unknown';
|
||||
|
||||
try {
|
||||
$repositoryId = $repository->getId();
|
||||
$projectId = $repository->getAttribute('projectId');
|
||||
@@ -109,11 +107,18 @@ trait Deployment
|
||||
|
||||
$owner = $github->getOwnerName($providerInstallationId) ?? '';
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
} 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)) {
|
||||
@@ -286,7 +291,10 @@ trait Deployment
|
||||
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
@@ -493,7 +501,7 @@ trait Deployment
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
|
||||
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
|
||||
|
||||
if (!empty($previewUrl)) {
|
||||
$comment = new Comment($platform);
|
||||
@@ -516,7 +524,10 @@ trait Deployment
|
||||
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class Create extends Action
|
||||
default => null,
|
||||
};
|
||||
|
||||
$response->json($parsedPayload);
|
||||
return $response->json($parsedPayload);
|
||||
}
|
||||
|
||||
protected function preprocessEvent(Request $request)
|
||||
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -12,8 +13,6 @@ 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;
|
||||
@@ -125,7 +124,7 @@ class Doctor extends Action
|
||||
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG', '');
|
||||
|
||||
try {
|
||||
$loggingProvider = new DSN($providerConfig);
|
||||
$loggingProvider = new DSN($providerConfig ?? '');
|
||||
|
||||
$providerName = $loggingProvider->getScheme();
|
||||
|
||||
@@ -213,18 +212,15 @@ class Doctor extends Action
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var EmailAdapter $smtp */
|
||||
$smtp = $register->get('smtp');
|
||||
/* @var PHPMailer $mail */
|
||||
$mail = $register->get('smtp');
|
||||
|
||||
$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->addAddress('demo@example.com', 'Example.com');
|
||||
$mail->Subject = 'Test SMTP Connection';
|
||||
$mail->Body = 'Hello World';
|
||||
$mail->AltBody = 'Hello World';
|
||||
|
||||
$smtp->send($emailMessage);
|
||||
$mail->send();
|
||||
Console::success('🟢 ' . str_pad("SMTP", 50, '.') . 'connected');
|
||||
} catch (\Throwable) {
|
||||
Console::error('🔴 ' . str_pad("SMTP", 47, '.') . 'disconnected');
|
||||
|
||||
@@ -227,7 +227,7 @@ class Install extends Action
|
||||
// Fall back to CLI mode
|
||||
$enableAssistant = false;
|
||||
$assistantExistsInOldCompose = false;
|
||||
if ($existingInstallation) {
|
||||
if ($existingInstallation && isset($compose)) {
|
||||
try {
|
||||
$assistantService = $compose->getService('appwrite-assistant');
|
||||
$assistantExistsInOldCompose = $assistantService !== null;
|
||||
|
||||
@@ -566,8 +566,6 @@ 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;
|
||||
|
||||
@@ -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)) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
$schedule = $this->schedules[$scheduleKey];
|
||||
|
||||
@@ -60,7 +60,7 @@ class StatsResources extends Action
|
||||
|
||||
$interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600');
|
||||
|
||||
Console::loop(function () use ($queueForStatsResources) {
|
||||
Console::loop(function () use ($queueForStatsResources, $dbForPlatform) {
|
||||
|
||||
$last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours'));
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -50,11 +51,13 @@ class Audits extends Action
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param callable $getProjectDB
|
||||
* @param Document $project
|
||||
* @param callable(Document): \Utopia\Audit\Audit $getAudit
|
||||
* @param callable $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
|
||||
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -362,6 +363,7 @@ class Deletes extends Action
|
||||
/**
|
||||
* @param Document $project
|
||||
* @param callable $getProjectDB
|
||||
* @param Document $target
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -436,6 +438,7 @@ 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
|
||||
@@ -515,6 +518,7 @@ class Deletes extends Action
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Database $dbForPlatform
|
||||
* @param callable $getProjectDB
|
||||
* @param string $hourlyUsageRetentionDatetime
|
||||
* @return void
|
||||
@@ -582,6 +586,7 @@ class Deletes extends Action
|
||||
* @param Database $dbForPlatform
|
||||
* @param Document $document
|
||||
* @return void
|
||||
* @throws Authorization
|
||||
* @throws DatabaseException
|
||||
* @throws Conflict
|
||||
* @throws Restricted
|
||||
@@ -618,6 +623,7 @@ 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
|
||||
@@ -946,7 +952,7 @@ class Deletes extends Action
|
||||
// fast path, no need to list anything!
|
||||
$delete($dbForProject, $resourceInternalId, $resourceType);
|
||||
} else {
|
||||
$processResource = function (string $type) use ($dbForProject, $delete) {
|
||||
$processResource = function (string $type) use ($dbForProject, $delete, $resourceType) {
|
||||
$this->listByGroup(
|
||||
collection: $type,
|
||||
queries: [Query::select(['$id', '$sequence'])],
|
||||
@@ -1103,7 +1109,7 @@ class Deletes extends Action
|
||||
Query::equal('resourceInternalId', [$siteInternalId]),
|
||||
Query::equal('resourceType', ['sites']),
|
||||
Query::orderAsc()
|
||||
], $dbForProject, function (Document $document) use ($deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds, &$deploymentIds) {
|
||||
], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
|
||||
$deploymentInternalIds[] = $document->getSequence();
|
||||
$deploymentIds[] = $document->getId();
|
||||
$this->deleteBuildFiles($deviceForBuilds, $document);
|
||||
@@ -1166,7 +1172,7 @@ class Deletes extends Action
|
||||
Query::equal('deploymentResourceInternalId', [$functionInternalId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
|
||||
], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) {
|
||||
$this->deleteRule($dbForPlatform, $document, $certificates);
|
||||
});
|
||||
|
||||
@@ -1190,7 +1196,7 @@ class Deletes extends Action
|
||||
Query::equal('resourceInternalId', [$functionInternalId]),
|
||||
Query::equal('resourceType', ['functions']),
|
||||
Query::orderAsc()
|
||||
], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
|
||||
], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
|
||||
$deploymentInternalIds[] = $document->getSequence();
|
||||
$this->deleteDeploymentFiles($deviceForFunctions, $document);
|
||||
$this->deleteBuildFiles($deviceForBuilds, $document);
|
||||
@@ -1315,7 +1321,7 @@ class Deletes extends Action
|
||||
|
||||
/**
|
||||
* @param Device $device
|
||||
* @param Document $deployment
|
||||
* @param Document $build
|
||||
* @return void
|
||||
*/
|
||||
private function deleteBuildFiles(Device $device, Document $deployment): void
|
||||
@@ -1625,9 +1631,9 @@ class Deletes extends Action
|
||||
try {
|
||||
$dbForProject->deleteDocuments('transactions', [
|
||||
Query::lessThan('expiresAt', DateTime::format(new \DateTime())),
|
||||
], onNext: function (Document $transaction) use (&$transactionInternalIds) {
|
||||
], onNext: function (Document $transaction) use ($dbForProject, $project, &$transactionInternalIds) {
|
||||
$transactionInternalIds[] = $transaction->getSequence();
|
||||
}, onError: function (Throwable $th) {
|
||||
}, onError: function (Throwable $th) use ($project) {
|
||||
// Swallow errors to avoid breaking the cleanup process
|
||||
});
|
||||
} catch (Throwable $th) {
|
||||
@@ -1640,7 +1646,7 @@ class Deletes extends Action
|
||||
|
||||
$dbForProject->deleteDocuments('transactionLogs', [
|
||||
Query::equal('transactionInternalId', $transactionInternalIds),
|
||||
], onError: function (Throwable $th) {
|
||||
], onError: function (Throwable $th) use ($project) {
|
||||
// Swallow errors to avoid breaking the cleanup process
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,9 +508,6 @@ class Functions extends Action
|
||||
]);
|
||||
|
||||
/** Execute function */
|
||||
$error = null;
|
||||
$errorCode = 0;
|
||||
|
||||
try {
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$command = $runtime['startCommand'];
|
||||
|
||||
@@ -4,13 +4,10 @@ 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;
|
||||
@@ -52,9 +49,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
|
||||
*/
|
||||
@@ -135,38 +132,36 @@ class Mails extends Action
|
||||
// render() will return the subject in <p> tags, so use strip_tags() to remove them
|
||||
$subject = \strip_tags($subjectTemplate->render());
|
||||
|
||||
/** @var EmailAdapter $adapter */
|
||||
$adapter = empty($smtp)
|
||||
/** @var PHPMailer $mail */
|
||||
$mail = empty($smtp)
|
||||
? $register->get('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,
|
||||
);
|
||||
: $this->getMailer($smtp);
|
||||
|
||||
// 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->clearAddresses();
|
||||
$mail->clearAllRecipients();
|
||||
$mail->clearReplyTos();
|
||||
$mail->clearAttachments();
|
||||
$mail->clearBCCs();
|
||||
$mail->clearCCs();
|
||||
$mail->addAddress($recipient, $name);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $body;
|
||||
|
||||
$fromEmail = !empty($smtp) ? ($smtp['senderEmail'] ?? $defaultFromEmail) : $defaultFromEmail;
|
||||
$fromName = !empty($smtp) ? ($smtp['senderName'] ?? $defaultFromName) : $defaultFromName;
|
||||
$replyTo = $defaultFromEmail;
|
||||
$replyToName = $defaultFromName;
|
||||
$mail->AltBody = $body;
|
||||
$mail->AltBody = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $mail->AltBody);
|
||||
$mail->AltBody = \strip_tags($mail->AltBody);
|
||||
$mail->AltBody = \trim($mail->AltBody);
|
||||
|
||||
$replyTo = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$replyToName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
|
||||
$customMailOptions = $payload['customMailOptions'] ?? [];
|
||||
|
||||
if (!empty($customMailOptions['senderEmail'])) {
|
||||
$fromEmail = $customMailOptions['senderEmail'];
|
||||
}
|
||||
if (!empty($customMailOptions['senderName'])) {
|
||||
$fromName = $customMailOptions['senderName'];
|
||||
// fallback hierarchy: Custom options > SMTP config > Defaults.
|
||||
if (!empty($customMailOptions['senderEmail']) || !empty($customMailOptions['senderName'])) {
|
||||
$fromEmail = $customMailOptions['senderEmail'] ?? $mail->From;
|
||||
$fromName = $customMailOptions['senderName'] ?? $mail->FromName;
|
||||
$mail->setFrom($fromEmail, $fromName);
|
||||
}
|
||||
|
||||
if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) {
|
||||
@@ -177,32 +172,18 @@ class Mails extends Action
|
||||
$replyToName = $smtp['senderName'] ?? $replyToName;
|
||||
}
|
||||
|
||||
$attachments = null;
|
||||
$mail->addReplyTo($replyTo, $replyToName);
|
||||
if (!empty($attachment['content'] ?? '')) {
|
||||
$attachments = [
|
||||
new Attachment(
|
||||
name: $attachment['filename'] ?? 'unknown.file',
|
||||
path: '',
|
||||
type: $attachment['type'] ?? 'plain/text',
|
||||
content: \base64_decode($attachment['content']),
|
||||
),
|
||||
];
|
||||
$mail->AddStringAttachment(
|
||||
base64_decode($attachment['content']),
|
||||
$attachment['filename'] ?? 'unknown.file',
|
||||
$attachment['encoding'] ?? PHPMailer::ENCODING_BASE64,
|
||||
$attachment['type'] ?? 'plain/text'
|
||||
);
|
||||
}
|
||||
|
||||
$emailMessage = new EmailMessage(
|
||||
to: [['email' => $recipient, 'name' => $name]],
|
||||
subject: $subject,
|
||||
content: $body,
|
||||
fromName: $fromName,
|
||||
fromEmail: $fromEmail,
|
||||
replyToName: $replyToName,
|
||||
replyToEmail: $replyTo,
|
||||
attachments: $attachments,
|
||||
html: true,
|
||||
);
|
||||
|
||||
try {
|
||||
$adapter->send($emailMessage);
|
||||
$mail->send();
|
||||
} catch (\Throwable $error) {
|
||||
if ($type === 'smtp') {
|
||||
throw new Exception('Error sending mail: ' . $error->getMessage(), 401);
|
||||
@@ -210,4 +191,38 @@ class Mails extends Action
|
||||
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $smtp
|
||||
* @return PHPMailer
|
||||
* @throws \PHPMailer\PHPMailer\Exception
|
||||
*/
|
||||
protected function getMailer(array $smtp): PHPMailer
|
||||
{
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
$mail->isSMTP();
|
||||
|
||||
$username = $smtp['username'];
|
||||
$password = $smtp['password'];
|
||||
|
||||
$mail->XMailer = 'Appwrite Mailer';
|
||||
$mail->Host = $smtp['host'];
|
||||
$mail->Port = $smtp['port'];
|
||||
$mail->SMTPAuth = (!empty($username) && !empty($password));
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
$mail->SMTPSecure = $smtp['secure'];
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->SMTPKeepAlive = true;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 10; /* Connection timeout */
|
||||
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
|
||||
|
||||
$mail->setFrom($smtp['senderEmail'], $smtp['senderName']);
|
||||
|
||||
$mail->isHTML();
|
||||
|
||||
return $mail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ class Messaging extends Action
|
||||
|
||||
try {
|
||||
$response = $adapter->send($data);
|
||||
$deliveredTotal += (int) $response['deliveredTo'];
|
||||
$deliveredTotal += $response['deliveredTo'];
|
||||
foreach ($response['results'] as $result) {
|
||||
if ($result['status'] === 'failure') {
|
||||
$deliveryErrors[] = "Failed sending to target {$result['recipient']} with error: {$result['error']}";
|
||||
@@ -380,7 +380,7 @@ class Messaging extends Action
|
||||
]));
|
||||
|
||||
// Delete any attachments that were downloaded to local storage
|
||||
if ($providerType === MESSAGE_TYPE_EMAIL) {
|
||||
if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) {
|
||||
if ($deviceForFiles->getType() === Storage::DEVICE_LOCAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -408,7 +408,6 @@ class Migrations extends Action
|
||||
$tempAPIKey = $this->generateAPIKey($project);
|
||||
|
||||
$transfer = $source = $destination = null;
|
||||
$aggregatedResources = [];
|
||||
|
||||
$host = System::getEnv('_APP_MIGRATION_HOST');
|
||||
if (empty($host)) {
|
||||
@@ -445,6 +444,7 @@ class Migrations extends Action
|
||||
$destination
|
||||
);
|
||||
|
||||
$aggregatedResources = [];
|
||||
/** Start Transfer */
|
||||
if (empty($source->getErrors())) {
|
||||
$migration->setAttribute('stage', 'migrating');
|
||||
|
||||
@@ -208,7 +208,7 @@ class StatsResources extends Action
|
||||
{
|
||||
$totalFiles = 0;
|
||||
$totalStorage = 0;
|
||||
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $region, &$totalFiles, &$totalStorage) {
|
||||
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalFiles, &$totalStorage) {
|
||||
try {
|
||||
$files = $dbForProject->count('bucket_' . $bucket->getSequence());
|
||||
} catch (Throwable $th) {
|
||||
|
||||
@@ -140,7 +140,7 @@ class StatsUsage extends Action
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param callable(Document): Database $getProjectDB
|
||||
* @param callable(): Database $getProjectDB
|
||||
* @param callable(): Database $getLogsDB
|
||||
* @param Registry $register
|
||||
* @return void
|
||||
@@ -212,7 +212,7 @@ class StatsUsage extends Action
|
||||
* @param Document $project
|
||||
* @param Document $document
|
||||
* @param array $metrics
|
||||
* @param callable(Document): Database $getProjectDB
|
||||
* @param callable(): Database $getProjectDB
|
||||
* @param string $databaseType Database type from context
|
||||
* @return void
|
||||
*/
|
||||
@@ -394,7 +394,7 @@ class StatsUsage extends Action
|
||||
|
||||
/**
|
||||
* Commit stats to DB
|
||||
* @param callable(Document): Database $getProjectDB
|
||||
* @param callable(): Database $getProjectDB
|
||||
* @return void
|
||||
*/
|
||||
public function commitToDb(callable $getProjectDB): void
|
||||
@@ -459,7 +459,7 @@ class StatsUsage extends Action
|
||||
/**
|
||||
* Sort by unique index key reduce locks/deadlocks
|
||||
*/
|
||||
usort($projectStats['stats'], function ($a, $b) {
|
||||
usort($projectStats['stats'], function ($a, $b) use ($sequence) {
|
||||
// Metric DESC
|
||||
$cmp = strcmp($b['metric'], $a['metric']);
|
||||
if ($cmp !== 0) {
|
||||
|
||||
@@ -233,7 +233,7 @@ class Webhooks extends Action
|
||||
$template->setParam('{{webhook}}', $webhook->getAttribute('name'));
|
||||
$template->setParam('{{project}}', $project->getAttribute('name'));
|
||||
$template->setParam('{{url}}', $webhook->getAttribute('url'));
|
||||
$template->setParam('{{error}}', 'The server returned ' . $statusCode . ' status code');
|
||||
$template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code');
|
||||
$template->setParam('{{path}}', "/console/project-$region-$projectId/settings/webhooks/$webhookId");
|
||||
$template->setParam('{{attempts}}', $attempts);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Promises;
|
||||
|
||||
/** @phpstan-consistent-constructor */
|
||||
abstract class Promise
|
||||
{
|
||||
protected const STATE_PENDING = 1;
|
||||
|
||||
@@ -197,7 +197,7 @@ class Method
|
||||
|
||||
public function isHidden(): bool|array
|
||||
{
|
||||
return $this->hide;
|
||||
return $this->hide ?? false;
|
||||
}
|
||||
|
||||
public function isPackaging(): bool
|
||||
|
||||
@@ -204,31 +204,15 @@ abstract class Format
|
||||
*
|
||||
* Get services value
|
||||
*
|
||||
* @param array $services
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function getServices(): array
|
||||
{
|
||||
return $this->services;
|
||||
}
|
||||
|
||||
protected function getDescriptionContents(?string $description): string
|
||||
{
|
||||
if ($description === null || $description === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!\str_ends_with($description, '.md')) {
|
||||
return $description;
|
||||
}
|
||||
|
||||
$contents = @\file_get_contents($description);
|
||||
|
||||
if ($contents === false) {
|
||||
throw new \RuntimeException('Documentation file not found or unreadable: ' . $description);
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
protected function getRequestEnumName(string $service, string $method, string $param): ?string
|
||||
{
|
||||
/* `$service` is `$namespace` */
|
||||
|
||||
@@ -79,8 +79,8 @@ class OpenAPI3 extends Format
|
||||
$output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '<YOUR_API_KEY>'];
|
||||
}
|
||||
|
||||
if (isset($output['components']['securitySchemes']['JWT'])) {
|
||||
$output['components']['securitySchemes']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
|
||||
if (isset($output['securityDefinitions']['JWT'])) {
|
||||
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
|
||||
}
|
||||
|
||||
if (isset($output['components']['securitySchemes']['Locale'])) {
|
||||
@@ -99,7 +99,7 @@ class OpenAPI3 extends Format
|
||||
|
||||
$sdk = $route->getLabel('sdk', false);
|
||||
|
||||
if ($sdk === false) {
|
||||
if (empty($sdk)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,8 @@ class OpenAPI3 extends Format
|
||||
|
||||
$namespace = $sdk->getNamespace() ?? 'default';
|
||||
|
||||
$descContents = $this->getDescriptionContents($desc);
|
||||
$desc ??= '';
|
||||
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
|
||||
|
||||
$temp = [
|
||||
'summary' => $route->getDesc(),
|
||||
@@ -162,7 +163,7 @@ class OpenAPI3 extends Format
|
||||
];
|
||||
}
|
||||
|
||||
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
|
||||
if (!empty($additionalMethods)) {
|
||||
$temp['x-appwrite']['methods'] = [];
|
||||
foreach ($additionalMethods as $methodObj) {
|
||||
/** @var Method $methodObj */
|
||||
@@ -190,7 +191,7 @@ class OpenAPI3 extends Format
|
||||
'parameters' => [],
|
||||
'required' => [],
|
||||
'responses' => [],
|
||||
'description' => $this->getDescriptionContents($desc),
|
||||
'description' => ($desc) ? \file_get_contents($desc) : '',
|
||||
'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md',
|
||||
'public' => $methodObj->isPublic(),
|
||||
];
|
||||
@@ -328,7 +329,7 @@ class OpenAPI3 extends Format
|
||||
|
||||
if (($response->getCode() ?? 500) === 204) {
|
||||
$temp['responses'][(string)$response->getCode() ?? '500']['description'] = 'No content';
|
||||
unset($temp['responses'][(string)$response->getCode() ?? '500']['content']);
|
||||
unset($temp['responses'][(string)$response->getCode() ?? '500']['schema']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,7 +383,7 @@ class OpenAPI3 extends Format
|
||||
$validator = $validator->getValidator();
|
||||
}
|
||||
|
||||
$class = $validator instanceof Validator
|
||||
$class = !empty($validator)
|
||||
? \get_class($validator)
|
||||
: '';
|
||||
|
||||
@@ -430,7 +431,7 @@ class OpenAPI3 extends Format
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
|
||||
break;
|
||||
case \Utopia\Database\Validator\Datetime::class:
|
||||
case \Utopia\Database\Validator\DatetimeValidator::class:
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
$node['schema']['format'] = 'datetime';
|
||||
$node['schema']['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
|
||||
@@ -462,6 +463,7 @@ class OpenAPI3 extends Format
|
||||
$node['schema']['x-example'] = ($param['example'] ?? '') ?: 'https://example.com';
|
||||
break;
|
||||
case \Utopia\Validator\JSON::class:
|
||||
case \Utopia\Validator\Mock::class:
|
||||
case \Utopia\Validator\Assoc::class:
|
||||
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
|
||||
$node['schema']['type'] = 'object';
|
||||
@@ -561,6 +563,12 @@ class OpenAPI3 extends Format
|
||||
$node['schema']['x-example'] = $param['example'];
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\Length::class:
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
if (!empty($param['example'])) {
|
||||
$node['schema']['x-example'] = $param['example'];
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\WhiteList::class:
|
||||
if ($array) {
|
||||
$validator = $validator->getValidator();
|
||||
|
||||
@@ -96,9 +96,10 @@ class Swagger2 extends Format
|
||||
|
||||
$scope = $route->getLabel('scope', '');
|
||||
|
||||
/** @var Method $sdk */
|
||||
$sdk = $route->getLabel('sdk', false);
|
||||
|
||||
if ($sdk === false) {
|
||||
if (empty($sdk)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -126,7 +127,8 @@ class Swagger2 extends Format
|
||||
$sdkPlatforms = array_values(array_unique($sdkPlatforms));
|
||||
$namespace = $sdk->getNamespace() ?? 'default';
|
||||
|
||||
$descContents = $this->getDescriptionContents($desc);
|
||||
$desc ??= '';
|
||||
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
|
||||
|
||||
$temp = [
|
||||
'summary' => $route->getDesc(),
|
||||
@@ -169,7 +171,7 @@ class Swagger2 extends Format
|
||||
$temp['produces'][] = $produces;
|
||||
}
|
||||
|
||||
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
|
||||
if (!empty($additionalMethods)) {
|
||||
$temp['x-appwrite']['methods'] = [];
|
||||
foreach ($additionalMethods as $methodObj) {
|
||||
/** @var Method $methodObj */
|
||||
@@ -198,7 +200,7 @@ class Swagger2 extends Format
|
||||
'parameters' => [],
|
||||
'required' => [],
|
||||
'responses' => [],
|
||||
'description' => $this->getDescriptionContents($desc),
|
||||
'description' => ($desc) ? \file_get_contents($desc) : '',
|
||||
'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md',
|
||||
'public' => $methodObj->isPublic(),
|
||||
];
|
||||
@@ -386,7 +388,7 @@ class Swagger2 extends Format
|
||||
$validator = $validator->getValidator();
|
||||
}
|
||||
|
||||
$class = $validator instanceof Validator
|
||||
$class = !empty($validator)
|
||||
? \get_class($validator)
|
||||
: '';
|
||||
|
||||
@@ -434,7 +436,7 @@ class Swagger2 extends Format
|
||||
$node['type'] = $validator->getType();
|
||||
$node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
|
||||
break;
|
||||
case \Utopia\Database\Validator\Datetime::class:
|
||||
case \Utopia\Database\Validator\DatetimeValidator::class:
|
||||
$node['type'] = $validator->getType();
|
||||
$node['format'] = 'datetime';
|
||||
$node['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
|
||||
@@ -477,6 +479,7 @@ class Swagger2 extends Format
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\JSON::class:
|
||||
case \Utopia\Validator\Mock::class:
|
||||
case \Utopia\Validator\Assoc::class:
|
||||
$node['type'] = 'object';
|
||||
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
|
||||
@@ -549,6 +552,12 @@ class Swagger2 extends Format
|
||||
$node['x-example'] = $param['example'];
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\Length::class:
|
||||
$node['type'] = $validator->getType();
|
||||
if (!empty($param['example'])) {
|
||||
$node['x-example'] = $param['example'];
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\WhiteList::class:
|
||||
if ($array) {
|
||||
$validator = $validator->getValidator();
|
||||
|
||||
@@ -86,6 +86,7 @@ class User extends Document
|
||||
/**
|
||||
* Check if user is anonymous.
|
||||
*
|
||||
* @param Document $this
|
||||
* @return bool
|
||||
*/
|
||||
public function isAnonymous(): bool
|
||||
@@ -152,6 +153,7 @@ class User extends Document
|
||||
/**
|
||||
* Verify session and check that its not expired.
|
||||
*
|
||||
* @param array<Document> $sessions
|
||||
* @param string $secret
|
||||
*
|
||||
* @return bool|string
|
||||
|
||||
@@ -17,7 +17,7 @@ class Request extends UtopiaRequest
|
||||
* @var array<Filter>
|
||||
*/
|
||||
private array $filters = [];
|
||||
private ?Route $route = null;
|
||||
private static ?Route $route = null;
|
||||
|
||||
public function __construct(SwooleRequest $request)
|
||||
{
|
||||
@@ -34,11 +34,11 @@ class Request extends UtopiaRequest
|
||||
{
|
||||
$parameters = parent::getParams();
|
||||
|
||||
if (!$this->hasFilters() || !$this->hasRoute()) {
|
||||
if (!$this->hasFilters() || !self::hasRoute()) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
$methods = $this->getRoute()?->getLabel('sdk', null);
|
||||
$methods = self::getRoute()->getLabel('sdk', null);
|
||||
|
||||
if (empty($methods)) {
|
||||
return $parameters;
|
||||
@@ -131,9 +131,9 @@ class Request extends UtopiaRequest
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setRoute(?Route $route): void
|
||||
public static function setRoute(?Route $route): void
|
||||
{
|
||||
$this->route = $route;
|
||||
self::$route = $route;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,9 +141,9 @@ class Request extends UtopiaRequest
|
||||
*
|
||||
* @return Route|null
|
||||
*/
|
||||
public function getRoute(): ?Route
|
||||
public static function getRoute(): ?Route
|
||||
{
|
||||
return $this->route;
|
||||
return self::$route;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,9 +151,9 @@ class Request extends UtopiaRequest
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasRoute(): bool
|
||||
public static function hasRoute(): bool
|
||||
{
|
||||
return $this->route !== null;
|
||||
return self::$route !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -299,7 +299,7 @@ class Response extends SwooleResponse
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $showSensitive = false;
|
||||
protected static bool $showSensitive = false;
|
||||
|
||||
/**
|
||||
* @var array<string, Model>
|
||||
@@ -509,7 +509,7 @@ class Response extends SwooleResponse
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
|
||||
if ((!$isPrivilegedUser && !$isAppUser) && !$this->showSensitive) {
|
||||
if ((!$isPrivilegedUser && !$isAppUser) && !self::$showSensitive) {
|
||||
$data->setAttribute($key, '');
|
||||
}
|
||||
}
|
||||
@@ -659,20 +659,18 @@ class Response extends SwooleResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to show sensitive data in response
|
||||
* Static wrapper to show sensitive data in response
|
||||
*
|
||||
* @param callable(): array $callback The callback to show sensitive information for
|
||||
* @return array
|
||||
*/
|
||||
public function showSensitive(callable $callback): array
|
||||
public static function showSensitive(callable $callback): array
|
||||
{
|
||||
$previous = $this->showSensitive;
|
||||
|
||||
try {
|
||||
$this->showSensitive = true;
|
||||
self::$showSensitive = true;
|
||||
return $callback();
|
||||
} finally {
|
||||
$this->showSensitive = $previous;
|
||||
self::$showSensitive = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,24 +137,6 @@ class Project extends Model
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authDisposableEmails', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not to disallow disposable email addresses during signup and email updates.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authCanonicalEmails', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not to require canonical email addresses during signup and email updates.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authFreeEmails', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not to disallow free email addresses during signup and email updates.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authMockNumbers', [
|
||||
'type' => Response::MODEL_MOCK_NUMBER,
|
||||
'description' => 'An array of mock numbers and their corresponding verification codes (OTPs).',
|
||||
@@ -433,9 +415,6 @@ class Project extends Model
|
||||
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
|
||||
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
|
||||
$document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false);
|
||||
$document->setAttribute('authDisposableEmails', $authValues['disposableEmails'] ?? false);
|
||||
$document->setAttribute('authCanonicalEmails', $authValues['canonicalEmails'] ?? false);
|
||||
$document->setAttribute('authFreeEmails', $authValues['freeEmails'] ?? false);
|
||||
$document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []);
|
||||
$document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false);
|
||||
$document->setAttribute('authMembershipsUserName', $authValues['membershipsUserName'] ?? true);
|
||||
|
||||
@@ -29,8 +29,8 @@ class DatabasesStringTypesTest extends Scope
|
||||
protected function setupDatabaseAndCollection(): array
|
||||
{
|
||||
$cacheKey = $this->getProject()['$id'] ?? 'default';
|
||||
if (!empty(self::$setupCache[$cacheKey])) {
|
||||
return self::$setupCache[$cacheKey];
|
||||
if (!empty(static::$setupCache[$cacheKey])) {
|
||||
return static::$setupCache[$cacheKey];
|
||||
}
|
||||
|
||||
$projectId = $this->getProject()['$id'];
|
||||
@@ -135,12 +135,12 @@ class DatabasesStringTypesTest extends Scope
|
||||
// Wait for all attributes to be available
|
||||
$this->waitForAllAttributes($databaseId, $collectionId);
|
||||
|
||||
self::$setupCache[$cacheKey] = [
|
||||
static::$setupCache[$cacheKey] = [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
];
|
||||
|
||||
return self::$setupCache[$cacheKey];
|
||||
return static::$setupCache[$cacheKey];
|
||||
}
|
||||
|
||||
public function testCreateDatabase(): void
|
||||
|
||||
@@ -45,12 +45,6 @@ trait DatabasesPermissionsBase
|
||||
return $recordId ? "{$base}/{$recordId}" : $base;
|
||||
}
|
||||
|
||||
protected function getIndexUrl(string $databaseId, string $containerId, string $indexId = ''): string
|
||||
{
|
||||
$base = "{$this->getContainerUrl($databaseId, $containerId)}/indexes";
|
||||
return $indexId ? "{$base}/{$indexId}" : $base;
|
||||
}
|
||||
|
||||
// User Management Methods
|
||||
public function createUser(string $id, string $email, string $password = 'test123!'): array
|
||||
{
|
||||
|
||||
@@ -100,24 +100,6 @@ trait FunctionsBase
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]));
|
||||
$this->assertNotEquals(401, $function['headers']['status-code'], 'Auth failed while polling function activation');
|
||||
|
||||
if (
|
||||
($function['body']['deploymentId'] ?? '') !== $deploymentId
|
||||
&& ($function['body']['latestDeploymentId'] ?? '') === $deploymentId
|
||||
&& ($function['body']['latestDeploymentStatus'] ?? '') === 'ready'
|
||||
) {
|
||||
$activation = $this->updateFunctionDeployment($functionId, $deploymentId);
|
||||
$this->assertContains(
|
||||
$activation['headers']['status-code'],
|
||||
[200, 409],
|
||||
'Deployment activation request failed: ' . json_encode($activation['body'], JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
if ($activation['headers']['status-code'] === 200) {
|
||||
$function = $activation;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertEquals($deploymentId, $function['body']['deploymentId'] ?? '', 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
|
||||
}, 120000, 500);
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ class FunctionsCustomClientTest extends Scope
|
||||
'offset' => 2
|
||||
]);
|
||||
$this->assertEquals(200, $templatesOffset['headers']['status-code']);
|
||||
$this->addToAssertionCount(1);
|
||||
$this->addToAssertionCount(1, $templatesOffset['body']['templates']);
|
||||
$this->assertEquals($templates['body']['templates'][2]['id'], $templatesOffset['body']['templates'][0]['id']);
|
||||
|
||||
// List templates with filters
|
||||
|
||||
@@ -1003,7 +1003,6 @@ class FunctionsCustomServerTest extends Scope
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
];
|
||||
$id = '';
|
||||
$largeTag = null;
|
||||
while (!feof($handle)) {
|
||||
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'large-fx.tar.gz');
|
||||
$headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user