2025-03-15 11:00:36 +00:00
< ? php
use Ahc\Jwt\JWT ;
use Ahc\Jwt\JWTException ;
2025-03-15 13:32:18 +00:00
use Appwrite\Auth\Key ;
2025-10-10 05:25:48 +00:00
use Appwrite\Databases\TransactionState ;
2025-12-14 01:43:35 +00:00
use Appwrite\Event\Audit as AuditEvent ;
2025-03-15 11:00:36 +00:00
use Appwrite\Event\Build ;
use Appwrite\Event\Certificate ;
use Appwrite\Event\Database as EventDatabase ;
use Appwrite\Event\Delete ;
use Appwrite\Event\Event ;
use Appwrite\Event\Func ;
use Appwrite\Event\Mail ;
use Appwrite\Event\Messaging ;
use Appwrite\Event\Migration ;
2026-03-11 14:01:26 +00:00
use Appwrite\Event\Publisher\Usage as UsagePublisher ;
2025-03-15 13:32:18 +00:00
use Appwrite\Event\Realtime ;
2026-01-08 15:51:04 +00:00
use Appwrite\Event\Screenshot ;
2025-09-01 14:03:49 +00:00
use Appwrite\Event\StatsResources ;
2025-03-15 13:32:18 +00:00
use Appwrite\Event\Webhook ;
2025-03-15 11:00:36 +00:00
use Appwrite\Extend\Exception ;
2026-01-07 14:57:57 +00:00
use Appwrite\Functions\EventProcessor ;
2025-03-15 11:00:36 +00:00
use Appwrite\GraphQL\Schema ;
2025-12-07 20:29:45 +00:00
use Appwrite\Network\Cors ;
2025-04-14 11:56:42 +00:00
use Appwrite\Network\Platform ;
2025-03-15 11:00:36 +00:00
use Appwrite\Network\Validator\Origin ;
2025-12-07 20:29:45 +00:00
use Appwrite\Network\Validator\Redirect ;
2026-03-11 14:01:26 +00:00
use Appwrite\Usage\Context as UsageContext ;
2025-11-04 03:48:57 +00:00
use Appwrite\Utopia\Database\Documents\User ;
2025-03-15 13:32:18 +00:00
use Appwrite\Utopia\Request ;
2025-11-03 09:32:56 +00:00
use Appwrite\Utopia\Response ;
2025-03-25 11:33:03 +00:00
use Executor\Executor ;
2025-03-15 13:38:01 +00:00
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis ;
2026-03-19 15:00:42 +00:00
use Utopia\Agents\Adapters\Ollama ;
use Utopia\Agents\Agent ;
2025-12-14 01:43:35 +00:00
use Utopia\Audit\Adapter\Database as AdapterDatabase ;
use Utopia\Audit\Audit ;
2025-11-03 09:32:56 +00:00
use Utopia\Auth\Hashes\Argon2 ;
use Utopia\Auth\Hashes\Sha ;
use Utopia\Auth\Proofs\Code ;
use Utopia\Auth\Proofs\Password ;
use Utopia\Auth\Proofs\Token ;
use Utopia\Auth\Store ;
2025-05-14 06:14:07 +00:00
use Utopia\Cache\Adapter\Pool as CachePool ;
2025-03-15 11:00:36 +00:00
use Utopia\Cache\Adapter\Sharding ;
use Utopia\Cache\Cache ;
use Utopia\Config\Config ;
2026-02-10 05:04:24 +00:00
use Utopia\Console ;
2025-05-14 06:14:07 +00:00
use Utopia\Database\Adapter\Pool as DatabasePool ;
2025-03-15 11:00:36 +00:00
use Utopia\Database\Database ;
2025-03-22 13:08:12 +00:00
use Utopia\Database\DateTime as DatabaseDateTime ;
2025-03-15 11:00:36 +00:00
use Utopia\Database\Document ;
use Utopia\Database\Query ;
use Utopia\Database\Validator\Authorization ;
use Utopia\DSN\DSN ;
2026-02-10 05:04:24 +00:00
use Utopia\Http\Http ;
2025-03-15 11:00:36 +00:00
use Utopia\Locale\Locale ;
use Utopia\Logger\Log ;
use Utopia\Pools\Group ;
2025-05-14 06:14:07 +00:00
use Utopia\Queue\Broker\Pool as BrokerPool ;
2025-03-15 13:32:18 +00:00
use Utopia\Queue\Publisher ;
2026-03-11 14:01:26 +00:00
use Utopia\Queue\Queue ;
2025-03-15 11:00:36 +00:00
use Utopia\Storage\Device ;
2025-02-28 12:52:34 +00:00
use Utopia\Storage\Device\AWS ;
2025-03-15 11:00:36 +00:00
use Utopia\Storage\Device\Backblaze ;
use Utopia\Storage\Device\DOSpaces ;
use Utopia\Storage\Device\Linode ;
use Utopia\Storage\Device\Local ;
use Utopia\Storage\Device\S3 ;
use Utopia\Storage\Device\Wasabi ;
use Utopia\Storage\Storage ;
use Utopia\System\System ;
2025-04-08 11:12:28 +00:00
use Utopia\Telemetry\Adapter as Telemetry ;
use Utopia\Telemetry\Adapter\None as NoTelemetry ;
2025-12-07 20:29:45 +00:00
use Utopia\Validator\URL ;
2025-04-14 03:55:04 +00:00
use Utopia\Validator\WhiteList ;
2025-04-08 11:24:05 +00:00
use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub ;
2025-03-15 11:00:36 +00:00
2025-03-15 12:18:34 +00:00
// Runtime Execution
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'log' , fn () => new Log ());
Http :: setResource ( 'logger' , function ( $register ) {
2025-03-15 11:00:36 +00:00
return $register -> get ( 'logger' );
}, [ 'register' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'hooks' , function ( $register ) {
2025-03-15 11:00:36 +00:00
return $register -> get ( 'hooks' );
}, [ 'register' ]);
2026-02-04 05:30:22 +00:00
global $register ;
Http :: setResource ( 'register' , fn () => $register );
Http :: setResource ( 'locale' , function () {
2025-08-12 13:13:08 +00:00
$locale = new Locale ( System :: getEnv ( '_APP_LOCALE' , 'en' ));
$locale -> setFallback ( System :: getEnv ( '_APP_LOCALE' , 'en' ));
2026-03-11 14:01:26 +00:00
2025-08-12 13:13:08 +00:00
return $locale ;
});
2025-03-15 11:00:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'localeCodes' , function () {
2025-03-15 11:00:36 +00:00
return array_map ( fn ( $locale ) => $locale [ 'code' ], Config :: getParam ( 'locale-codes' , []));
});
// Queues
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'publisher' , function ( Group $pools ) {
2025-05-14 06:14:07 +00:00
return new BrokerPool ( publisher : $pools -> get ( 'publisher' ));
2025-03-15 11:00:36 +00:00
}, [ 'pools' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'publisherDatabases' , function ( Publisher $publisher ) {
2025-09-03 11:08:07 +00:00
return $publisher ;
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'publisherFunctions' , function ( Publisher $publisher ) {
2025-09-03 11:08:07 +00:00
return $publisher ;
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'publisherMigrations' , function ( Publisher $publisher ) {
2025-09-03 11:08:07 +00:00
return $publisher ;
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'publisherMails' , function ( Publisher $publisher ) {
2025-09-03 11:08:07 +00:00
return $publisher ;
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'publisherDeletes' , function ( Publisher $publisher ) {
2025-09-03 11:08:07 +00:00
return $publisher ;
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'publisherMessaging' , function ( Publisher $publisher ) {
2025-09-03 11:08:07 +00:00
return $publisher ;
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'publisherWebhooks' , function ( Publisher $publisher ) {
2025-09-03 11:08:07 +00:00
return $publisher ;
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForMessaging' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Messaging ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForMails' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Mail ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForBuilds' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Build ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForScreenshots' , function ( Publisher $publisher ) {
2026-01-08 15:51:04 +00:00
return new Screenshot ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForDatabase' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new EventDatabase ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForDeletes' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Delete ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForEvents' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Event ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForWebhooks' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Webhook ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForRealtime' , function () {
2025-03-15 12:18:34 +00:00
return new Realtime ();
}, []);
2026-03-11 14:01:26 +00:00
Http :: setResource ( 'usage' , function () {
return new UsageContext ();
}, []);
Http :: setResource ( 'publisherForUsage' , fn ( Publisher $publisher ) => new UsagePublisher (
$publisher ,
new Queue ( System :: getEnv ( '_APP_STATS_USAGE_QUEUE_NAME' , Event :: STATS_USAGE_QUEUE_NAME ))
), [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForAudits' , function ( Publisher $publisher ) {
2025-12-14 01:43:35 +00:00
return new AuditEvent ( $publisher );
2025-03-15 12:18:34 +00:00
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForFunctions' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Func ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'eventProcessor' , function () {
2026-01-07 14:57:57 +00:00
return new EventProcessor ();
}, []);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForCertificates' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Certificate ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForMigrations' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Migration ( $publisher );
}, [ 'publisher' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'queueForStatsResources' , function ( Publisher $publisher ) {
2025-09-03 11:08:07 +00:00
return new StatsResources ( $publisher );
}, [ 'publisher' ]);
2025-12-07 20:29:45 +00:00
/**
* Platform configuration
*/
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'platform' , function () {
2025-12-16 22:35:39 +00:00
return Config :: getParam ( 'platform' , []);
}, []);
2025-12-07 20:29:45 +00:00
/**
* List of allowed request hostnames for the request .
*/
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'allowedHostnames' , function ( array $platform , Document $project , Document $rule , Document $devKey , Request $request ) {
2025-12-11 18:36:11 +00:00
$allowed = [ ... ( $platform [ 'hostnames' ] ? ? [])];
2025-12-07 20:29:45 +00:00
/* Add platform configured hostnames */
2026-03-11 14:01:26 +00:00
if ( ! $project -> isEmpty () && $project -> getId () !== 'console' ) {
2025-12-07 20:29:45 +00:00
$platforms = $project -> getAttribute ( 'platforms' , []);
$hostnames = Platform :: getHostnames ( $platforms );
$allowed = [ ... $allowed , ... $hostnames ];
2025-03-15 11:00:36 +00:00
}
2025-12-07 20:29:45 +00:00
/* Add the request hostname if a dev key is found */
2026-03-11 14:01:26 +00:00
if ( ! $devKey -> isEmpty ()) {
2025-12-07 20:29:45 +00:00
$allowed [] = $request -> getHostname ();
}
2025-09-16 08:46:02 +00:00
2025-12-07 20:29:45 +00:00
$originHostname = parse_url ( $request -> getOrigin (), PHP_URL_HOST );
2026-02-08 13:36:34 +00:00
$refererHostname = parse_url ( $request -> getReferer (), PHP_URL_HOST );
$hostname = $originHostname ;
if ( empty ( $hostname )) {
$hostname = $refererHostname ;
}
2025-12-12 09:21:57 +00:00
/* Add request hostname for preflight requests */
if ( $request -> getMethod () === 'OPTIONS' ) {
2026-02-08 13:36:34 +00:00
$allowed [] = $hostname ;
}
/* Allow the request origin of rule */
2026-03-11 14:01:26 +00:00
if ( ! $rule -> isEmpty () && ! empty ( $rule -> getAttribute ( 'domain' , '' ))) {
2026-02-08 13:36:34 +00:00
$allowed [] = $rule -> getAttribute ( 'domain' , '' );
2025-12-12 09:21:57 +00:00
}
2026-02-08 13:36:34 +00:00
/* Allow the request origin if a dev key is found */
2026-03-11 14:01:26 +00:00
if ( ! $devKey -> isEmpty () && ! empty ( $hostname )) {
2026-02-08 13:36:34 +00:00
$allowed [] = $hostname ;
2025-03-15 11:00:36 +00:00
}
2025-12-07 20:29:45 +00:00
return array_unique ( $allowed );
2025-12-11 18:36:11 +00:00
}, [ 'platform' , 'project' , 'rule' , 'devKey' , 'request' ]);
2025-12-07 20:29:45 +00:00
/**
* List of allowed request schemes for the request .
*/
2026-02-25 10:50:44 +00:00
Http :: setResource ( 'allowedSchemes' , function ( array $platform , Document $project ) {
$allowed = [ ... ( $platform [ 'schemas' ] ? ? [])];
2025-12-07 20:29:45 +00:00
2026-03-11 14:01:26 +00:00
if ( ! $project -> isEmpty () && $project -> getId () !== 'console' ) {
2025-12-07 20:29:45 +00:00
/* Add hardcoded schemes */
$allowed [] = 'exp' ;
$allowed [] = 'appwrite-callback-' . $project -> getId ();
/* Add platform configured schemes */
$platforms = $project -> getAttribute ( 'platforms' , []);
$schemes = Platform :: getSchemes ( $platforms );
$allowed = [ ... $allowed , ... $schemes ];
2025-03-15 11:00:36 +00:00
}
2025-12-07 20:29:45 +00:00
return array_unique ( $allowed );
2026-02-25 10:50:44 +00:00
}, [ 'platform' , 'project' ]);
2025-09-16 08:46:02 +00:00
2025-12-07 20:29:45 +00:00
/**
* Rule associated with a request origin .
*/
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'rule' , function ( Request $request , Database $dbForPlatform , Document $project , Authorization $authorization ) {
2025-12-07 20:29:45 +00:00
$domain = \parse_url ( $request -> getOrigin (), PHP_URL_HOST );
2026-02-08 13:12:23 +00:00
2026-02-08 13:36:34 +00:00
if ( empty ( $domain )) {
$domain = \parse_url ( $request -> getReferer (), PHP_URL_HOST );
2026-02-08 13:12:23 +00:00
}
2025-12-07 20:29:45 +00:00
if ( empty ( $domain )) {
return new Document ();
2025-09-16 08:46:02 +00:00
}
2025-12-07 20:29:45 +00:00
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System :: getEnv ( '_APP_RULES_FORMAT' ) === 'md5' ;
2026-01-07 07:04:28 +00:00
$rule = $authorization -> skip ( function () use ( $dbForPlatform , $domain , $isMd5 ) {
2025-12-07 20:29:45 +00:00
if ( $isMd5 ) {
return $dbForPlatform -> getDocument ( 'rules' , md5 ( $domain ));
2025-09-16 08:46:02 +00:00
}
2025-12-07 20:29:45 +00:00
return $dbForPlatform -> findOne ( 'rules' , [
Query :: equal ( 'domain' , [ $domain ]),
]) ? ? new Document ();
});
2026-02-08 13:36:34 +00:00
$permitsCurrentProject = $rule -> getAttribute ( 'projectInternalId' , '' ) === $project -> getSequence ();
// Temporary implementation until custom wildcard domains are an official feature
// Allow trusted projects; Used for Console (website) previews
2026-03-11 14:01:26 +00:00
if ( ! $permitsCurrentProject && ! $rule -> isEmpty () && ! empty ( $rule -> getAttribute ( 'projectId' , '' ))) {
2026-02-08 13:36:34 +00:00
$trustedProjects = [];
foreach ( \explode ( ',' , System :: getEnv ( '_APP_CONSOLE_TRUSTED_PROJECTS' , '' )) as $trustedProject ) {
if ( empty ( $trustedProject )) {
continue ;
2026-02-04 13:45:38 +00:00
}
2026-02-08 13:36:34 +00:00
$trustedProjects [] = $trustedProject ;
2026-02-04 13:45:38 +00:00
}
2026-02-08 13:36:34 +00:00
if ( \in_array ( $rule -> getAttribute ( 'projectId' , '' ), $trustedProjects )) {
$permitsCurrentProject = true ;
2026-02-04 13:45:38 +00:00
}
}
2026-03-11 14:01:26 +00:00
if ( ! $permitsCurrentProject ) {
2025-12-07 20:29:45 +00:00
return new Document ();
2025-09-16 08:46:02 +00:00
}
2025-12-07 20:29:45 +00:00
return $rule ;
2026-01-07 07:04:28 +00:00
}, [ 'request' , 'dbForPlatform' , 'project' , 'authorization' ]);
2025-12-07 20:29:45 +00:00
/**
* CORS service
*/
2026-02-10 10:52:53 +00:00
Http :: setResource ( 'cors' , function ( array $allowedHostnames ) {
$corsConfig = Config :: getParam ( 'cors' );
return new Cors (
$allowedHostnames ,
allowedMethods : $corsConfig [ 'allowedMethods' ],
allowedHeaders : $corsConfig [ 'allowedHeaders' ],
allowCredentials : true ,
exposedHeaders : $corsConfig [ 'exposedHeaders' ],
);
}, [ 'allowedHostnames' ]);
2025-12-07 20:29:45 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'originValidator' , function ( Document $devKey , array $allowedHostnames , array $allowedSchemes ) {
2026-03-11 14:01:26 +00:00
if ( ! $devKey -> isEmpty ()) {
2025-12-07 20:29:45 +00:00
return new URL ();
}
2026-03-11 14:01:26 +00:00
2025-12-07 20:29:45 +00:00
return new Origin ( $allowedHostnames , $allowedSchemes );
}, [ 'devKey' , 'allowedHostnames' , 'allowedSchemes' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'redirectValidator' , function ( Document $devKey , array $allowedHostnames , array $allowedSchemes ) {
2026-03-11 14:01:26 +00:00
if ( ! $devKey -> isEmpty ()) {
2025-12-07 20:29:45 +00:00
return new URL ();
}
2026-03-11 14:01:26 +00:00
2025-12-07 20:29:45 +00:00
return new Redirect ( $allowedHostnames , $allowedSchemes );
}, [ 'devKey' , 'allowedHostnames' , 'allowedSchemes' ]);
2025-03-15 11:00:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'user' , function ( string $mode , Document $project , Document $console , Request $request , Response $response , Database $dbForProject , Database $dbForPlatform , Store $store , Token $proofForToken , $authorization ) {
2025-11-03 09:32:56 +00:00
/**
* Handles user authentication and session validation .
*
* This function follows a series of steps to determine the appropriate user session
* based on cookies , headers , and JWT tokens .
*
* Process :
* 1. Checks the cookie based on mode :
* - If in admin mode , uses console project id for key .
* - Otherwise , sets the key using the project ID
* 2. If no cookie is found , attempts to retrieve the fallback header `x-fallback-cookies` .
* - If this method is used , returns the header : `X-Debug-Fallback: true` .
* 3. Fetches the user document from the appropriate database based on the mode .
* 4. If the user document is empty or the session key cannot be verified , sets an empty user document .
* 5. Regardless of the results from steps 1 - 4 , attempts to fetch the JWT token .
* 6. If the JWT user has a valid session ID , updates the user variable with the user from `projectDB` ,
* overwriting the previous value .
2026-01-15 15:16:09 +00:00
* 7. If account API key is passed , use user of the account API key as long as user ID header matches too
2025-11-03 09:32:56 +00:00
*/
2026-01-07 07:04:28 +00:00
$authorization -> setDefaultStatus ( true );
2025-03-15 11:00:36 +00:00
2025-11-03 09:32:56 +00:00
$store -> setKey ( 'a_session_' . $project -> getId ());
2025-03-15 11:00:36 +00:00
2026-03-11 14:01:26 +00:00
if ( $mode === APP_MODE_ADMIN ) {
2025-11-03 09:32:56 +00:00
$store -> setKey ( 'a_session_' . $console -> getId ());
2025-03-15 11:00:36 +00:00
}
2025-11-03 09:32:56 +00:00
$store -> decode (
2025-03-15 11:00:36 +00:00
$request -> getCookie (
2025-11-03 09:32:56 +00:00
$store -> getKey (), // Get sessions
$request -> getCookie ( $store -> getKey () . '_legacy' , '' )
2025-03-15 11:00:36 +00:00
)
);
// Get session from header for SSR clients
2025-11-03 09:32:56 +00:00
if ( empty ( $store -> getProperty ( 'id' , '' )) && empty ( $store -> getProperty ( 'secret' , '' ))) {
2025-03-15 11:00:36 +00:00
$sessionHeader = $request -> getHeader ( 'x-appwrite-session' , '' );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $sessionHeader )) {
2025-11-03 09:32:56 +00:00
$store -> decode ( $sessionHeader );
2025-03-15 11:00:36 +00:00
}
}
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
2025-11-03 09:32:56 +00:00
if ( $response ) { // if in http context - add debug header
2025-03-15 11:00:36 +00:00
$response -> addHeader ( 'X-Debug-Fallback' , 'false' );
}
2025-11-03 09:32:56 +00:00
if ( empty ( $store -> getProperty ( 'id' , '' )) && empty ( $store -> getProperty ( 'secret' , '' ))) {
2025-03-15 11:00:36 +00:00
if ( $response ) {
$response -> addHeader ( 'X-Debug-Fallback' , 'true' );
}
$fallback = $request -> getHeader ( 'x-fallback-cookies' , '' );
$fallback = \json_decode ( $fallback , true );
2025-11-03 09:32:56 +00:00
$store -> decode ((( is_array ( $fallback ) && isset ( $fallback [ $store -> getKey ()])) ? $fallback [ $store -> getKey ()] : '' ));
2025-03-15 11:00:36 +00:00
}
2025-11-05 01:41:15 +00:00
$user = null ;
2026-03-11 14:01:26 +00:00
if ( $mode === APP_MODE_ADMIN ) {
2026-03-29 03:04:34 +00:00
/** @var User $user */
$user = $dbForPlatform -> getDocument ( 'users' , $store -> getProperty ( 'id' , '' ));
2025-11-03 09:32:56 +00:00
} else {
if ( $project -> isEmpty ()) {
2025-11-04 06:08:35 +00:00
$user = new User ([]);
2025-11-03 09:32:56 +00:00
} else {
2026-03-11 14:01:26 +00:00
if ( ! empty ( $store -> getProperty ( 'id' , '' ))) {
2025-11-03 09:32:56 +00:00
if ( $project -> getId () === 'console' ) {
2026-03-29 03:04:34 +00:00
/** @var User $user */
$user = $dbForPlatform -> getDocument ( 'users' , $store -> getProperty ( 'id' , '' ));
2025-11-03 09:32:56 +00:00
} else {
2026-03-29 03:04:34 +00:00
/** @var User $user */
$user = $dbForProject -> getDocument ( 'users' , $store -> getProperty ( 'id' , '' ));
2025-11-03 09:32:56 +00:00
}
2025-03-15 11:00:36 +00:00
}
}
}
if (
2026-03-11 14:01:26 +00:00
! $user ||
2025-03-15 11:00:36 +00:00
$user -> isEmpty () // Check a document has been found in the DB
2026-03-11 14:01:26 +00:00
|| ! $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken )
2025-03-15 11:00:36 +00:00
) { // Validate user has valid login token
2025-11-05 01:41:15 +00:00
$user = new User ([]);
2025-03-15 11:00:36 +00:00
}
2026-02-25 10:50:44 +00:00
2025-03-15 11:00:36 +00:00
$authJWT = $request -> getHeader ( 'x-appwrite-jwt' , '' );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $authJWT ) && ! $project -> isEmpty ()) { // JWT authentication
if ( ! $user -> isEmpty ()) {
2026-01-15 15:16:09 +00:00
throw new Exception ( Exception :: USER_JWT_AND_COOKIE_SET );
}
2025-03-15 11:00:36 +00:00
$jwt = new JWT ( System :: getEnv ( '_APP_OPENSSL_KEY_V1' ), 'HS256' , 3600 , 0 );
try {
$payload = $jwt -> decode ( $authJWT );
} catch ( JWTException $error ) {
throw new Exception ( Exception :: USER_JWT_INVALID , 'Failed to verify JWT. ' . $error -> getMessage ());
}
2026-01-15 15:16:09 +00:00
2025-03-15 11:00:36 +00:00
$jwtUserId = $payload [ 'userId' ] ? ? '' ;
2026-03-11 14:01:26 +00:00
if ( ! empty ( $jwtUserId )) {
2025-07-03 16:06:36 +00:00
if ( $mode === APP_MODE_ADMIN ) {
2026-03-29 03:04:34 +00:00
/** @var User $user */
$user = $dbForPlatform -> getDocument ( 'users' , $jwtUserId );
2025-07-03 16:06:36 +00:00
} else {
2026-03-29 03:04:34 +00:00
/** @var User $user */
$user = $dbForProject -> getDocument ( 'users' , $jwtUserId );
2025-07-03 15:54:51 +00:00
}
2025-03-15 11:00:36 +00:00
}
$jwtSessionId = $payload [ 'sessionId' ] ? ? '' ;
2026-03-11 14:01:26 +00:00
if ( ! empty ( $jwtSessionId )) {
2025-03-15 11:00:36 +00:00
if ( empty ( $user -> find ( '$id' , $jwtSessionId , 'sessions' ))) { // Match JWT to active token
2025-11-05 01:41:15 +00:00
$user = new User ([]);
2025-03-15 11:00:36 +00:00
}
}
}
2025-12-23 10:59:38 +00:00
// Account based on account API key
$accountKey = $request -> getHeader ( 'x-appwrite-key' , '' );
2025-12-23 11:09:07 +00:00
$accountKeyUserId = $request -> getHeader ( 'x-appwrite-user' , '' );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $accountKeyUserId ) && ! empty ( $accountKey )) {
if ( ! $user -> isEmpty ()) {
2026-01-15 15:16:09 +00:00
throw new Exception ( Exception :: USER_API_KEY_AND_SESSION_SET );
}
2026-03-29 03:04:34 +00:00
/** @var User $accountKeyUser */
$accountKeyUser = $dbForPlatform -> getAuthorization () -> skip ( fn () => $dbForPlatform -> getDocument ( 'users' , $accountKeyUserId ));
if ( ! $accountKeyUser -> isEmpty ()) {
2025-12-23 10:59:38 +00:00
$key = $accountKeyUser -> find (
key : 'secret' ,
find : $accountKey ,
subject : 'keys'
);
2026-03-11 14:01:26 +00:00
if ( ! empty ( $key )) {
2025-12-29 09:47:27 +00:00
$expire = $key -> getAttribute ( 'expire' );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $expire ) && $expire < DatabaseDateTime :: formatTz ( DatabaseDateTime :: now ())) {
2026-01-10 16:01:31 +00:00
throw new Exception ( Exception :: ACCOUNT_KEY_EXPIRED );
2025-12-29 09:47:27 +00:00
}
2025-12-29 07:59:07 +00:00
2026-01-10 16:01:31 +00:00
$user = $accountKeyUser ;
2025-12-23 10:59:38 +00:00
}
}
}
2026-03-12 18:08:25 +00:00
// Impersonation: if current user has impersonator capability and headers are set, act as another user
$impersonateUserId = $request -> getHeader ( 'x-appwrite-impersonate-user-id' , '' );
$impersonateEmail = $request -> getHeader ( 'x-appwrite-impersonate-user-email' , '' );
$impersonatePhone = $request -> getHeader ( 'x-appwrite-impersonate-user-phone' , '' );
if ( ! $user -> isEmpty () && $user -> getAttribute ( 'impersonator' , false )) {
$userDb = ( APP_MODE_ADMIN === $mode || $project -> getId () === 'console' ) ? $dbForPlatform : $dbForProject ;
$targetUser = null ;
if ( ! empty ( $impersonateUserId )) {
$targetUser = $userDb -> getAuthorization () -> skip ( fn () => $userDb -> getDocument ( 'users' , $impersonateUserId ));
} elseif ( ! empty ( $impersonateEmail )) {
$targetUser = $userDb -> getAuthorization () -> skip ( fn () => $userDb -> findOne ( 'users' , [ Query :: equal ( 'email' , [ \strtolower ( $impersonateEmail )])]));
} elseif ( ! empty ( $impersonatePhone )) {
$targetUser = $userDb -> getAuthorization () -> skip ( fn () => $userDb -> findOne ( 'users' , [ Query :: equal ( 'phone' , [ $impersonatePhone ])]));
}
if ( $targetUser !== null && ! $targetUser -> isEmpty ()) {
2026-03-13 08:18:39 +00:00
$impersonator = clone $user ;
2026-03-12 18:08:25 +00:00
$user = clone $targetUser ;
2026-03-13 08:18:39 +00:00
$user -> setAttribute ( 'impersonatorUserId' , $impersonator -> getId ());
$user -> setAttribute ( 'impersonatorUserInternalId' , $impersonator -> getSequence ());
$user -> setAttribute ( 'impersonatorUserName' , $impersonator -> getAttribute ( 'name' , '' ));
$user -> setAttribute ( 'impersonatorUserEmail' , $impersonator -> getAttribute ( 'email' , '' ));
$user -> setAttribute ( 'impersonatorAccessedAt' , $impersonator -> getAttribute ( 'accessedAt' , 0 ));
2026-03-12 18:08:25 +00:00
}
}
2025-03-15 11:00:36 +00:00
$dbForProject -> setMetadata ( 'user' , $user -> getId ());
2025-03-15 12:18:34 +00:00
$dbForPlatform -> setMetadata ( 'user' , $user -> getId ());
2025-03-15 11:00:36 +00:00
return $user ;
2026-01-07 07:04:28 +00:00
}, [ 'mode' , 'project' , 'console' , 'request' , 'response' , 'dbForProject' , 'dbForPlatform' , 'store' , 'proofForToken' , 'authorization' ]);
2025-03-15 11:00:36 +00:00
2026-03-19 10:27:13 +00:00
Http :: setResource ( 'project' , function ( $dbForPlatform , $request , $console , $authorization , Http $utopia ) {
2025-03-15 11:00:36 +00:00
/** @var Appwrite\Utopia\Request $request */
2025-03-15 12:18:34 +00:00
/** @var Utopia\Database\Database $dbForPlatform */
2025-03-15 11:00:36 +00:00
/** @var Utopia\Database\Document $console */
$projectId = $request -> getParam ( 'project' , $request -> getHeader ( 'x-appwrite-project' , '' ));
2026-02-09 06:18:04 +00:00
// Realtime channel "project" can send project=Query array
2026-03-11 14:01:26 +00:00
if ( ! \is_string ( $projectId )) {
2026-02-09 06:18:04 +00:00
$projectId = $request -> getHeader ( 'x-appwrite-project' , '' );
}
2025-03-15 11:00:36 +00:00
2026-03-19 10:27:13 +00:00
// Backwards compatibility for new services, originally project resources
// These endpoints moved from /v1/projects/:projectId/<resource> to /v1/<resource>
// When accessed via the old alias path, extract projectId from the URI
2026-03-19 10:47:02 +00:00
$deprecatedProjectPathPrefix = '/v1/projects/' ;
2026-03-19 10:27:13 +00:00
$route = $utopia -> match ( $request );
if ( ! empty ( $route )) {
2026-03-19 10:47:02 +00:00
$isDeprecatedAlias = \str_starts_with ( $request -> getURI (), $deprecatedProjectPathPrefix ) &&
! \str_starts_with ( $route -> getPath (), $deprecatedProjectPathPrefix );
2026-03-19 10:27:13 +00:00
2026-03-19 10:47:02 +00:00
if ( $isDeprecatedAlias ) {
$projectId = \explode ( '/' , $request -> getURI (), 5 )[ 3 ] ? ? '' ;
}
2026-03-19 10:27:13 +00:00
}
2025-03-15 11:00:36 +00:00
if ( empty ( $projectId ) || $projectId === 'console' ) {
return $console ;
}
2026-01-07 07:04:28 +00:00
$project = $authorization -> skip ( fn () => $dbForPlatform -> getDocument ( 'projects' , $projectId ));
2025-03-15 11:00:36 +00:00
return $project ;
2026-03-19 10:27:13 +00:00
}, [ 'dbForPlatform' , 'request' , 'console' , 'authorization' , 'utopia' ]);
2025-03-15 11:00:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'session' , function ( User $user , Store $store , Token $proofForToken ) {
2025-03-15 11:00:36 +00:00
if ( $user -> isEmpty ()) {
return ;
}
$sessions = $user -> getAttribute ( 'sessions' , []);
2025-11-04 06:08:35 +00:00
$sessionId = $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken );
2025-03-15 11:00:36 +00:00
2026-03-11 14:01:26 +00:00
if ( ! $sessionId ) {
2025-03-15 11:00:36 +00:00
return ;
}
2025-11-05 01:41:15 +00:00
foreach ( $sessions as $session ) {
/** @var Document $session */
2025-03-15 11:00:36 +00:00
if ( $sessionId === $session -> getId ()) {
return $session ;
}
}
2025-11-03 09:39:52 +00:00
}, [ 'user' , 'store' , 'proofForToken' ]);
2025-03-15 11:00:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'store' , function () : Store {
2025-11-03 09:32:56 +00:00
return new Store ();
});
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'proofForPassword' , function () : Password {
2025-11-03 09:32:56 +00:00
$hash = new Argon2 ();
$hash
-> setMemoryCost ( 7168 )
-> setTimeCost ( 5 )
-> setThreads ( 1 );
$password = new Password ();
$password
-> setHash ( $hash );
return $password ;
});
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'proofForToken' , function () : Token {
2025-11-03 09:32:56 +00:00
$token = new Token ();
$token -> setHash ( new Sha ());
2026-03-11 14:01:26 +00:00
2025-11-03 09:32:56 +00:00
return $token ;
});
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'proofForCode' , function () : Code {
2025-11-03 09:32:56 +00:00
$code = new Code ();
$code -> setHash ( new Sha ());
2026-03-11 14:01:26 +00:00
2025-11-03 09:32:56 +00:00
return $code ;
});
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'console' , function () {
2026-01-07 07:04:28 +00:00
return new Document ( Config :: getParam ( 'console' ));
}, []);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'authorization' , function () {
2026-01-07 07:04:28 +00:00
return new Authorization ();
}, []);
2026-03-19 15:00:42 +00:00
Http :: setResource ( 'dbForProject' , function ( Group $pools , Database $dbForPlatform , Cache $cache , Document $project , Response $response , Publisher $publisher , Publisher $publisherFunctions , Publisher $publisherWebhooks , Event $queueForEvents , Func $queueForFunctions , Webhook $queueForWebhooks , Realtime $queueForRealtime , UsageContext $usage , Authorization $authorization , Request $request ) {
2025-03-15 11:00:36 +00:00
if ( $project -> isEmpty () || $project -> getId () === 'console' ) {
2025-03-15 12:18:34 +00:00
return $dbForPlatform ;
2025-03-15 11:00:36 +00:00
}
2026-02-20 10:24:42 +00:00
$database = $project -> getAttribute ( 'database' , '' );
if ( empty ( $database )) {
2026-02-19 19:28:29 +00:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , 'Project database is not configured' );
}
2025-09-26 12:31:39 +00:00
try {
2026-02-19 19:28:29 +00:00
$dsn = new DSN ( $database );
2025-09-26 12:31:39 +00:00
} catch ( \InvalidArgumentException ) {
// TODO: Temporary until all projects are using shared tables
2026-02-19 19:28:29 +00:00
$dsn = new DSN ( 'mysql://' . $database );
2025-09-26 12:31:39 +00:00
}
2025-09-30 13:45:43 +00:00
$adapter = new DatabasePool ( $pools -> get ( $dsn -> getHost ()));
2025-09-19 14:14:46 +00:00
$database = new Database ( $adapter , $cache );
2025-09-30 13:45:43 +00:00
2025-09-19 14:14:46 +00:00
$database
2026-01-15 06:08:25 +00:00
-> setDatabase ( APP_DATABASE )
2026-01-07 07:04:28 +00:00
-> setAuthorization ( $authorization )
2025-09-19 14:14:46 +00:00
-> setMetadata ( 'host' , \gethostname ())
-> setMetadata ( 'project' , $project -> getId ())
-> setTimeout ( APP_DATABASE_TIMEOUT_MILLISECONDS_API )
2025-09-26 12:31:39 +00:00
-> setMaxQueryValues ( APP_DATABASE_QUERY_MAX_VALUES );
2025-11-04 03:48:57 +00:00
$database -> setDocumentType ( 'users' , User :: class );
2025-09-26 12:31:39 +00:00
2025-09-30 13:45:43 +00:00
$sharedTables = \explode ( ',' , System :: getEnv ( '_APP_DATABASE_SHARED_TABLES' , '' ));
2025-09-26 12:31:39 +00:00
if ( \in_array ( $dsn -> getHost (), $sharedTables )) {
$database
-> setSharedTables ( true )
2026-03-13 12:16:14 +00:00
-> setTenant ( $project -> getSequence ())
2025-09-26 12:31:39 +00:00
-> setNamespace ( $dsn -> getParam ( 'namespace' ));
} else {
$database
-> setSharedTables ( false )
-> setTenant ( null )
-> setNamespace ( '_' . $project -> getSequence ());
}
2025-09-19 14:14:46 +00:00
2026-01-07 14:57:57 +00:00
/**
* This isolated event handling for `users.*.create` which is based on a `Database::EVENT_DOCUMENT_CREATE` listener may look odd , but it is ** intentional **.
*
* Accounts can be created in many ways beyond `createAccount`
* ( anonymous , OAuth , phone , etc . ), and those flows are probably not covered in event tests ; so we handle this here .
*/
$eventDatabaseListener = function ( Document $project , Document $document , Response $response , Event $queueForEvents , Func $queueForFunctions , Webhook $queueForWebhooks , Realtime $queueForRealtime ) {
// Only trigger events for user creation with the database listener.
if ( $document -> getCollection () !== 'users' ) {
return ;
}
$queueForEvents
-> setEvent ( 'users.[userId].create' )
-> setParam ( 'userId' , $document -> getId ())
-> setPayload ( $response -> output ( $document , Response :: MODEL_USER ));
// Trigger functions, webhooks, and realtime events
$queueForFunctions
-> from ( $queueForEvents )
-> trigger ();
/** Trigger webhooks events only if a project has them enabled */
2026-03-11 14:01:26 +00:00
if ( ! empty ( $project -> getAttribute ( 'webhooks' ))) {
2026-01-07 14:57:57 +00:00
$queueForWebhooks
-> from ( $queueForEvents )
-> trigger ();
}
/** Trigger realtime events only for non console events */
if ( $queueForEvents -> getProject () -> getId () !== 'console' ) {
$queueForRealtime
-> from ( $queueForEvents )
-> trigger ();
}
};
/**
* Purge function events cache when functions are created , updated or deleted .
*/
$functionsEventsCacheListener = function ( string $event , Document $document , Document $project , Database $dbForProject ) {
if ( $document -> getCollection () !== 'functions' ) {
return ;
}
if ( $project -> isEmpty () || $project -> getId () === 'console' ) {
return ;
}
$hostname = $dbForProject -> getAdapter () -> getHostname ();
$cacheKey = \sprintf (
'%s-cache-%s:%s:%s:project:%s:functions:events' ,
$dbForProject -> getCacheName (),
$hostname ? ? '' ,
$dbForProject -> getNamespace (),
$dbForProject -> getTenant (),
$project -> getId ()
);
$dbForProject -> getCache () -> purge ( $cacheKey );
};
2026-03-19 15:00:42 +00:00
/**
* Prefix metrics with database type when applicable .
* Avoids prefixing for legacy and tablesdb types to preserve historical metrics .
*/
$getDatabaseTypePrefixedMetric = function ( string $databaseType , string $metric ) : string {
if (
$databaseType === '' ||
$databaseType === DATABASE_TYPE_LEGACY ||
$databaseType === DATABASE_TYPE_TABLESDB
) {
return $metric ;
}
return $databaseType . '.' . $metric ;
};
// Determine database type from request path, similar to api.php
$path = $request -> getURI ();
$databaseType = match ( true ) {
str_contains ( $path , '/documentsdb' ) => DATABASE_TYPE_DOCUMENTSDB ,
str_contains ( $path , '/vectorsdb' ) => DATABASE_TYPE_VECTORSDB ,
default => '' ,
};
$usageDatabaseListener = function ( string $event , Document $document , UsageContext $usage ) use ( $getDatabaseTypePrefixedMetric , $databaseType ) {
2026-01-07 14:57:57 +00:00
$value = 1 ;
switch ( $event ) {
case Database :: EVENT_DOCUMENT_DELETE :
$value = - 1 ;
break ;
case Database :: EVENT_DOCUMENTS_DELETE :
$value = - 1 * $document -> getAttribute ( 'modified' , 0 );
break ;
case Database :: EVENT_DOCUMENTS_CREATE :
$value = $document -> getAttribute ( 'modified' , 0 );
break ;
case Database :: EVENT_DOCUMENTS_UPSERT :
$value = $document -> getAttribute ( 'created' , 0 );
break ;
}
switch ( true ) {
case $document -> getCollection () === 'teams' :
2026-03-11 14:01:26 +00:00
$usage -> addMetric ( METRIC_TEAMS , $value ); // per project
2026-01-07 14:57:57 +00:00
break ;
case $document -> getCollection () === 'users' :
2026-03-11 14:01:26 +00:00
$usage -> addMetric ( METRIC_USERS , $value ); // per project
2026-01-07 14:57:57 +00:00
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
2026-03-11 14:01:26 +00:00
$usage -> addReduce ( $document );
2026-01-07 14:57:57 +00:00
}
break ;
case $document -> getCollection () === 'sessions' : // sessions
2026-03-11 14:01:26 +00:00
$usage -> addMetric ( METRIC_SESSIONS , $value ); // per project
2026-01-07 14:57:57 +00:00
break ;
case $document -> getCollection () === 'databases' : // databases
2026-03-19 15:00:42 +00:00
$metric = $getDatabaseTypePrefixedMetric ( $databaseType , METRIC_DATABASES );
$usage -> addMetric ( $metric , $value ); // per project
2026-01-07 14:57:57 +00:00
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
2026-03-11 14:01:26 +00:00
$usage -> addReduce ( $document );
2026-01-07 14:57:57 +00:00
}
break ;
2026-03-11 14:01:26 +00:00
case str_starts_with ( $document -> getCollection (), 'database_' ) && ! str_contains ( $document -> getCollection (), 'collection' ) : // collections
2026-01-07 14:57:57 +00:00
$parts = explode ( '_' , $document -> getCollection ());
$databaseInternalId = $parts [ 1 ] ? ? 0 ;
2026-03-19 15:00:42 +00:00
$collectionMetric = $getDatabaseTypePrefixedMetric ( $databaseType , METRIC_COLLECTIONS );
$databaseIdCollectionMetric = $getDatabaseTypePrefixedMetric ( $databaseType , METRIC_DATABASE_ID_COLLECTIONS );
2026-03-11 14:01:26 +00:00
$usage
2026-03-19 15:00:42 +00:00
-> addMetric ( $collectionMetric , $value ) // per project
-> addMetric ( str_replace ( '{databaseInternalId}' , $databaseInternalId , $databaseIdCollectionMetric ), $value );
2026-01-07 14:57:57 +00:00
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
2026-03-11 14:01:26 +00:00
$usage -> addReduce ( $document );
2026-01-07 14:57:57 +00:00
}
break ;
2026-03-11 14:01:26 +00:00
case str_starts_with ( $document -> getCollection (), 'database_' ) && str_contains ( $document -> getCollection (), '_collection_' ) : // documents
2026-01-07 14:57:57 +00:00
$parts = explode ( '_' , $document -> getCollection ());
2026-03-11 14:01:26 +00:00
$databaseInternalId = $parts [ 1 ] ? ? 0 ;
2026-01-07 14:57:57 +00:00
$collectionInternalId = $parts [ 3 ] ? ? 0 ;
2026-03-19 15:00:42 +00:00
$documentsMetric = $getDatabaseTypePrefixedMetric ( $databaseType , METRIC_DOCUMENTS );
$databaseIdDocumentsMetric = $getDatabaseTypePrefixedMetric ( $databaseType , METRIC_DATABASE_ID_DOCUMENTS );
$databaseIdCollectionIdDocumentsMetric = $getDatabaseTypePrefixedMetric ( $databaseType , METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS );
2026-03-11 14:01:26 +00:00
$usage
2026-03-19 15:00:42 +00:00
-> addMetric ( $documentsMetric , $value ) // per project
-> addMetric ( str_replace ( '{databaseInternalId}' , $databaseInternalId , $databaseIdDocumentsMetric ), $value ) // per database
-> addMetric ( str_replace ([ '{databaseInternalId}' , '{collectionInternalId}' ], [ $databaseInternalId , $collectionInternalId ], $databaseIdCollectionIdDocumentsMetric ), $value ); // per collection
2026-01-07 14:57:57 +00:00
break ;
2026-03-11 14:01:26 +00:00
case $document -> getCollection () === 'buckets' : // buckets
$usage -> addMetric ( METRIC_BUCKETS , $value ); // per project
2026-01-07 14:57:57 +00:00
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
2026-03-11 14:01:26 +00:00
$usage
2026-01-07 14:57:57 +00:00
-> addReduce ( $document );
}
break ;
case str_starts_with ( $document -> getCollection (), 'bucket_' ) : // files
$parts = explode ( '_' , $document -> getCollection ());
2026-03-11 14:01:26 +00:00
$bucketInternalId = $parts [ 1 ];
$usage
2026-01-07 14:57:57 +00:00
-> addMetric ( METRIC_FILES , $value ) // per project
-> addMetric ( METRIC_FILES_STORAGE , $document -> getAttribute ( 'sizeOriginal' ) * $value ) // per project
-> addMetric ( str_replace ( '{bucketInternalId}' , $bucketInternalId , METRIC_BUCKET_ID_FILES ), $value ) // per bucket
-> addMetric ( str_replace ( '{bucketInternalId}' , $bucketInternalId , METRIC_BUCKET_ID_FILES_STORAGE ), $document -> getAttribute ( 'sizeOriginal' ) * $value ); // per bucket
break ;
case $document -> getCollection () === 'functions' :
2026-03-11 14:01:26 +00:00
$usage -> addMetric ( METRIC_FUNCTIONS , $value ); // per project
2026-01-07 14:57:57 +00:00
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
2026-03-11 14:01:26 +00:00
$usage
2026-01-07 14:57:57 +00:00
-> addReduce ( $document );
}
break ;
case $document -> getCollection () === 'sites' :
2026-03-11 14:01:26 +00:00
$usage -> addMetric ( METRIC_SITES , $value ); // per project
2026-01-07 14:57:57 +00:00
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
2026-03-11 14:01:26 +00:00
$usage
2026-01-07 14:57:57 +00:00
-> addReduce ( $document );
}
break ;
case $document -> getCollection () === 'deployments' :
2026-03-11 14:01:26 +00:00
$usage
2026-01-07 14:57:57 +00:00
-> addMetric ( METRIC_DEPLOYMENTS , $value ) // per project
-> addMetric ( METRIC_DEPLOYMENTS_STORAGE , $document -> getAttribute ( 'size' ) * $value ) // per project
-> addMetric ( str_replace ([ '{resourceType}' ], [ $document -> getAttribute ( 'resourceType' )], METRIC_RESOURCE_TYPE_DEPLOYMENTS ), $value ) // per function
-> addMetric ( str_replace ([ '{resourceType}' ], [ $document -> getAttribute ( 'resourceType' )], METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE ), $document -> getAttribute ( 'size' ) * $value )
-> addMetric ( str_replace ([ '{resourceType}' , '{resourceInternalId}' ], [ $document -> getAttribute ( 'resourceType' ), $document -> getAttribute ( 'resourceInternalId' )], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS ), $value ) // per function
-> addMetric ( str_replace ([ '{resourceType}' , '{resourceInternalId}' ], [ $document -> getAttribute ( 'resourceType' ), $document -> getAttribute ( 'resourceInternalId' )], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE ), $document -> getAttribute ( 'size' ) * $value );
break ;
default :
break ;
}
};
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event ( $publisher );
$queueForFunctions = new Func ( $publisherFunctions );
$queueForWebhooks = new Webhook ( $publisherWebhooks );
$queueForRealtime = new Realtime ();
$database
2026-03-11 14:01:26 +00:00
-> on ( Database :: EVENT_DOCUMENT_CREATE , 'calculate-usage' , fn ( $event , $document ) => $usageDatabaseListener ( $event , $document , $usage ))
-> on ( Database :: EVENT_DOCUMENT_DELETE , 'calculate-usage' , fn ( $event , $document ) => $usageDatabaseListener ( $event , $document , $usage ))
-> on ( Database :: EVENT_DOCUMENTS_CREATE , 'calculate-usage' , fn ( $event , $document ) => $usageDatabaseListener ( $event , $document , $usage ))
-> on ( Database :: EVENT_DOCUMENTS_DELETE , 'calculate-usage' , fn ( $event , $document ) => $usageDatabaseListener ( $event , $document , $usage ))
-> on ( Database :: EVENT_DOCUMENTS_UPSERT , 'calculate-usage' , fn ( $event , $document ) => $usageDatabaseListener ( $event , $document , $usage ))
-> on ( Database :: EVENT_DOCUMENT_CREATE , 'create-trigger-events' , fn ( $event , $document ) => $eventDatabaseListener (
$project ,
$document ,
$response ,
$queueForEventsClone -> from ( $queueForEvents ),
$queueForFunctions -> from ( $queueForEvents ),
$queueForWebhooks -> from ( $queueForEvents ),
$queueForRealtime -> from ( $queueForEvents )
))
-> on ( Database :: EVENT_DOCUMENT_CREATE , 'purge-function-events-cache' , fn ( $event , $document ) => $functionsEventsCacheListener ( $event , $document , $project , $database ))
-> on ( Database :: EVENT_DOCUMENT_UPDATE , 'purge-function-events-cache' , fn ( $event , $document ) => $functionsEventsCacheListener ( $event , $document , $project , $database ))
-> on ( Database :: EVENT_DOCUMENT_DELETE , 'purge-function-events-cache' , fn ( $event , $document ) => $functionsEventsCacheListener ( $event , $document , $project , $database ));
2026-01-07 14:57:57 +00:00
2025-09-19 14:14:46 +00:00
return $database ;
2026-03-19 15:00:42 +00:00
}, [ 'pools' , 'dbForPlatform' , 'cache' , 'project' , 'response' , 'publisher' , 'publisherFunctions' , 'publisherWebhooks' , 'queueForEvents' , 'queueForFunctions' , 'queueForWebhooks' , 'queueForRealtime' , 'usage' , 'authorization' , 'request' ]);
2026-01-07 07:04:28 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'dbForPlatform' , function ( Group $pools , Cache $cache , Authorization $authorization ) {
2025-09-19 14:14:46 +00:00
2025-09-30 13:45:43 +00:00
$adapter = new DatabasePool ( $pools -> get ( 'console' ));
$database = new Database ( $adapter , $cache );
$database
2026-01-15 05:43:28 +00:00
-> setDatabase ( APP_DATABASE )
2026-01-07 07:04:28 +00:00
-> setAuthorization ( $authorization )
2025-09-30 13:45:43 +00:00
-> setNamespace ( '_console' )
-> setMetadata ( 'host' , \gethostname ())
-> setMetadata ( 'project' , 'console' )
-> setTimeout ( APP_DATABASE_TIMEOUT_MILLISECONDS_API )
-> setMaxQueryValues ( APP_DATABASE_QUERY_MAX_VALUES );
2025-11-04 03:48:57 +00:00
$database -> setDocumentType ( 'users' , User :: class );
2025-09-30 13:45:43 +00:00
return $database ;
2026-01-07 07:04:28 +00:00
}, [ 'pools' , 'cache' , 'authorization' ]);
2025-09-30 13:45:43 +00:00
2026-03-19 15:00:42 +00:00
Http :: setResource ( 'getDatabasesDB' , function ( Group $pools , Cache $cache , Document $project , Request $request , UsageContext $usage , Authorization $authorization ) {
return function ( Document $database ) use ( $pools , $cache , $project , $request , $usage , $authorization ) : Database {
$databaseDSN = $database -> getAttribute ( 'database' , $project -> getAttribute ( 'database' , '' ));
$databaseType = $database -> getAttribute ( 'type' , '' );
try {
$databaseDSN = new DSN ( $databaseDSN );
} catch ( \InvalidArgumentException ) {
// for old databases migrated through patch script
// databaseDSN determines the adapter
$databaseDSN = new DSN ( 'mysql://' . $databaseDSN );
}
try {
$dsn = new DSN ( $project -> getAttribute ( 'database' ));
} catch ( \InvalidArgumentException ) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN ( 'mysql://' . $project -> getAttribute ( 'database' ));
}
$pool = $pools -> get ( $databaseDSN -> getHost ());
$adapter = new DatabasePool ( $pool );
$database = new Database ( $adapter , $cache );
$sharedTables = \explode ( ',' , System :: getEnv ( '_APP_DATABASE_SHARED_TABLES' , '' ));
$database
-> setDatabase ( APP_DATABASE )
-> setAuthorization ( $authorization )
-> setMetadata ( 'host' , \gethostname ())
-> setMetadata ( 'project' , $project -> getId ())
-> setTimeout ( APP_DATABASE_TIMEOUT_MILLISECONDS_API )
-> setMaxQueryValues ( APP_DATABASE_QUERY_MAX_VALUES );
// inside pools authorization needs to be set first
$database -> getAdapter () -> setSupportForAttributes ( $databaseType !== DOCUMENTSDB );
if ( \in_array ( $dsn -> getHost (), $sharedTables )) {
$database
-> setSharedTables ( true )
-> setTenant (( int ) $project -> getSequence ())
-> setNamespace ( $dsn -> getParam ( 'namespace' ));
} else {
$database
-> setSharedTables ( false )
-> setTenant ( null )
-> setNamespace ( '_' . $project -> getSequence ());
}
$timeout = \intval ( $request -> getHeader ( 'x-appwrite-timeout' ));
if ( ! empty ( $timeout ) && Http :: isDevelopment ()) {
$database -> setTimeout ( $timeout );
}
// Register database event listeners for usage stats collection
$documentsMetric = METRIC_DOCUMENTS ;
$databaseIdDocumentsMetric = METRIC_DATABASE_ID_DOCUMENTS ;
$databaseIdCollectionIdDocumentsMetric = METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS ;
if ( $databaseType !== DATABASE_TYPE_LEGACY && $databaseType !== DATABASE_TYPE_TABLESDB ) {
$documentsMetric = $databaseType . '.' . $documentsMetric ;
$databaseIdDocumentsMetric = $databaseType . '.' . $databaseIdDocumentsMetric ;
$databaseIdCollectionIdDocumentsMetric = $databaseType . '.' . $databaseIdCollectionIdDocumentsMetric ;
}
$database
-> on ( Database :: EVENT_DOCUMENT_CREATE , 'calculate-usage' , function ( $event , $document ) use ( $usage , $documentsMetric , $databaseIdDocumentsMetric , $databaseIdCollectionIdDocumentsMetric ) {
$value = 1 ;
if ( str_starts_with ( $document -> getCollection (), 'database_' ) && str_contains ( $document -> getCollection (), '_collection_' )) {
$parts = explode ( '_' , $document -> getCollection ());
$databaseInternalId = $parts [ 1 ] ? ? 0 ;
$collectionInternalId = $parts [ 3 ] ? ? 0 ;
$usage
-> addMetric ( $documentsMetric , $value ) // per project
-> addMetric ( str_replace ( '{databaseInternalId}' , $databaseInternalId , $databaseIdDocumentsMetric ), $value ) // per database
-> addMetric ( str_replace ([ '{databaseInternalId}' , '{collectionInternalId}' ], [ $databaseInternalId , $collectionInternalId ], $databaseIdCollectionIdDocumentsMetric ), $value ); // per collection
}
})
-> on ( Database :: EVENT_DOCUMENT_DELETE , 'calculate-usage' , function ( $event , $document ) use ( $usage , $documentsMetric , $databaseIdDocumentsMetric , $databaseIdCollectionIdDocumentsMetric ) {
$value = - 1 ;
if ( str_starts_with ( $document -> getCollection (), 'database_' ) && str_contains ( $document -> getCollection (), '_collection_' )) {
$parts = explode ( '_' , $document -> getCollection ());
$databaseInternalId = $parts [ 1 ] ? ? 0 ;
$collectionInternalId = $parts [ 3 ] ? ? 0 ;
$usage
-> addMetric ( $documentsMetric , $value ) // per project
-> addMetric ( str_replace ( '{databaseInternalId}' , $databaseInternalId , $databaseIdDocumentsMetric ), $value ) // per database
-> addMetric ( str_replace ([ '{databaseInternalId}' , '{collectionInternalId}' ], [ $databaseInternalId , $collectionInternalId ], $databaseIdCollectionIdDocumentsMetric ), $value ); // per collection
}
})
-> on ( Database :: EVENT_DOCUMENTS_CREATE , 'calculate-usage' , function ( $event , $document ) use ( $usage , $documentsMetric , $databaseIdDocumentsMetric , $databaseIdCollectionIdDocumentsMetric ) {
$value = $document -> getAttribute ( 'modified' , 0 );
if ( str_starts_with ( $document -> getCollection (), 'database_' ) && str_contains ( $document -> getCollection (), '_collection_' )) {
$parts = explode ( '_' , $document -> getCollection ());
$databaseInternalId = $parts [ 1 ] ? ? 0 ;
$collectionInternalId = $parts [ 3 ] ? ? 0 ;
$usage
-> addMetric ( $documentsMetric , $value ) // per project
-> addMetric ( str_replace ( '{databaseInternalId}' , $databaseInternalId , $databaseIdDocumentsMetric ), $value ) // per database
-> addMetric ( str_replace ([ '{databaseInternalId}' , '{collectionInternalId}' ], [ $databaseInternalId , $collectionInternalId ], $databaseIdCollectionIdDocumentsMetric ), $value ); // per collection
}
})
-> on ( Database :: EVENT_DOCUMENTS_DELETE , 'calculate-usage' , function ( $event , $document ) use ( $usage , $documentsMetric , $databaseIdDocumentsMetric , $databaseIdCollectionIdDocumentsMetric ) {
$value = - 1 * $document -> getAttribute ( 'modified' , 0 );
if ( str_starts_with ( $document -> getCollection (), 'database_' ) && str_contains ( $document -> getCollection (), '_collection_' )) {
$parts = explode ( '_' , $document -> getCollection ());
$databaseInternalId = $parts [ 1 ] ? ? 0 ;
$collectionInternalId = $parts [ 3 ] ? ? 0 ;
$usage
-> addMetric ( $documentsMetric , $value ) // per project
-> addMetric ( str_replace ( '{databaseInternalId}' , $databaseInternalId , $databaseIdDocumentsMetric ), $value ) // per database
-> addMetric ( str_replace ([ '{databaseInternalId}' , '{collectionInternalId}' ], [ $databaseInternalId , $collectionInternalId ], $databaseIdCollectionIdDocumentsMetric ), $value ); // per collection
}
})
-> on ( Database :: EVENT_DOCUMENTS_UPSERT , 'calculate-usage' , function ( $event , $document ) use ( $usage , $documentsMetric , $databaseIdDocumentsMetric , $databaseIdCollectionIdDocumentsMetric ) {
$value = $document -> getAttribute ( 'created' , 0 );
if ( str_starts_with ( $document -> getCollection (), 'database_' ) && str_contains ( $document -> getCollection (), '_collection_' )) {
$parts = explode ( '_' , $document -> getCollection ());
$databaseInternalId = $parts [ 1 ] ? ? 0 ;
$collectionInternalId = $parts [ 3 ] ? ? 0 ;
$usage
-> addMetric ( $documentsMetric , $value ) // per project
-> addMetric ( str_replace ( '{databaseInternalId}' , $databaseInternalId , $databaseIdDocumentsMetric ), $value ) // per database
-> addMetric ( str_replace ([ '{databaseInternalId}' , '{collectionInternalId}' ], [ $databaseInternalId , $collectionInternalId ], $databaseIdCollectionIdDocumentsMetric ), $value ); // per collection
}
});
return $database ;
};
}, [ 'pools' , 'cache' , 'project' , 'request' , 'usage' , 'authorization' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'getProjectDB' , function ( Group $pools , Database $dbForPlatform , $cache , Authorization $authorization ) {
2025-05-14 06:14:07 +00:00
$databases = [];
2025-03-15 11:00:36 +00:00
2026-01-07 07:04:28 +00:00
return function ( Document $project ) use ( $pools , $dbForPlatform , $cache , $authorization , & $databases ) {
2025-03-15 11:00:36 +00:00
if ( $project -> isEmpty () || $project -> getId () === 'console' ) {
2025-03-15 12:18:34 +00:00
return $dbForPlatform ;
2025-03-15 11:00:36 +00:00
}
2026-02-20 10:41:09 +00:00
$database = $project -> getAttribute ( 'database' , '' );
if ( empty ( $database )) {
2026-02-19 19:28:29 +00:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , 'Project database is not configured' );
}
2025-03-15 11:00:36 +00:00
try {
2026-02-19 19:28:29 +00:00
$dsn = new DSN ( $database );
2025-03-15 11:00:36 +00:00
} catch ( \InvalidArgumentException ) {
// TODO: Temporary until all projects are using shared tables
2026-02-19 19:28:29 +00:00
$dsn = new DSN ( 'mysql://' . $database );
2025-03-15 11:00:36 +00:00
}
2026-01-07 07:04:28 +00:00
$configure = ( function ( Database $database ) use ( $project , $dsn , $authorization ) {
2025-03-15 11:00:36 +00:00
$database
2026-01-15 06:08:25 +00:00
-> setDatabase ( APP_DATABASE )
2026-01-07 07:04:28 +00:00
-> setAuthorization ( $authorization )
2025-03-15 11:00:36 +00:00
-> setMetadata ( 'host' , \gethostname ())
-> setMetadata ( 'project' , $project -> getId ())
2025-03-15 12:18:34 +00:00
-> setTimeout ( APP_DATABASE_TIMEOUT_MILLISECONDS_API )
2026-01-07 07:04:28 +00:00
-> setMaxQueryValues ( APP_DATABASE_QUERY_MAX_VALUES )
2026-03-11 14:01:26 +00:00
-> setDocumentType ( 'users' , User :: class );
2025-03-15 12:18:34 +00:00
$sharedTables = \explode ( ',' , System :: getEnv ( '_APP_DATABASE_SHARED_TABLES' , '' ));
2025-03-15 11:00:36 +00:00
2025-03-15 12:18:34 +00:00
if ( \in_array ( $dsn -> getHost (), $sharedTables )) {
2025-03-15 11:00:36 +00:00
$database
-> setSharedTables ( true )
2026-03-13 12:16:14 +00:00
-> setTenant ( $project -> getSequence ())
2025-03-15 11:00:36 +00:00
-> setNamespace ( $dsn -> getParam ( 'namespace' ));
} else {
$database
-> setSharedTables ( false )
-> setTenant ( null )
2025-05-26 05:42:11 +00:00
-> setNamespace ( '_' . $project -> getSequence ());
2025-03-15 11:00:36 +00:00
}
});
if ( isset ( $databases [ $dsn -> getHost ()])) {
$database = $databases [ $dsn -> getHost ()];
$configure ( $database );
2026-03-11 14:01:26 +00:00
2025-03-15 11:00:36 +00:00
return $database ;
}
2025-05-14 06:14:07 +00:00
$adapter = new DatabasePool ( $pools -> get ( $dsn -> getHost ()));
$database = new Database ( $adapter , $cache );
2025-03-15 11:00:36 +00:00
$databases [ $dsn -> getHost ()] = $database ;
$configure ( $database );
return $database ;
};
2026-01-07 07:04:28 +00:00
}, [ 'pools' , 'dbForPlatform' , 'cache' , 'authorization' ]);
2025-03-15 12:18:34 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'getLogsDB' , function ( Group $pools , Cache $cache , Authorization $authorization ) {
2025-03-15 12:18:34 +00:00
$database = null ;
2025-05-14 06:14:07 +00:00
2026-01-07 07:04:28 +00:00
return function ( ? Document $project = null ) use ( $pools , $cache , $authorization , & $database ) {
2025-03-15 12:18:34 +00:00
if ( $database !== null && $project !== null && ! $project -> isEmpty () && $project -> getId () !== 'console' ) {
2026-03-13 12:16:14 +00:00
$database -> setTenant ( $project -> getSequence ());
2025-03-15 12:18:34 +00:00
return $database ;
}
2025-05-14 06:14:07 +00:00
$adapter = new DatabasePool ( $pools -> get ( 'logs' ));
$database = new Database ( $adapter , $cache );
2025-03-15 12:18:34 +00:00
$database
2026-01-15 05:43:28 +00:00
-> setDatabase ( APP_DATABASE )
2026-01-07 07:04:28 +00:00
-> setAuthorization ( $authorization )
2025-03-15 12:18:34 +00:00
-> setSharedTables ( true )
-> setNamespace ( 'logsV1' )
-> setTimeout ( APP_DATABASE_TIMEOUT_MILLISECONDS_API )
-> setMaxQueryValues ( APP_DATABASE_QUERY_MAX_VALUES );
// set tenant
if ( $project !== null && ! $project -> isEmpty () && $project -> getId () !== 'console' ) {
2026-03-13 12:16:14 +00:00
$database -> setTenant ( $project -> getSequence ());
2025-03-15 12:18:34 +00:00
}
return $database ;
};
2026-01-07 07:04:28 +00:00
}, [ 'pools' , 'cache' , 'authorization' ]);
2025-03-15 11:00:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'audit' , function ( $dbForProject ) {
2025-12-14 01:43:35 +00:00
$adapter = new AdapterDatabase ( $dbForProject );
2026-03-11 14:01:26 +00:00
2025-12-14 01:43:35 +00:00
return new Audit ( $adapter );
}, [ 'dbForProject' ]);
2025-03-15 11:00:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'telemetry' , fn () => new NoTelemetry ());
2025-04-08 11:12:28 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'cache' , function ( Group $pools , Telemetry $telemetry ) {
2025-03-15 11:00:36 +00:00
$list = Config :: getParam ( 'pools-cache' , []);
$adapters = [];
foreach ( $list as $value ) {
2025-05-14 06:14:07 +00:00
$adapters [] = new CachePool ( $pools -> get ( $value ));
2025-03-15 11:00:36 +00:00
}
2025-04-08 11:12:28 +00:00
$cache = new Cache ( new Sharding ( $adapters ));
$cache -> setTelemetry ( $telemetry );
2026-03-11 14:01:26 +00:00
2025-04-08 11:12:28 +00:00
return $cache ;
}, [ 'pools' , 'telemetry' ]);
2025-03-15 11:00:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'redis' , function () {
2025-03-15 12:18:34 +00:00
$host = System :: getEnv ( '_APP_REDIS_HOST' , 'localhost' );
$port = System :: getEnv ( '_APP_REDIS_PORT' , 6379 );
$pass = System :: getEnv ( '_APP_REDIS_PASS' , '' );
$redis = new \Redis ();
2025-09-18 07:08:18 +00:00
@ $redis -> pconnect ( $host , ( int ) $port );
2025-03-15 12:18:34 +00:00
if ( $pass ) {
$redis -> auth ( $pass );
}
$redis -> setOption ( \Redis :: OPT_READ_TIMEOUT , - 1 );
return $redis ;
});
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'timelimit' , function ( \Redis $redis ) {
2025-03-15 12:18:34 +00:00
return function ( string $key , int $limit , int $time ) use ( $redis ) {
return new TimeLimitRedis ( $key , $limit , $time , $redis );
};
}, [ 'redis' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'deviceForLocal' , function ( Telemetry $telemetry ) {
2025-05-26 15:42:54 +00:00
return new Device\Telemetry ( $telemetry , new Local ());
}, [ 'telemetry' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'deviceForFiles' , function ( $project , Telemetry $telemetry ) {
2025-05-26 15:42:54 +00:00
return new Device\Telemetry ( $telemetry , getDevice ( APP_STORAGE_UPLOADS . '/app-' . $project -> getId ()));
}, [ 'project' , 'telemetry' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'deviceForSites' , function ( $project , Telemetry $telemetry ) {
2025-05-26 15:42:54 +00:00
return new Device\Telemetry ( $telemetry , getDevice ( APP_STORAGE_SITES . '/app-' . $project -> getId ()));
}, [ 'project' , 'telemetry' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'deviceForMigrations' , function ( $project , Telemetry $telemetry ) {
2025-05-26 15:42:54 +00:00
return new Device\Telemetry ( $telemetry , getDevice ( APP_STORAGE_IMPORTS . '/app-' . $project -> getId ()));
}, [ 'project' , 'telemetry' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'deviceForFunctions' , function ( $project , Telemetry $telemetry ) {
2025-05-26 15:42:54 +00:00
return new Device\Telemetry ( $telemetry , getDevice ( APP_STORAGE_FUNCTIONS . '/app-' . $project -> getId ()));
}, [ 'project' , 'telemetry' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'deviceForBuilds' , function ( $project , Telemetry $telemetry ) {
2025-05-26 15:42:54 +00:00
return new Device\Telemetry ( $telemetry , getDevice ( APP_STORAGE_BUILDS . '/app-' . $project -> getId ()));
}, [ 'project' , 'telemetry' ]);
2025-03-15 11:00:36 +00:00
2025-03-15 12:18:34 +00:00
function getDevice ( string $root , string $connection = '' ) : Device
2025-03-15 11:00:36 +00:00
{
2026-03-11 14:01:26 +00:00
$connection = ! empty ( $connection ) ? $connection : System :: getEnv ( '_APP_CONNECTIONS_STORAGE' , '' );
2025-03-15 11:00:36 +00:00
2026-03-11 14:01:26 +00:00
if ( ! empty ( $connection )) {
2025-03-15 11:00:36 +00:00
$acl = 'private' ;
$device = Storage :: DEVICE_LOCAL ;
$accessKey = '' ;
$accessSecret = '' ;
$bucket = '' ;
$region = '' ;
2025-03-16 09:36:32 +00:00
$url = System :: getEnv ( '_APP_STORAGE_S3_ENDPOINT' , '' );
2025-03-15 11:00:36 +00:00
try {
$dsn = new DSN ( $connection );
$device = $dsn -> getScheme ();
$accessKey = $dsn -> getUser () ? ? '' ;
$accessSecret = $dsn -> getPassword () ? ? '' ;
$bucket = $dsn -> getPath () ? ? '' ;
$region = $dsn -> getParam ( 'region' );
} catch ( \Throwable $e ) {
Console :: warning ( $e -> getMessage () . 'Invalid DSN. Defaulting to Local device.' );
}
switch ( $device ) {
case Storage :: DEVICE_S3 :
2026-03-11 14:01:26 +00:00
if ( ! empty ( $url )) {
$bucketRoot = ( ! empty ( $bucket ) ? $bucket . '/' : '' ) . \ltrim ( $root , '/' );
2025-05-22 12:49:26 +00:00
return new S3 ( $bucketRoot , $accessKey , $accessSecret , $url , $region , $acl );
2025-02-28 12:52:34 +00:00
} else {
return new AWS ( $root , $accessKey , $accessSecret , $bucket , $region , $acl );
}
// no break
2025-03-15 11:00:36 +00:00
case STORAGE :: DEVICE_DO_SPACES :
$device = new DOSpaces ( $root , $accessKey , $accessSecret , $bucket , $region , $acl );
$device -> setHttpVersion ( S3 :: HTTP_VERSION_1_1 );
2026-03-11 14:01:26 +00:00
2025-03-15 11:00:36 +00:00
return $device ;
case Storage :: DEVICE_BACKBLAZE :
return new Backblaze ( $root , $accessKey , $accessSecret , $bucket , $region , $acl );
case Storage :: DEVICE_LINODE :
return new Linode ( $root , $accessKey , $accessSecret , $bucket , $region , $acl );
case Storage :: DEVICE_WASABI :
return new Wasabi ( $root , $accessKey , $accessSecret , $bucket , $region , $acl );
case Storage :: DEVICE_LOCAL :
default :
return new Local ( $root );
}
} else {
switch ( strtolower ( System :: getEnv ( '_APP_STORAGE_DEVICE' , Storage :: DEVICE_LOCAL ) ? ? '' )) {
case Storage :: DEVICE_LOCAL :
default :
return new Local ( $root );
case Storage :: DEVICE_S3 :
$s3AccessKey = System :: getEnv ( '_APP_STORAGE_S3_ACCESS_KEY' , '' );
$s3SecretKey = System :: getEnv ( '_APP_STORAGE_S3_SECRET' , '' );
$s3Region = System :: getEnv ( '_APP_STORAGE_S3_REGION' , '' );
$s3Bucket = System :: getEnv ( '_APP_STORAGE_S3_BUCKET' , '' );
$s3Acl = 'private' ;
2025-03-16 09:36:32 +00:00
$s3EndpointUrl = System :: getEnv ( '_APP_STORAGE_S3_ENDPOINT' , '' );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $s3EndpointUrl )) {
$bucketRoot = ( ! empty ( $s3Bucket ) ? $s3Bucket . '/' : '' ) . \ltrim ( $root , '/' );
2025-05-22 12:49:26 +00:00
return new S3 ( $bucketRoot , $s3AccessKey , $s3SecretKey , $s3EndpointUrl , $s3Region , $s3Acl );
2025-02-28 12:52:34 +00:00
} else {
return new AWS ( $root , $s3AccessKey , $s3SecretKey , $s3Bucket , $s3Region , $s3Acl );
}
// no break
2025-03-15 11:00:36 +00:00
case Storage :: DEVICE_DO_SPACES :
$doSpacesAccessKey = System :: getEnv ( '_APP_STORAGE_DO_SPACES_ACCESS_KEY' , '' );
$doSpacesSecretKey = System :: getEnv ( '_APP_STORAGE_DO_SPACES_SECRET' , '' );
$doSpacesRegion = System :: getEnv ( '_APP_STORAGE_DO_SPACES_REGION' , '' );
$doSpacesBucket = System :: getEnv ( '_APP_STORAGE_DO_SPACES_BUCKET' , '' );
$doSpacesAcl = 'private' ;
$device = new DOSpaces ( $root , $doSpacesAccessKey , $doSpacesSecretKey , $doSpacesBucket , $doSpacesRegion , $doSpacesAcl );
$device -> setHttpVersion ( S3 :: HTTP_VERSION_1_1 );
2026-03-11 14:01:26 +00:00
2025-03-15 11:00:36 +00:00
return $device ;
case Storage :: DEVICE_BACKBLAZE :
$backblazeAccessKey = System :: getEnv ( '_APP_STORAGE_BACKBLAZE_ACCESS_KEY' , '' );
$backblazeSecretKey = System :: getEnv ( '_APP_STORAGE_BACKBLAZE_SECRET' , '' );
$backblazeRegion = System :: getEnv ( '_APP_STORAGE_BACKBLAZE_REGION' , '' );
$backblazeBucket = System :: getEnv ( '_APP_STORAGE_BACKBLAZE_BUCKET' , '' );
$backblazeAcl = 'private' ;
2026-03-11 14:01:26 +00:00
2025-03-15 11:00:36 +00:00
return new Backblaze ( $root , $backblazeAccessKey , $backblazeSecretKey , $backblazeBucket , $backblazeRegion , $backblazeAcl );
case Storage :: DEVICE_LINODE :
$linodeAccessKey = System :: getEnv ( '_APP_STORAGE_LINODE_ACCESS_KEY' , '' );
$linodeSecretKey = System :: getEnv ( '_APP_STORAGE_LINODE_SECRET' , '' );
$linodeRegion = System :: getEnv ( '_APP_STORAGE_LINODE_REGION' , '' );
$linodeBucket = System :: getEnv ( '_APP_STORAGE_LINODE_BUCKET' , '' );
$linodeAcl = 'private' ;
2026-03-11 14:01:26 +00:00
2025-03-15 11:00:36 +00:00
return new Linode ( $root , $linodeAccessKey , $linodeSecretKey , $linodeBucket , $linodeRegion , $linodeAcl );
case Storage :: DEVICE_WASABI :
$wasabiAccessKey = System :: getEnv ( '_APP_STORAGE_WASABI_ACCESS_KEY' , '' );
$wasabiSecretKey = System :: getEnv ( '_APP_STORAGE_WASABI_SECRET' , '' );
$wasabiRegion = System :: getEnv ( '_APP_STORAGE_WASABI_REGION' , '' );
$wasabiBucket = System :: getEnv ( '_APP_STORAGE_WASABI_BUCKET' , '' );
$wasabiAcl = 'private' ;
2026-03-11 14:01:26 +00:00
2025-03-15 11:00:36 +00:00
return new Wasabi ( $root , $wasabiAccessKey , $wasabiSecretKey , $wasabiBucket , $wasabiRegion , $wasabiAcl );
}
}
}
2026-03-19 10:27:13 +00:00
Http :: setResource ( 'mode' , function ( Request $request , Document $project ) {
2025-03-15 11:00:36 +00:00
/**
* Defines the mode for the request :
* - 'default' => Requests for Client and Server Side
* - 'admin' => Request from the Console on non - console projects
*/
2026-03-19 10:27:13 +00:00
$mode = $request -> getParam ( 'mode' , $request -> getHeader ( 'x-appwrite-mode' , APP_MODE_DEFAULT ));
$projectId = $request -> getParam ( 'project' , $request -> getHeader ( 'x-appwrite-project' , '' ));
2026-03-19 10:47:02 +00:00
if ( ! empty ( $projectId ) && $project -> getId () !== $projectId ) {
2026-03-19 10:27:13 +00:00
$mode = APP_MODE_ADMIN ;
}
return $mode ;
}, [ 'request' , 'project' ]);
2025-03-15 11:00:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'geodb' , function ( $register ) {
2025-03-15 11:00:36 +00:00
/** @var Utopia\Registry\Registry $register */
return $register -> get ( 'geodb' );
}, [ 'register' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'passwordsDictionary' , function ( $register ) {
2025-03-15 11:00:36 +00:00
/** @var Utopia\Registry\Registry $register */
return $register -> get ( 'passwordsDictionary' );
}, [ 'register' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'servers' , function () {
2025-12-07 20:29:45 +00:00
$platforms = Config :: getParam ( 'sdks' );
2025-12-13 16:06:44 +00:00
$server = $platforms [ APP_SDK_PLATFORM_SERVER ];
2025-03-15 11:00:36 +00:00
$languages = array_map ( function ( $language ) {
return strtolower ( $language [ 'name' ]);
}, $server [ 'sdks' ]);
return $languages ;
});
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'promiseAdapter' , function ( $register ) {
2025-03-15 11:00:36 +00:00
return $register -> get ( 'promiseAdapter' );
}, [ 'register' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'schema' , function ( $utopia , $dbForProject , $authorization ) {
2025-03-15 11:00:36 +00:00
$complexity = function ( int $complexity , array $args ) {
$queries = Query :: parseQueries ( $args [ 'queries' ] ? ? []);
$query = Query :: getByType ( $queries , [ Query :: TYPE_LIMIT ])[ 0 ] ? ? null ;
$limit = $query ? $query -> getValue () : APP_LIMIT_LIST_DEFAULT ;
return $complexity * $limit ;
};
2026-01-07 07:04:28 +00:00
$attributes = function ( int $limit , int $offset ) use ( $dbForProject , $authorization ) {
$attrs = $authorization -> skip ( fn () => $dbForProject -> find ( 'attributes' , [
2025-03-15 11:00:36 +00:00
Query :: limit ( $limit ),
Query :: offset ( $offset ),
]));
return \array_map ( function ( $attr ) {
return $attr -> getArrayCopy ();
}, $attrs );
};
$urls = [
'list' => function ( string $databaseId , string $collectionId , array $args ) {
return " /v1/databases/ $databaseId /collections/ $collectionId /documents " ;
},
'create' => function ( string $databaseId , string $collectionId , array $args ) {
return " /v1/databases/ $databaseId /collections/ $collectionId /documents " ;
},
'read' => function ( string $databaseId , string $collectionId , array $args ) {
return " /v1/databases/ $databaseId /collections/ $collectionId /documents/ { $args [ 'documentId' ] } " ;
},
'update' => function ( string $databaseId , string $collectionId , array $args ) {
return " /v1/databases/ $databaseId /collections/ $collectionId /documents/ { $args [ 'documentId' ] } " ;
},
'delete' => function ( string $databaseId , string $collectionId , array $args ) {
return " /v1/databases/ $databaseId /collections/ $collectionId /documents/ { $args [ 'documentId' ] } " ;
},
];
2025-05-07 05:43:15 +00:00
// NOTE: `params` and `urls` are not used internally in the `Schema::build` function below!
2025-03-15 11:00:36 +00:00
$params = [
'list' => function ( string $databaseId , string $collectionId , array $args ) {
2025-09-18 07:08:18 +00:00
return [ 'queries' => $args [ 'queries' ]];
2025-03-15 11:00:36 +00:00
},
'create' => function ( string $databaseId , string $collectionId , array $args ) {
$id = $args [ 'id' ] ? ? 'unique()' ;
$permissions = $args [ 'permissions' ] ? ? null ;
unset ( $args [ 'id' ]);
unset ( $args [ 'permissions' ]);
// Order must be the same as the route params
return [
'databaseId' => $databaseId ,
2025-05-07 05:43:15 +00:00
'documentId' => $id ,
'collectionId' => $collectionId ,
2025-03-15 11:00:36 +00:00
'data' => $args ,
'permissions' => $permissions ,
];
},
'update' => function ( string $databaseId , string $collectionId , array $args ) {
$documentId = $args [ 'id' ];
$permissions = $args [ 'permissions' ] ? ? null ;
unset ( $args [ 'id' ]);
unset ( $args [ 'permissions' ]);
// Order must be the same as the route params
return [
'databaseId' => $databaseId ,
2025-05-07 05:43:15 +00:00
'collectionId' => $collectionId ,
'documentId' => $documentId ,
2025-03-15 11:00:36 +00:00
'data' => $args ,
'permissions' => $permissions ,
];
},
];
return Schema :: build (
$utopia ,
$complexity ,
$attributes ,
$urls ,
$params ,
);
2026-01-07 07:04:28 +00:00
}, [ 'utopia' , 'dbForProject' , 'authorization' ]);
2025-03-15 11:00:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'gitHub' , function ( Cache $cache ) {
2025-03-15 11:00:36 +00:00
return new VcsGitHub ( $cache );
}, [ 'cache' ]);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'requestTimestamp' , function ( $request ) {
2026-03-11 14:01:26 +00:00
// TODO: Move this to the Request class itself
2025-03-15 11:00:36 +00:00
$timestampHeader = $request -> getHeader ( 'x-appwrite-timestamp' );
$requestTimestamp = null ;
2026-03-11 14:01:26 +00:00
if ( ! empty ( $timestampHeader )) {
2025-03-15 11:00:36 +00:00
try {
$requestTimestamp = new \DateTime ( $timestampHeader );
} catch ( \Throwable $e ) {
throw new Exception ( Exception :: GENERAL_ARGUMENT_INVALID , 'Invalid X-Appwrite-Timestamp header value' );
}
}
2026-03-11 14:01:26 +00:00
2025-03-15 11:00:36 +00:00
return $requestTimestamp ;
}, [ 'request' ]);
2025-03-15 12:18:34 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'plan' , function ( array $plan = []) {
2025-03-15 11:00:36 +00:00
return [];
});
2025-03-15 12:18:34 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'smsRates' , function () {
2025-03-15 12:18:34 +00:00
return [];
});
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'devKey' , function ( Request $request , Document $project , array $servers , Database $dbForPlatform , Authorization $authorization ) {
2025-03-22 13:05:10 +00:00
$devKey = $request -> getHeader ( 'x-appwrite-dev-key' , $request -> getParam ( 'devKey' , '' ));
2025-04-14 03:55:04 +00:00
2025-03-22 13:05:10 +00:00
// Check if given key match project's development keys
$key = $project -> find ( 'secret' , $devKey , 'devKeys' );
2026-03-11 14:01:26 +00:00
if ( ! $key ) {
2025-04-14 03:55:04 +00:00
return new Document ([]);
}
// check expiration
$expire = $key -> getAttribute ( 'expire' );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $expire ) && $expire < DatabaseDateTime :: formatTz ( DatabaseDateTime :: now ())) {
2025-04-14 03:55:04 +00:00
return new Document ([]);
}
// update access time
2025-04-17 14:13:09 +00:00
$accessedAt = $key -> getAttribute ( 'accessedAt' , 0 );
2025-04-15 12:02:55 +00:00
if ( empty ( $accessedAt ) || DatabaseDateTime :: formatTz ( DatabaseDateTime :: addSeconds ( new \DateTime (), - APP_KEY_ACCESS )) > $accessedAt ) {
2025-04-14 03:55:04 +00:00
$key -> setAttribute ( 'accessedAt' , DatabaseDateTime :: now ());
2026-03-06 09:42:07 +00:00
$authorization -> skip ( fn () => $dbForPlatform -> updateDocument ( 'devKeys' , $key -> getId (), new Document ([
'accessedAt' => $key -> getAttribute ( 'accessedAt' )
])));
2025-04-14 03:55:04 +00:00
$dbForPlatform -> purgeCachedDocument ( 'projects' , $project -> getId ());
}
// add sdk to key
$sdkValidator = new WhiteList ( $servers , true );
2025-05-18 13:56:08 +00:00
$sdk = \strtolower ( $request -> getHeader ( 'x-sdk-name' , 'UNKNOWN' ));
2025-04-14 03:55:04 +00:00
2025-04-15 12:08:18 +00:00
if ( $sdk !== 'UNKNOWN' && $sdkValidator -> isValid ( $sdk )) {
2025-04-14 03:55:04 +00:00
$sdks = $key -> getAttribute ( 'sdks' , []);
2026-03-11 14:01:26 +00:00
if ( ! in_array ( $sdk , $sdks )) {
2025-04-14 03:55:04 +00:00
$sdks [] = $sdk ;
$key -> setAttribute ( 'sdks' , $sdks );
2025-03-22 13:05:10 +00:00
2025-04-14 03:55:04 +00:00
/** Update access time as well */
2025-03-22 13:05:10 +00:00
$key -> setAttribute ( 'accessedAt' , DatabaseDateTime :: now ());
2026-03-06 09:42:07 +00:00
$key = $authorization -> skip ( fn () => $dbForPlatform -> updateDocument ( 'devKeys' , $key -> getId (), new Document ([
'sdks' => $key -> getAttribute ( 'sdks' ),
'accessedAt' => $key -> getAttribute ( 'accessedAt' )
])));
2025-03-22 13:05:10 +00:00
$dbForPlatform -> purgeCachedDocument ( 'projects' , $project -> getId ());
}
}
2025-12-07 20:29:45 +00:00
2025-04-14 03:55:04 +00:00
return $key ;
2026-01-07 07:04:28 +00:00
}, [ 'request' , 'project' , 'servers' , 'dbForPlatform' , 'authorization' ]);
2025-03-22 13:05:10 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'team' , function ( Document $project , Database $dbForPlatform , Http $utopia , Request $request , Authorization $authorization ) {
2025-03-15 12:18:34 +00:00
$teamInternalId = '' ;
if ( $project -> getId () !== 'console' ) {
$teamInternalId = $project -> getAttribute ( 'teamInternalId' , '' );
} else {
$route = $utopia -> match ( $request );
2026-03-11 14:01:26 +00:00
$path = ! empty ( $route ) ? $route -> getPath () : $request -> getURI ();
2026-02-16 15:14:43 +00:00
$orgHeader = $request -> getHeader ( 'x-appwrite-organization' , '' );
2025-03-15 12:18:34 +00:00
if ( str_starts_with ( $path , '/v1/projects/:projectId' )) {
$uri = $request -> getURI ();
$pid = explode ( '/' , $uri )[ 3 ];
2026-01-07 07:04:28 +00:00
$p = $authorization -> skip ( fn () => $dbForPlatform -> getDocument ( 'projects' , $pid ));
2025-03-15 12:18:34 +00:00
$teamInternalId = $p -> getAttribute ( 'teamInternalId' , '' );
} elseif ( $path === '/v1/projects' ) {
$teamId = $request -> getParam ( 'teamId' , '' );
2025-07-29 08:10:25 +00:00
2025-07-29 08:24:41 +00:00
if ( empty ( $teamId )) {
2025-07-29 08:10:25 +00:00
return new Document ([]);
}
2026-01-07 07:04:28 +00:00
$team = $authorization -> skip ( fn () => $dbForPlatform -> getDocument ( 'teams' , $teamId ));
2026-03-11 14:01:26 +00:00
2025-03-15 12:18:34 +00:00
return $team ;
2026-03-11 14:01:26 +00:00
} elseif ( ! empty ( $orgHeader )) {
2026-02-16 15:14:43 +00:00
return $authorization -> skip ( fn () => $dbForPlatform -> getDocument ( 'teams' , $orgHeader ));
2025-03-15 12:18:34 +00:00
}
}
2025-08-06 12:07:04 +00:00
// if teamInternalId is empty, return an empty document
2025-07-31 09:47:12 +00:00
2025-07-29 08:24:41 +00:00
if ( empty ( $teamInternalId )) {
2025-07-29 08:10:25 +00:00
return new Document ([]);
}
2026-01-07 07:04:28 +00:00
$team = $authorization -> skip ( function () use ( $dbForPlatform , $teamInternalId ) {
2025-03-15 12:18:34 +00:00
return $dbForPlatform -> findOne ( 'teams' , [
2025-05-26 05:42:11 +00:00
Query :: equal ( '$sequence' , [ $teamInternalId ]),
2025-03-15 12:18:34 +00:00
]);
});
return $team ;
2026-01-07 07:04:28 +00:00
}, [ 'project' , 'dbForPlatform' , 'utopia' , 'request' , 'authorization' ]);
2025-03-15 12:18:34 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource (
2025-03-15 12:18:34 +00:00
'isResourceBlocked' ,
fn () => fn ( Document $project , string $resourceType , ? string $resourceId ) => false
);
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'previewHostname' , function ( Request $request , ? Key $apiKey ) {
2025-03-17 10:53:09 +00:00
$allowed = false ;
2025-03-17 10:53:23 +00:00
2026-02-04 05:30:22 +00:00
if ( Http :: isDevelopment ()) {
2025-03-17 10:53:09 +00:00
$allowed = true ;
2026-03-11 14:01:26 +00:00
} elseif ( ! \is_null ( $apiKey ) && $apiKey -> getHostnameOverride () === true ) {
2025-03-17 10:53:09 +00:00
$allowed = true ;
}
2025-03-17 10:53:23 +00:00
2025-03-17 10:53:09 +00:00
if ( $allowed ) {
$host = $request -> getQuery ( 'appwrite-hostname' , $request -> getHeader ( 'x-appwrite-hostname' , '' )) ? ? '' ;
2026-03-11 14:01:26 +00:00
if ( ! empty ( $host )) {
2025-03-15 12:18:34 +00:00
return $host ;
}
}
return '' ;
2025-03-17 10:53:09 +00:00
}, [ 'request' , 'apiKey' ]);
2025-03-15 12:18:34 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'apiKey' , function ( Request $request , Document $project , Document $team , Document $user ) : ? Key {
2025-03-15 12:18:34 +00:00
$key = $request -> getHeader ( 'x-appwrite-key' );
if ( empty ( $key )) {
return null ;
}
2026-01-15 15:16:09 +00:00
$key = Key :: decode ( $project , $team , $user , $key );
$userHeader = $request -> getHeader ( 'x-appwrite-user' );
$organizationHeader = $request -> getHeader ( 'x-appwrite-organization' );
$projectHeader = $request -> getHeader ( 'x-appwrite-project' );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $key -> getProjectId ())) {
2026-01-15 15:16:09 +00:00
if ( empty ( $projectHeader ) || $projectHeader !== $key -> getProjectId ()) {
throw new Exception ( Exception :: PROJECT_ID_MISSING );
}
}
2026-03-11 14:01:26 +00:00
if ( ! empty ( $key -> getUserId ())) {
2026-01-15 15:16:09 +00:00
if ( empty ( $userHeader ) || $userHeader !== $key -> getUserId ()) {
throw new Exception ( Exception :: USER_ID_MISSING );
}
}
2026-03-11 14:01:26 +00:00
if ( ! empty ( $key -> getTeamId ())) {
2026-01-15 15:16:09 +00:00
if ( empty ( $organizationHeader ) || $organizationHeader !== $key -> getTeamId ()) {
throw new Exception ( Exception :: ORGANIZATION_ID_MISSING );
}
}
return $key ;
2025-12-23 12:06:19 +00:00
}, [ 'request' , 'project' , 'team' , 'user' ]);
2025-03-25 11:33:03 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'executor' , fn () => new Executor ());
2025-04-17 06:19:18 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'resourceToken' , function ( $project , $dbForProject , $request , Authorization $authorization ) {
2025-04-17 06:19:18 +00:00
$tokenJWT = $request -> getParam ( 'token' );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $tokenJWT ) && ! $project -> isEmpty ()) { // JWT authentication
2025-11-27 14:10:09 +00:00
// Use a large but reasonable maxAge to avoid auto-exp when token has no expiry
2025-11-27 14:32:39 +00:00
$jwt = new JWT ( System :: getEnv ( '_APP_OPENSSL_KEY_V1' ), RESOURCE_TOKEN_ALGORITHM , RESOURCE_TOKEN_MAX_AGE , RESOURCE_TOKEN_LEEWAY ); // Instantiate with key, algo, maxAge and leeway.
2025-04-17 06:19:18 +00:00
try {
$payload = $jwt -> decode ( $tokenJWT );
} catch ( JWTException $error ) {
return new Document ([]);
}
$tokenId = $payload [ 'tokenId' ] ? ? '' ;
2025-05-13 10:44:29 +00:00
if ( empty ( $tokenId )) {
2025-04-17 06:19:18 +00:00
return new Document ([]);
}
2026-01-07 07:04:28 +00:00
$token = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'resourceTokens' , $tokenId ));
2025-04-17 06:19:18 +00:00
2025-05-13 10:11:26 +00:00
if ( $token -> isEmpty ()) {
2025-04-17 06:19:18 +00:00
return new Document ([]);
}
2025-05-13 10:11:26 +00:00
$expiry = $token -> getAttribute ( 'expire' );
2025-04-17 06:19:18 +00:00
2025-05-13 10:11:26 +00:00
if ( $expiry !== null ) {
$now = new \DateTime ();
$expiryDate = new \DateTime ( $expiry );
2025-04-17 06:19:18 +00:00
2025-05-13 10:11:26 +00:00
if ( $expiryDate < $now ) {
2025-04-17 06:19:18 +00:00
return new Document ([]);
}
2025-05-13 10:11:26 +00:00
}
2025-04-17 06:19:18 +00:00
2025-05-13 10:11:26 +00:00
return match ( $token -> getAttribute ( 'resourceType' )) {
2026-01-07 07:04:28 +00:00
TOKENS_RESOURCE_TYPE_FILES => ( function () use ( $token , $dbForProject , $authorization ) {
2025-05-27 01:36:23 +00:00
$sequences = explode ( ':' , $token -> getAttribute ( 'resourceInternalId' ));
2025-05-13 10:11:26 +00:00
$ids = explode ( ':' , $token -> getAttribute ( 'resourceId' ));
2025-04-24 05:38:56 +00:00
2025-05-27 01:36:23 +00:00
if ( count ( $sequences ) !== 2 || count ( $ids ) !== 2 ) {
2025-05-13 10:11:26 +00:00
return new Document ([]);
}
$accessedAt = $token -> getAttribute ( 'accessedAt' , 0 );
2025-09-18 07:08:18 +00:00
if ( empty ( $accessedAt ) || DatabaseDateTime :: formatTz ( DatabaseDateTime :: addSeconds ( new \DateTime (), - APP_RESOURCE_TOKEN_ACCESS )) > $accessedAt ) {
2025-05-13 10:11:26 +00:00
$token -> setAttribute ( 'accessedAt' , DatabaseDateTime :: now ());
2026-03-06 09:42:07 +00:00
$authorization -> skip ( fn () => $dbForProject -> updateDocument ( 'resourceTokens' , $token -> getId (), new Document ([
'accessedAt' => $token -> getAttribute ( 'accessedAt' )
])));
2025-05-13 10:11:26 +00:00
}
return new Document ([
'bucketId' => $ids [ 0 ],
'fileId' => $ids [ 1 ],
2025-05-27 01:36:23 +00:00
'bucketInternalId' => $sequences [ 0 ],
'fileInternalId' => $sequences [ 1 ],
2025-05-13 10:11:26 +00:00
]);
})(),
2025-05-13 10:46:33 +00:00
default => throw new Exception ( Exception :: TOKEN_RESOURCE_TYPE_INVALID ),
2025-05-13 10:11:26 +00:00
};
2025-04-17 06:19:18 +00:00
}
2026-03-11 14:01:26 +00:00
2025-04-17 06:19:18 +00:00
return new Document ([]);
2026-01-07 07:04:28 +00:00
}, [ 'project' , 'dbForProject' , 'request' , 'authorization' ]);
2025-06-23 09:28:18 +00:00
2026-03-19 15:00:42 +00:00
Http :: setResource ( 'transactionState' , function ( Database $dbForProject , Authorization $authorization , callable $getDatabasesDB ) {
return new TransactionState ( $dbForProject , $authorization , $getDatabasesDB );
}, [ 'dbForProject' , 'authorization' , 'getDatabasesDB' ]);
2026-01-16 09:36:35 +00:00
2026-02-04 05:30:22 +00:00
Http :: setResource ( 'executionsRetentionCount' , function ( Document $project , array $plan ) {
2026-01-16 09:36:35 +00:00
if ( $project -> getId () === 'console' || empty ( $plan )) {
return 0 ;
}
return ( int ) ( $plan [ 'executionsRetentionCount' ] ? ? 100 );
}, [ 'project' , 'plan' ]);
2026-03-19 15:00:42 +00:00
Http :: setResource ( 'embeddingAgent' , function ( $register ) {
$adapter = new Ollama ();
$adapter -> setEndpoint ( System :: getEnv ( '_APP_EMBEDDING_ENDPOINT' , 'http://ollama:11434/api/embed' ));
$adapter -> setTimeout (( int ) System :: getEnv ( '_APP_EMBEDDING_TIMEOUT' , '30000' ));
return new Agent ( $adapter );
}, [ 'register' ]);