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-09-08 07:21:59 +00:00
use Appwrite\Databases\TransactionState ;
2025-03-15 11:00:36 +00:00
use Appwrite\Event\Audit ;
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 ;
2025-03-15 13:32:18 +00:00
use Appwrite\Event\Realtime ;
2025-09-01 14:03:49 +00:00
use Appwrite\Event\StatsResources ;
2025-03-15 13:32:18 +00:00
use Appwrite\Event\StatsUsage ;
use Appwrite\Event\Webhook ;
2025-03-15 11:00:36 +00:00
use Appwrite\Extend\Exception ;
use Appwrite\GraphQL\Schema ;
2025-04-14 11:56:42 +00:00
use Appwrite\Network\Platform ;
2025-06-30 13:59:42 +00:00
use Appwrite\Network\Validator\Origin ;
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 ;
2025-03-15 11:00:36 +00:00
use Utopia\App ;
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\CLI\Console ;
use Utopia\Config\Config ;
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\Helpers\ID ;
use Utopia\Database\Query ;
use Utopia\Database\Validator\Authorization ;
use Utopia\DSN\DSN ;
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 ;
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-04-08 11:24:05 +00:00
use Utopia\Validator\Hostname ;
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
2025-03-15 11:00:36 +00:00
App :: setResource ( 'log' , fn () => new Log ());
App :: setResource ( 'logger' , function ( $register ) {
return $register -> get ( 'logger' );
}, [ 'register' ]);
App :: setResource ( 'hooks' , function ( $register ) {
return $register -> get ( 'hooks' );
}, [ 'register' ]);
App :: setResource ( 'register' , fn () => $register );
2025-08-12 13:13:08 +00:00
App :: setResource ( 'locale' , function () {
$locale = new Locale ( System :: getEnv ( '_APP_LOCALE' , 'en' ));
$locale -> setFallback ( System :: getEnv ( '_APP_LOCALE' , 'en' ));
return $locale ;
});
2025-03-15 11:00:36 +00:00
App :: setResource ( 'localeCodes' , function () {
return array_map ( fn ( $locale ) => $locale [ 'code' ], Config :: getParam ( 'locale-codes' , []));
});
// Queues
2025-03-15 12:18:34 +00:00
App :: 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' ]);
2025-09-03 11:08:07 +00:00
App :: setResource ( 'publisherDatabases' , function ( Publisher $publisher ) {
2025-07-16 17:11:04 +00:00
return $publisher ;
}, [ 'publisher' ]);
2025-09-03 11:08:07 +00:00
App :: setResource ( 'publisherFunctions' , function ( Publisher $publisher ) {
2025-07-16 17:11:04 +00:00
return $publisher ;
}, [ 'publisher' ]);
2025-09-03 11:08:07 +00:00
App :: setResource ( 'publisherMigrations' , function ( Publisher $publisher ) {
return $publisher ;
}, [ 'publisher' ]);
App :: setResource ( 'publisherStatsUsage' , function ( Publisher $publisher ) {
return $publisher ;
}, [ 'publisher' ]);
App :: setResource ( 'publisherMails' , function ( Publisher $publisher ) {
return $publisher ;
}, [ 'publisher' ]);
App :: setResource ( 'publisherDeletes' , function ( Publisher $publisher ) {
return $publisher ;
}, [ 'publisher' ]);
App :: setResource ( 'publisherMessaging' , function ( Publisher $publisher ) {
return $publisher ;
}, [ 'publisher' ]);
App :: setResource ( 'publisherWebhooks' , function ( Publisher $publisher ) {
2025-07-16 17:11:04 +00:00
return $publisher ;
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForMessaging' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Messaging ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForMails' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Mail ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForBuilds' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Build ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForDatabase' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new EventDatabase ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForDeletes' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Delete ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForEvents' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Event ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForWebhooks' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Webhook ( $publisher );
}, [ 'publisher' ]);
App :: setResource ( 'queueForRealtime' , function () {
return new Realtime ();
}, []);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForStatsUsage' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new StatsUsage ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForAudits' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Audit ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForFunctions' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Func ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForCertificates' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Certificate ( $publisher );
}, [ 'publisher' ]);
2025-03-15 13:32:18 +00:00
App :: setResource ( 'queueForMigrations' , function ( Publisher $publisher ) {
2025-03-15 12:18:34 +00:00
return new Migration ( $publisher );
}, [ 'publisher' ]);
2025-09-03 11:08:07 +00:00
App :: setResource ( 'queueForStatsResources' , function ( Publisher $publisher ) {
return new StatsResources ( $publisher );
}, [ 'publisher' ]);
2025-09-16 08:46:02 +00:00
App :: setResource ( 'platforms' , function ( Request $request , Document $console , Document $project , Database $dbForPlatform ) {
2025-03-15 11:00:36 +00:00
$console -> setAttribute ( 'platforms' , [ // Always allow current host
'$collection' => ID :: custom ( 'platforms' ),
'name' => 'Current Host' ,
2025-04-14 11:56:42 +00:00
'type' => Platform :: TYPE_WEB ,
2025-03-15 11:00:36 +00:00
'hostname' => $request -> getHostname (),
], Document :: SET_TYPE_APPEND );
$hostnames = explode ( ',' , System :: getEnv ( '_APP_CONSOLE_HOSTNAMES' , '' ));
$validator = new Hostname ();
foreach ( $hostnames as $hostname ) {
$hostname = trim ( $hostname );
if ( ! $validator -> isValid ( $hostname )) {
continue ;
}
$console -> setAttribute ( 'platforms' , [
'$collection' => ID :: custom ( 'platforms' ),
2025-04-14 11:56:42 +00:00
'type' => Platform :: TYPE_WEB ,
2025-03-15 11:00:36 +00:00
'name' => $hostname ,
'hostname' => $hostname ,
], Document :: SET_TYPE_APPEND );
}
2025-04-14 11:56:42 +00:00
// Add `exp` and `appwrite-callback-{projectId}` schemes
if ( ! $project -> isEmpty () && $project -> getId () !== 'console' ) {
$project -> setAttribute ( 'platforms' , [
'$collection' => ID :: custom ( 'platforms' ),
'type' => Platform :: TYPE_SCHEME ,
'name' => 'Expo' ,
'key' => 'exp' ,
], Document :: SET_TYPE_APPEND );
$project -> setAttribute ( 'platforms' , [
'$collection' => ID :: custom ( 'platforms' ),
'type' => Platform :: TYPE_SCHEME ,
'name' => 'Appwrite Callback' ,
'key' => 'appwrite-callback-' . $project -> getId (),
], Document :: SET_TYPE_APPEND );
2025-03-15 11:00:36 +00:00
}
2025-09-16 08:46:02 +00:00
$origin = \parse_url ( $request -> getOrigin (), PHP_URL_HOST );
if ( empty ( $origin )) {
$origin = \parse_url ( $request -> getReferer (), PHP_URL_HOST );
}
// Safe if rule with same project ID exists
if ( ! empty ( $origin )) {
if ( System :: getEnv ( '_APP_RULES_FORMAT' ) === 'md5' ) {
$rule = Authorization :: skip ( fn () => $dbForPlatform -> getDocument ( 'rules' , md5 ( $origin ? ? '' )));
} else {
$rule = Authorization :: skip (
fn () => $dbForPlatform -> find ( 'rules' , [
Query :: equal ( 'domain' , [ $origin ]),
Query :: limit ( 1 )
])
)[ 0 ] ? ? new Document ();
}
if ( ! $rule -> isEmpty () && $rule -> getAttribute ( 'projectInternalId' ) === $project -> getSequence ()) {
$project -> setAttribute ( 'platforms' , [
'$collection' => ID :: custom ( 'platforms' ),
'type' => Platform :: TYPE_WEB ,
'name' => $origin ,
'hostname' => $origin ,
], Document :: SET_TYPE_APPEND );
}
}
2025-04-14 11:56:42 +00:00
return [
... $console -> getAttribute ( 'platforms' , []),
... $project -> getAttribute ( 'platforms' , []),
];
2025-09-16 08:46:02 +00:00
}, [ 'request' , 'console' , 'project' , 'dbForPlatform' ]);
2025-03-15 11:00:36 +00:00
2025-11-03 09:32:56 +00:00
App :: setResource ( 'user' , function ( string $mode , Document $project , Document $console , Request $request , Response $response , Database $dbForProject , Database $dbForPlatform , Store $store , Token $proofForToken ) {
/**
* 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 .
*/
2025-03-15 11:00:36 +00:00
Authorization :: setDefaultStatus ( true );
2025-11-03 09:32:56 +00:00
$store -> setKey ( 'a_session_' . $project -> getId ());
2025-03-15 11:00:36 +00:00
if ( APP_MODE_ADMIN === $mode ) {
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' , '' );
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 ;
2025-11-03 09:32:56 +00:00
if ( APP_MODE_ADMIN === $mode ) {
2025-11-04 06:08:35 +00:00
/** @var User $user */
2025-11-03 09:32:56 +00:00
$user = $dbForPlatform -> getDocument ( 'users' , $store -> getProperty ( 'id' , '' ));
} else {
if ( $project -> isEmpty ()) {
2025-11-04 06:08:35 +00:00
$user = new User ([]);
2025-11-03 09:32:56 +00:00
} else {
if ( ! empty ( $store -> getProperty ( 'id' , '' ))) {
if ( $project -> getId () === 'console' ) {
2025-11-04 06:08:35 +00:00
/** @var User $user */
2025-11-03 09:32:56 +00:00
$user = $dbForPlatform -> getDocument ( 'users' , $store -> getProperty ( 'id' , '' ));
} else {
2025-11-04 06:08:35 +00:00
/** @var User $user */
2025-11-03 09:32:56 +00:00
$user = $dbForProject -> getDocument ( 'users' , $store -> getProperty ( 'id' , '' ));
}
2025-03-15 11:00:36 +00:00
}
}
}
if (
2025-11-03 09:32:56 +00:00
! $user ||
2025-03-15 11:00:36 +00:00
$user -> isEmpty () // Check a document has been found in the DB
2025-11-04 06:08:35 +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
}
2025-03-15 12:18:34 +00:00
// if (APP_MODE_ADMIN === $mode) {
// if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) {
// Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
// } else {
// $user = new Document([]);
// }
// }
2025-03-15 11:00:36 +00:00
$authJWT = $request -> getHeader ( 'x-appwrite-jwt' , '' );
if ( ! empty ( $authJWT ) && ! $project -> isEmpty ()) { // JWT authentication
$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 ());
}
$jwtUserId = $payload [ 'userId' ] ? ? '' ;
if ( ! empty ( $jwtUserId )) {
2025-07-03 16:06:36 +00:00
if ( $mode === APP_MODE_ADMIN ) {
2025-07-03 15:54:51 +00:00
$user = $dbForPlatform -> getDocument ( 'users' , $jwtUserId );
2025-07-03 16:06:36 +00:00
} else {
$user = $dbForProject -> getDocument ( 'users' , $jwtUserId );
2025-07-03 15:54:51 +00:00
}
2025-03-15 11:00:36 +00:00
}
$jwtSessionId = $payload [ 'sessionId' ] ? ? '' ;
if ( ! empty ( $jwtSessionId )) {
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
}
}
}
$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 ;
2025-11-03 09:32:56 +00:00
}, [ 'mode' , 'project' , 'console' , 'request' , 'response' , 'dbForProject' , 'dbForPlatform' , 'store' , 'proofForToken' ]);
2025-03-15 11:00:36 +00:00
2025-03-15 12:18:34 +00:00
App :: setResource ( 'project' , function ( $dbForPlatform , $request , $console ) {
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' , '' ));
if ( empty ( $projectId ) || $projectId === 'console' ) {
return $console ;
}
2025-03-15 12:18:34 +00:00
$project = Authorization :: skip ( fn () => $dbForPlatform -> getDocument ( 'projects' , $projectId ));
2025-03-15 11:00:36 +00:00
return $project ;
2025-03-15 12:18:34 +00:00
}, [ 'dbForPlatform' , 'request' , 'console' ]);
2025-03-15 11:00:36 +00:00
2025-11-04 06:08:35 +00:00
App :: 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
if ( ! $sessionId ) {
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 ;
}
}
return ;
2025-11-03 09:39:52 +00:00
}, [ 'user' , 'store' , 'proofForToken' ]);
2025-03-15 11:00:36 +00:00
App :: setResource ( 'console' , function () {
2025-03-15 12:18:34 +00:00
return new Document ( Config :: getParam ( 'console' ));
2025-03-15 11:00:36 +00:00
}, []);
2025-11-03 09:32:56 +00:00
App :: setResource ( 'store' , function () : Store {
return new Store ();
});
App :: setResource ( 'proofForPassword' , function () : Password {
$hash = new Argon2 ();
$hash
-> setMemoryCost ( 7168 )
-> setTimeCost ( 5 )
-> setThreads ( 1 );
$password = new Password ();
$password
-> setHash ( $hash );
return $password ;
});
App :: setResource ( 'proofForToken' , function () : Token {
$token = new Token ();
$token -> setHash ( new Sha ());
return $token ;
});
App :: setResource ( 'proofForCode' , function () : Code {
$code = new Code ();
$code -> setHash ( new Sha ());
return $code ;
});
2025-03-15 12:18:34 +00:00
App :: setResource ( 'dbForProject' , function ( Group $pools , Database $dbForPlatform , Cache $cache , Document $project ) {
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
}
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' ));
}
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
$database
-> setMetadata ( 'host' , \gethostname ())
-> setMetadata ( 'project' , $project -> getId ())
2025-03-15 12:18:34 +00:00
-> 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-03-15 11:00:36 +00:00
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 )
2025-09-18 07:08:18 +00:00
-> setTenant (( int ) $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
}
return $database ;
2025-03-15 12:18:34 +00:00
}, [ 'pools' , 'dbForPlatform' , 'cache' , 'project' ]);
2025-03-15 11:00:36 +00:00
2025-03-15 12:18:34 +00:00
App :: setResource ( 'dbForPlatform' , function ( Group $pools , Cache $cache ) {
2025-05-14 06:14:07 +00:00
$adapter = new DatabasePool ( $pools -> get ( 'console' ));
$database = new Database ( $adapter , $cache );
2025-03-15 11:00:36 +00:00
$database
-> setNamespace ( '_console' )
-> setMetadata ( 'host' , \gethostname ())
-> setMetadata ( 'project' , 'console' )
2025-03-15 12:18:34 +00:00
-> setTimeout ( APP_DATABASE_TIMEOUT_MILLISECONDS_API )
-> setMaxQueryValues ( APP_DATABASE_QUERY_MAX_VALUES );
2025-03-15 11:00:36 +00:00
2025-11-04 03:48:57 +00:00
$database -> setDocumentType ( 'users' , User :: class );
2025-03-15 11:00:36 +00:00
return $database ;
}, [ 'pools' , 'cache' ]);
2025-03-15 12:18:34 +00:00
App :: setResource ( 'getProjectDB' , function ( Group $pools , Database $dbForPlatform , $cache ) {
2025-05-14 06:14:07 +00:00
$databases = [];
2025-03-15 11:00:36 +00:00
2025-03-15 12:18:34 +00:00
return function ( Document $project ) use ( $pools , $dbForPlatform , $cache , & $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
}
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' ));
}
$configure = ( function ( Database $database ) use ( $project , $dsn ) {
$database
-> setMetadata ( 'host' , \gethostname ())
-> setMetadata ( 'project' , $project -> getId ())
2025-03-15 12:18:34 +00:00
-> setTimeout ( APP_DATABASE_TIMEOUT_MILLISECONDS_API )
-> setMaxQueryValues ( APP_DATABASE_QUERY_MAX_VALUES );
2025-11-05 01:41:15 +00:00
$database -> 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 )
2025-09-18 07:08:18 +00:00
-> setTenant (( int ) $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 );
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 ;
};
2025-03-15 12:18:34 +00:00
}, [ 'pools' , 'dbForPlatform' , 'cache' ]);
App :: setResource ( 'getLogsDB' , function ( Group $pools , Cache $cache ) {
$database = null ;
2025-05-14 06:14:07 +00:00
return function ( ? Document $project = null ) use ( $pools , $cache , & $database ) {
2025-03-15 12:18:34 +00:00
if ( $database !== null && $project !== null && ! $project -> isEmpty () && $project -> getId () !== 'console' ) {
2025-09-18 07:08:18 +00:00
$database -> setTenant (( int ) $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
-> 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' ) {
2025-09-18 07:08:18 +00:00
$database -> setTenant (( int ) $project -> getSequence ());
2025-03-15 12:18:34 +00:00
}
return $database ;
};
}, [ 'pools' , 'cache' ]);
2025-03-15 11:00:36 +00:00
2025-04-08 11:12:28 +00:00
App :: setResource ( 'telemetry' , fn () => new NoTelemetry ());
App :: 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 );
return $cache ;
}, [ 'pools' , 'telemetry' ]);
2025-03-15 11:00:36 +00:00
2025-03-15 12:18:34 +00:00
App :: setResource ( 'redis' , function () {
$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 ;
});
App :: setResource ( 'timelimit' , function ( \Redis $redis ) {
return function ( string $key , int $limit , int $time ) use ( $redis ) {
return new TimeLimitRedis ( $key , $limit , $time , $redis );
};
}, [ 'redis' ]);
2025-05-26 15:42:54 +00:00
App :: setResource ( 'deviceForLocal' , function ( Telemetry $telemetry ) {
return new Device\Telemetry ( $telemetry , new Local ());
}, [ 'telemetry' ]);
App :: setResource ( 'deviceForFiles' , function ( $project , Telemetry $telemetry ) {
return new Device\Telemetry ( $telemetry , getDevice ( APP_STORAGE_UPLOADS . '/app-' . $project -> getId ()));
}, [ 'project' , 'telemetry' ]);
App :: setResource ( 'deviceForSites' , function ( $project , Telemetry $telemetry ) {
return new Device\Telemetry ( $telemetry , getDevice ( APP_STORAGE_SITES . '/app-' . $project -> getId ()));
}, [ 'project' , 'telemetry' ]);
2025-08-05 12:40:39 +00:00
App :: 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' ]);
App :: setResource ( 'deviceForFunctions' , function ( $project , Telemetry $telemetry ) {
return new Device\Telemetry ( $telemetry , getDevice ( APP_STORAGE_FUNCTIONS . '/app-' . $project -> getId ()));
}, [ 'project' , 'telemetry' ]);
App :: setResource ( 'deviceForBuilds' , function ( $project , Telemetry $telemetry ) {
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
{
2025-03-15 12:18:34 +00:00
$connection = ! empty ( $connection ) ? $connection : System :: getEnv ( '_APP_CONNECTIONS_STORAGE' , '' );
2025-03-15 11:00:36 +00:00
if ( ! empty ( $connection )) {
$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 :
2025-02-28 12:52:34 +00:00
if ( ! empty ( $url )) {
2025-05-22 12:49:26 +00:00
$bucketRoot = ( ! empty ( $bucket ) ? $bucket . '/' : '' ) . \ltrim ( $root , '/' );
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 );
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' , '' );
2025-02-28 12:52:34 +00:00
if ( ! empty ( $s3EndpointUrl )) {
2025-05-22 12:49:26 +00:00
$bucketRoot = ( ! empty ( $s3Bucket ) ? $s3Bucket . '/' : '' ) . \ltrim ( $root , '/' );
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 );
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' ;
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' ;
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' ;
return new Wasabi ( $root , $wasabiAccessKey , $wasabiSecretKey , $wasabiBucket , $wasabiRegion , $wasabiAcl );
}
}
}
App :: setResource ( 'mode' , function ( $request ) {
/** @var Appwrite\Utopia\Request $request */
/**
* Defines the mode for the request :
* - 'default' => Requests for Client and Server Side
* - 'admin' => Request from the Console on non - console projects
*/
return $request -> getParam ( 'mode' , $request -> getHeader ( 'x-appwrite-mode' , APP_MODE_DEFAULT ));
}, [ 'request' ]);
App :: setResource ( 'geodb' , function ( $register ) {
/** @var Utopia\Registry\Registry $register */
return $register -> get ( 'geodb' );
}, [ 'register' ]);
App :: setResource ( 'passwordsDictionary' , function ( $register ) {
/** @var Utopia\Registry\Registry $register */
return $register -> get ( 'passwordsDictionary' );
}, [ 'register' ]);
App :: setResource ( 'servers' , function () {
$platforms = Config :: getParam ( 'platforms' );
$server = $platforms [ APP_PLATFORM_SERVER ];
$languages = array_map ( function ( $language ) {
return strtolower ( $language [ 'name' ]);
}, $server [ 'sdks' ]);
return $languages ;
});
App :: setResource ( 'promiseAdapter' , function ( $register ) {
return $register -> get ( 'promiseAdapter' );
}, [ 'register' ]);
App :: setResource ( 'schema' , function ( $utopia , $dbForProject ) {
$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 ;
};
$attributes = function ( int $limit , int $offset ) use ( $dbForProject ) {
$attrs = Authorization :: skip ( fn () => $dbForProject -> find ( 'attributes' , [
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 ,
'documentId' => $id ,
'collectionId' => $collectionId ,
'data' => $args ,
'permissions' => $permissions ,
];
},
'update' => function ( string $databaseId , string $collectionId , array $args ) {
$documentId = $args [ 'id' ];
$permissions = $args [ 'permissions' ] ? ? null ;
unset ( $args [ 'id' ]);
unset ( $args [ 'permissions' ]);
// Order must be the same as the route params
return [
'databaseId' => $databaseId ,
'collectionId' => $collectionId ,
'documentId' => $documentId ,
'data' => $args ,
'permissions' => $permissions ,
];
},
];
return Schema :: build (
$utopia ,
$complexity ,
$attributes ,
$urls ,
$params ,
);
}, [ 'utopia' , 'dbForProject' ]);
App :: setResource ( 'contributors' , function () {
$path = 'app/config/contributors.json' ;
$list = ( file_exists ( $path )) ? json_decode ( file_get_contents ( $path ), true ) : [];
return $list ;
});
App :: setResource ( 'employees' , function () {
$path = 'app/config/employees.json' ;
$list = ( file_exists ( $path )) ? json_decode ( file_get_contents ( $path ), true ) : [];
return $list ;
});
App :: setResource ( 'heroes' , function () {
$path = 'app/config/heroes.json' ;
$list = ( file_exists ( $path )) ? json_decode ( file_get_contents ( $path ), true ) : [];
return $list ;
});
App :: setResource ( 'gitHub' , function ( Cache $cache ) {
return new VcsGitHub ( $cache );
}, [ 'cache' ]);
App :: setResource ( 'requestTimestamp' , function ( $request ) {
//TODO: Move this to the Request class itself
$timestampHeader = $request -> getHeader ( 'x-appwrite-timestamp' );
$requestTimestamp = null ;
if ( ! empty ( $timestampHeader )) {
try {
$requestTimestamp = new \DateTime ( $timestampHeader );
} catch ( \Throwable $e ) {
throw new Exception ( Exception :: GENERAL_ARGUMENT_INVALID , 'Invalid X-Appwrite-Timestamp header value' );
}
}
return $requestTimestamp ;
}, [ 'request' ]);
2025-03-15 12:18:34 +00:00
2025-03-15 11:00:36 +00:00
App :: setResource ( 'plan' , function ( array $plan = []) {
return [];
});
2025-03-15 12:18:34 +00:00
App :: setResource ( 'smsRates' , function () {
return [];
});
2025-04-14 03:55:04 +00:00
App :: setResource ( 'devKey' , function ( Request $request , Document $project , array $servers , Database $dbForPlatform ) {
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' );
2025-04-14 03:55:04 +00:00
if ( ! $key ) {
return new Document ([]);
}
// check expiration
$expire = $key -> getAttribute ( 'expire' );
if ( ! empty ( $expire ) && $expire < DatabaseDateTime :: formatTz ( DatabaseDateTime :: now ())) {
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 ());
2025-04-14 07:34:05 +00:00
Authorization :: skip ( fn () => $dbForPlatform -> updateDocument ( 'devKeys' , $key -> getId (), $key ));
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' , []);
if ( ! in_array ( $sdk , $sdks )) {
$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 ());
2025-04-15 10:47:04 +00:00
$key = Authorization :: skip ( fn () => $dbForPlatform -> updateDocument ( 'devKeys' , $key -> getId (), $key ));
2025-03-22 13:05:10 +00:00
$dbForPlatform -> purgeCachedDocument ( 'projects' , $project -> getId ());
}
}
2025-04-14 03:55:04 +00:00
return $key ;
}, [ 'request' , 'project' , 'servers' , 'dbForPlatform' ]);
2025-03-22 13:05:10 +00:00
2025-03-15 12:18:34 +00:00
App :: setResource ( 'team' , function ( Document $project , Database $dbForPlatform , App $utopia , Request $request ) {
$teamInternalId = '' ;
if ( $project -> getId () !== 'console' ) {
$teamInternalId = $project -> getAttribute ( 'teamInternalId' , '' );
} else {
$route = $utopia -> match ( $request );
$path = $route -> getPath ();
if ( str_starts_with ( $path , '/v1/projects/:projectId' )) {
$uri = $request -> getURI ();
$pid = explode ( '/' , $uri )[ 3 ];
$p = Authorization :: skip ( fn () => $dbForPlatform -> getDocument ( 'projects' , $pid ));
$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 ([]);
}
2025-03-15 12:18:34 +00:00
$team = Authorization :: skip ( fn () => $dbForPlatform -> getDocument ( 'teams' , $teamId ));
return $team ;
}
}
2025-07-29 08:24:41 +00:00
if ( empty ( $teamInternalId )) {
2025-07-29 08:10:25 +00:00
return new Document ([]);
}
2025-03-15 12:18:34 +00:00
$team = Authorization :: skip ( function () use ( $dbForPlatform , $teamInternalId ) {
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 ;
}, [ 'project' , 'dbForPlatform' , 'utopia' , 'request' ]);
App :: setResource (
'isResourceBlocked' ,
fn () => fn ( Document $project , string $resourceType , ? string $resourceId ) => false
);
2025-03-17 10:53:09 +00:00
App :: setResource ( 'previewHostname' , function ( Request $request , ? Key $apiKey ) {
$allowed = false ;
2025-03-17 10:53:23 +00:00
2025-03-15 12:18:34 +00:00
if ( App :: isDevelopment ()) {
2025-03-17 10:53:09 +00:00
$allowed = true ;
} elseif ( ! \is_null ( $apiKey ) && $apiKey -> getHostnameOverride () === true ) {
$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' , '' )) ? ? '' ;
2025-03-15 12:18:34 +00:00
if ( ! empty ( $host )) {
return $host ;
}
}
return '' ;
2025-03-17 10:53:09 +00:00
}, [ 'request' , 'apiKey' ]);
2025-03-15 12:18:34 +00:00
App :: setResource ( 'apiKey' , function ( Request $request , Document $project ) : ? Key {
$key = $request -> getHeader ( 'x-appwrite-key' );
if ( empty ( $key )) {
return null ;
}
return Key :: decode ( $project , $key );
}, [ 'request' , 'project' ]);
2025-03-25 11:33:03 +00:00
2025-06-12 13:18:35 +00:00
App :: setResource ( 'executor' , fn () => new Executor ());
2025-04-17 06:19:18 +00:00
App :: setResource ( 'resourceToken' , function ( $project , $dbForProject , $request ) {
$tokenJWT = $request -> getParam ( 'token' );
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 ([]);
}
$token = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'resourceTokens' , $tokenId ));
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 );
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' )) {
TOKENS_RESOURCE_TYPE_FILES => ( function () use ( $token , $dbForProject ) {
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 ());
Authorization :: skip ( fn () => $dbForProject -> updateDocument ( 'resourceTokens' , $token -> getId (), $token ));
}
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
}
return new Document ([]);
2025-04-17 06:25:42 +00:00
}, [ 'project' , 'dbForProject' , 'request' ]);
2025-06-23 09:28:18 +00:00
App :: setResource ( 'httpReferrer' , function ( Request $request ) : string {
$referrer = $request -> getReferer ();
return $referrer ;
}, [ 'request' ]);
2025-06-30 13:59:42 +00:00
App :: setResource ( 'httpReferrerSafe' , function ( Request $request , string $httpReferrer , array $platforms , Database $dbForPlatform , Document $project , App $utopia ) : string {
2025-06-23 09:28:18 +00:00
$origin = \parse_url ( $request -> getOrigin ( $httpReferrer ), PHP_URL_HOST );
2025-06-25 12:22:55 +00:00
$protocol = \parse_url ( $request -> getOrigin ( $httpReferrer ), PHP_URL_SCHEME );
$port = \parse_url ( $request -> getOrigin ( $httpReferrer ), PHP_URL_PORT );
$referrer = ( ! empty ( $protocol ) ? $protocol : $request -> getProtocol ()) . '://' . $origin . ( ! empty ( $port ) ? ':' . $port : '' );
2025-06-23 09:28:18 +00:00
2025-06-23 09:57:06 +00:00
// Safe if route is publicly accessible
2025-06-23 09:28:18 +00:00
$route = $utopia -> getRoute ();
2025-06-23 09:57:06 +00:00
if ( $route -> getLabel ( 'origin' , false )) {
2025-06-25 12:22:55 +00:00
return $referrer ;
2025-06-23 09:28:18 +00:00
}
// Safe if added as web platform
2025-06-30 13:59:42 +00:00
$originValidator = new Origin ( $platforms );
2025-06-30 15:20:29 +00:00
if ( $originValidator -> isValid ( $request -> getOrigin ( $httpReferrer ))) {
2025-06-25 12:22:55 +00:00
return $referrer ;
2025-06-23 09:28:18 +00:00
}
// Unsafe; Localhost is always safe for ease of local development
$origin = 'localhost' ;
$protocol = \parse_url ( $request -> getOrigin ( $httpReferrer ), PHP_URL_SCHEME );
$port = \parse_url ( $request -> getOrigin ( $httpReferrer ), PHP_URL_PORT );
$referrer = ( ! empty ( $protocol ) ? $protocol : $request -> getProtocol ()) . '://' . $origin . ( ! empty ( $port ) ? ':' . $port : '' );
return $referrer ;
2025-06-30 13:59:42 +00:00
}, [ 'request' , 'httpReferrer' , 'platforms' , 'dbForPlatform' , 'project' , 'utopia' ]);
2025-09-05 16:26:49 +00:00
2025-09-08 07:21:59 +00:00
App :: setResource ( 'transactionState' , function ( Database $dbForProject ) {
return new TransactionState ( $dbForProject );
2025-09-05 16:26:49 +00:00
}, [ 'dbForProject' ]);