2019-05-09 18:54:39 +12:00
< ? php
2019-10-01 17:57:41 +13:00
2021-01-12 10:52:05 +13:00
use Appwrite\Auth\Auth ;
2022-05-26 23:51:08 +12:00
use Appwrite\Event\Audit ;
2024-02-21 00:40:55 +13:00
use Appwrite\Event\Build ;
2022-06-22 22:51:49 +12:00
use Appwrite\Event\Database as EventDatabase ;
2022-05-26 23:51:08 +12:00
use Appwrite\Event\Delete ;
2022-04-04 18:30:07 +12:00
use Appwrite\Event\Event ;
2022-11-16 07:13:17 +13:00
use Appwrite\Event\Func ;
2023-12-07 23:25:19 +13:00
use Appwrite\Event\Messaging ;
2023-05-24 01:43:03 +12:00
use Appwrite\Extend\Exception ;
2023-10-25 20:39:59 +13:00
use Appwrite\Event\Usage ;
2021-06-30 23:36:58 +12:00
use Appwrite\Messaging\Adapter\Realtime ;
2022-05-26 23:51:08 +12:00
use Appwrite\Utopia\Response ;
use Appwrite\Utopia\Request ;
2020-06-29 05:31:21 +12:00
use Utopia\App ;
2019-11-30 07:23:29 +13:00
use Utopia\Abuse\Abuse ;
use Utopia\Abuse\Adapters\TimeLimit ;
2022-07-24 05:42:42 +12:00
use Utopia\Cache\Adapter\Filesystem ;
use Utopia\Cache\Cache ;
2022-05-26 23:51:08 +12:00
use Utopia\Database\Database ;
2022-08-31 20:52:55 +12:00
use Utopia\Database\DateTime ;
2021-10-08 04:35:17 +13:00
use Utopia\Database\Document ;
2022-01-19 00:05:04 +13:00
use Utopia\Database\Validator\Authorization ;
2024-02-17 05:26:50 +13:00
use Utopia\Config\Config ;
use Utopia\Database\Helpers\Role ;
use Utopia\Validator\WhiteList ;
2019-11-30 07:23:29 +13:00
2022-08-17 00:28:30 +12:00
$parseLabel = function ( string $label , array $responsePayload , array $requestParams , Document $user ) {
preg_match_all ( '/{(.*?)}/' , $label , $matches );
foreach ( $matches [ 1 ] ? ? [] as $pos => $match ) {
$find = $matches [ 0 ][ $pos ];
$parts = explode ( '.' , $match );
if ( count ( $parts ) !== 2 ) {
2023-08-02 04:21:34 +12:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , " The server encountered an error while parsing the label: $label . Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose " );
2022-08-17 00:28:30 +12:00
}
$namespace = $parts [ 0 ] ? ? '' ;
$replace = $parts [ 1 ] ? ? '' ;
$params = match ( $namespace ) {
'user' => ( array ) $user ,
'request' => $requestParams ,
default => $responsePayload ,
};
if ( array_key_exists ( $replace , $params )) {
$label = \str_replace ( $find , $params [ $replace ], $label );
}
}
return $label ;
};
2019-11-30 07:23:29 +13:00
2023-10-25 20:39:59 +13:00
$databaseListener = function ( string $event , Document $document , Document $project , Usage $queueForUsage , Database $dbForProject ) {
$value = 1 ;
2022-10-30 18:16:51 +13:00
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
2023-10-25 20:39:59 +13:00
$value = - 1 ;
2022-10-30 18:14:46 +13:00
}
2023-10-25 20:39:59 +13:00
switch ( true ) {
case $document -> getCollection () === 'teams' :
$queueForUsage
-> addMetric ( METRIC_TEAMS , $value ); // per project
2023-05-24 01:43:03 +12:00
break ;
2023-10-25 20:39:59 +13:00
case $document -> getCollection () === 'users' :
$queueForUsage
-> addMetric ( METRIC_USERS , $value ); // per project
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
$queueForUsage
-> addReduce ( $document );
}
2022-10-30 18:14:46 +13:00
break ;
2023-10-25 20:39:59 +13:00
case $document -> getCollection () === 'sessions' : // sessions
$queueForUsage
-> addMetric ( METRIC_SESSIONS , $value ); //per project
2022-10-30 18:14:46 +13:00
break ;
2023-10-25 20:39:59 +13:00
case $document -> getCollection () === 'databases' : // databases
$queueForUsage
-> addMetric ( METRIC_DATABASES , $value ); // per project
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
$queueForUsage
-> addReduce ( $document );
}
2023-05-24 01:43:03 +12:00
break ;
2023-10-25 20:39:59 +13:00
case str_starts_with ( $document -> getCollection (), 'database_' ) && ! str_contains ( $document -> getCollection (), 'collection' ) : //collections
$parts = explode ( '_' , $document -> getCollection ());
$databaseInternalId = $parts [ 1 ] ? ? 0 ;
$queueForUsage
-> addMetric ( METRIC_COLLECTIONS , $value ) // per project
-> addMetric ( str_replace ( '{databaseInternalId}' , $databaseInternalId , METRIC_DATABASE_ID_COLLECTIONS ), $value ) // per database
;
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
$queueForUsage
-> addReduce ( $document );
2023-10-16 06:41:09 +13:00
}
2023-05-24 01:43:03 +12:00
break ;
2023-10-25 20:39:59 +13:00
case str_starts_with ( $document -> getCollection (), 'database_' ) && str_contains ( $document -> getCollection (), '_collection_' ) : //documents
$parts = explode ( '_' , $document -> getCollection ());
$databaseInternalId = $parts [ 1 ] ? ? 0 ;
$collectionInternalId = $parts [ 3 ] ? ? 0 ;
$queueForUsage
-> addMetric ( METRIC_DOCUMENTS , $value ) // per project
-> addMetric ( str_replace ( '{databaseInternalId}' , $databaseInternalId , METRIC_DATABASE_ID_DOCUMENTS ), $value ) // per database
-> addMetric ( str_replace ([ '{databaseInternalId}' , '{collectionInternalId}' ], [ $databaseInternalId , $collectionInternalId ], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS ), $value ); // per collection
break ;
case $document -> getCollection () === 'buckets' : //buckets
$queueForUsage
-> addMetric ( METRIC_BUCKETS , $value ); // per project
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
$queueForUsage
-> addReduce ( $document );
}
break ;
case str_starts_with ( $document -> getCollection (), 'bucket_' ) : // files
$parts = explode ( '_' , $document -> getCollection ());
$bucketInternalId = $parts [ 1 ];
$queueForUsage
-> 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' :
$queueForUsage
-> addMetric ( METRIC_FUNCTIONS , $value ); // per project
if ( $event === Database :: EVENT_DOCUMENT_DELETE ) {
$queueForUsage
-> addReduce ( $document );
2023-10-16 06:41:09 +13:00
}
2023-05-24 01:43:03 +12:00
break ;
2023-10-25 20:39:59 +13:00
case $document -> getCollection () === 'deployments' :
$queueForUsage
-> addMetric ( METRIC_DEPLOYMENTS , $value ) // per project
-> addMetric ( METRIC_DEPLOYMENTS_STORAGE , $document -> getAttribute ( 'size' ) * $value ) // per project
2024-02-17 05:26:50 +13:00
-> addMetric ( str_replace ([ '{resourceType}' , '{resourceInternalId}' ], [ $document -> getAttribute ( 'resourceType' ), $document -> getAttribute ( 'resourceInternalId' )], METRIC_FUNCTION_ID_DEPLOYMENTS ), $value ) // per function
2023-10-25 20:39:59 +13:00
-> addMetric ( str_replace ([ '{resourceType}' , '{resourceInternalId}' ], [ $document -> getAttribute ( 'resourceType' ), $document -> getAttribute ( 'resourceInternalId' )], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE ), $document -> getAttribute ( 'size' ) * $value );
break ;
default :
break ;
2022-10-30 18:14:46 +13:00
}
};
2024-02-17 05:26:50 +13:00
App :: init ()
-> groups ([ 'api' ])
-> inject ( 'utopia' )
-> inject ( 'request' )
-> inject ( 'dbForConsole' )
-> inject ( 'project' )
-> inject ( 'user' )
-> inject ( 'session' )
-> inject ( 'servers' )
-> inject ( 'mode' )
-> action ( function ( App $utopia , Request $request , Database $dbForConsole , Document $project , Document $user , ? Document $session , array $servers , string $mode ) {
$route = $utopia -> getRoute ();
2024-02-17 06:58:51 +13:00
if ( $project -> isEmpty ()) {
throw new Exception ( Exception :: PROJECT_NOT_FOUND );
}
2024-02-17 05:26:50 +13:00
/**
* ACL Check
*/
$role = ( $user -> isEmpty ())
? Role :: guests () -> toString ()
: Role :: users () -> toString ();
// Add user roles
$memberships = $user -> find ( 'teamId' , $project -> getAttribute ( 'teamId' ), 'memberships' );
if ( $memberships ) {
foreach ( $memberships -> getAttribute ( 'roles' , []) as $memberRole ) {
switch ( $memberRole ) {
case 'owner' :
$role = Auth :: USER_ROLE_OWNER ;
break ;
case 'admin' :
$role = Auth :: USER_ROLE_ADMIN ;
break ;
case 'developer' :
$role = Auth :: USER_ROLE_DEVELOPER ;
break ;
}
}
}
$roles = Config :: getParam ( 'roles' , []);
$scope = $route -> getLabel ( 'scope' , 'none' ); // Allowed scope for chosen route
$scopes = $roles [ $role ][ 'scopes' ]; // Allowed scopes for user role
$authKey = $request -> getHeader ( 'x-appwrite-key' , '' );
if ( ! empty ( $authKey )) { // API Key authentication
2024-02-21 00:45:11 +13:00
// Do not allow API key and session to be set at the same time
if ( ! $user -> isEmpty ()) {
throw new Exception ( Exception :: USER_API_KEY_AND_SESSION_SET );
}
2024-02-17 05:26:50 +13:00
// Check if given key match project API keys
$key = $project -> find ( 'secret' , $authKey , 'keys' );
2024-02-21 00:45:11 +13:00
if ( $key ) {
2024-02-17 05:26:50 +13:00
$user = new Document ([
'$id' => '' ,
'status' => true ,
'email' => 'app.' . $project -> getId () . '@service.' . $request -> getHostname (),
'password' => '' ,
'name' => $project -> getAttribute ( 'name' , 'Untitled' ),
]);
$role = Auth :: USER_ROLE_APPS ;
$scopes = \array_merge ( $roles [ $role ][ 'scopes' ], $key -> getAttribute ( 'scopes' , []));
$expire = $key -> getAttribute ( 'expire' );
if ( ! empty ( $expire ) && $expire < DateTime :: formatTz ( DateTime :: now ())) {
throw new Exception ( Exception :: PROJECT_KEY_EXPIRED );
}
Authorization :: setRole ( Auth :: USER_ROLE_APPS );
Authorization :: setDefaultStatus ( false ); // Cancel security segmentation for API keys.
$accessedAt = $key -> getAttribute ( 'accessedAt' , '' );
if ( DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_KEY_ACCCESS )) > $accessedAt ) {
$key -> setAttribute ( 'accessedAt' , DateTime :: now ());
$dbForConsole -> updateDocument ( 'keys' , $key -> getId (), $key );
$dbForConsole -> purgeCachedDocument ( 'projects' , $project -> getId ());
}
$sdkValidator = new WhiteList ( $servers , true );
$sdk = $request -> getHeader ( 'x-sdk-name' , 'UNKNOWN' );
if ( $sdkValidator -> isValid ( $sdk )) {
$sdks = $key -> getAttribute ( 'sdks' , []);
if ( ! in_array ( $sdk , $sdks )) {
array_push ( $sdks , $sdk );
$key -> setAttribute ( 'sdks' , $sdks );
/** Update access time as well */
$key -> setAttribute ( 'accessedAt' , Datetime :: now ());
$dbForConsole -> updateDocument ( 'keys' , $key -> getId (), $key );
$dbForConsole -> purgeCachedDocument ( 'projects' , $project -> getId ());
}
}
}
}
Authorization :: setRole ( $role );
foreach ( Auth :: getRoles ( $user ) as $authRole ) {
Authorization :: setRole ( $authRole );
}
$service = $route -> getLabel ( 'sdk.namespace' , '' );
if ( ! empty ( $service )) {
if (
array_key_exists ( $service , $project -> getAttribute ( 'services' , []))
&& ! $project -> getAttribute ( 'services' , [])[ $service ]
&& ! ( Auth :: isPrivilegedUser ( Authorization :: getRoles ()) || Auth :: isAppUser ( Authorization :: getRoles ()))
) {
throw new Exception ( Exception :: GENERAL_SERVICE_DISABLED );
}
}
if ( ! \in_array ( $scope , $scopes )) {
if ( $project -> isEmpty ()) { // Check if permission is denied because project is missing
throw new Exception ( Exception :: PROJECT_NOT_FOUND );
}
throw new Exception ( Exception :: GENERAL_UNAUTHORIZED_SCOPE , $user -> getAttribute ( 'email' , 'User' ) . ' (role: ' . \strtolower ( $roles [ $role ][ 'label' ]) . ') missing scope (' . $scope . ')' );
}
if ( false === $user -> getAttribute ( 'status' )) { // Account is blocked
throw new Exception ( Exception :: USER_BLOCKED );
}
if ( $user -> getAttribute ( 'reset' )) {
throw new Exception ( Exception :: USER_PASSWORD_RESET_REQUIRED );
}
if ( $mode !== APP_MODE_ADMIN ) {
$mfaEnabled = $user -> getAttribute ( 'mfa' , false );
$hasVerifiedAuthenticator = $user -> getAttribute ( 'totpVerification' , false );
$hasVerifiedEmail = $user -> getAttribute ( 'emailVerification' , false );
$hasVerifiedPhone = $user -> getAttribute ( 'phoneVerification' , false );
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator ;
$minimumFactors = ( $mfaEnabled && $hasMoreFactors ) ? 2 : 1 ;
if ( ! in_array ( 'mfa' , $route -> getGroups ())) {
if ( $session && \count ( $session -> getAttribute ( 'factors' )) < $minimumFactors ) {
throw new Exception ( Exception :: USER_MORE_FACTORS_REQUIRED );
}
}
}
});
2022-07-22 18:00:42 +12:00
App :: init ()
2022-08-02 13:10:48 +12:00
-> groups ([ 'api' ])
2022-07-22 18:00:42 +12:00
-> inject ( 'utopia' )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'user' )
2022-12-21 05:11:30 +13:00
-> inject ( 'queueForEvents' )
2023-12-07 23:25:19 +13:00
-> inject ( 'queueForMessaging' )
2022-12-21 05:11:30 +13:00
-> inject ( 'queueForAudits' )
-> inject ( 'queueForDeletes' )
-> inject ( 'queueForDatabase' )
2024-02-21 00:40:55 +13:00
-> inject ( 'queueForBuilds' )
2024-01-24 23:24:59 +13:00
-> inject ( 'queueForUsage' )
2022-07-22 18:00:42 +12:00
-> inject ( 'dbForProject' )
-> inject ( 'mode' )
2024-02-21 00:40:55 +13:00
-> action ( function ( App $utopia , Request $request , Response $response , Document $project , Document $user , Event $queueForEvents , Messaging $queueForMessaging , Audit $queueForAudits , Delete $queueForDeletes , EventDatabase $queueForDatabase , Build $queueForBuilds , Usage $queueForUsage , Database $dbForProject , string $mode ) use ( $databaseListener ) {
2022-07-22 18:00:42 +12:00
2023-02-20 00:04:12 +13:00
$route = $utopia -> getRoute ();
2022-07-22 18:00:42 +12:00
2022-08-19 16:20:19 +12:00
/*
2022-07-22 18:00:42 +12:00
* Abuse Check
*/
2022-08-12 11:53:52 +12:00
$abuseKeyLabel = $route -> getLabel ( 'abuse-key' , 'url:{url},ip:{ip}' );
$timeLimitArray = [];
2021-06-07 17:17:29 +12:00
2022-07-22 18:00:42 +12:00
$abuseKeyLabel = ( ! is_array ( $abuseKeyLabel )) ? [ $abuseKeyLabel ] : $abuseKeyLabel ;
2021-06-07 17:17:29 +12:00
2022-08-12 11:53:52 +12:00
foreach ( $abuseKeyLabel as $abuseKey ) {
2023-11-17 06:34:38 +13:00
$start = $request -> getContentRangeStart ();
$end = $request -> getContentRangeEnd ();
2022-08-12 11:53:52 +12:00
$timeLimit = new TimeLimit ( $abuseKey , $route -> getLabel ( 'abuse-limit' , 0 ), $route -> getLabel ( 'abuse-time' , 3600 ), $dbForProject );
$timeLimit
2024-02-12 14:18:19 +13:00
-> setParam ( '{projectId}' , $project -> getId ())
2023-05-24 01:43:03 +12:00
-> setParam ( '{userId}' , $user -> getId ())
-> setParam ( '{userAgent}' , $request -> getUserAgent ( '' ))
-> setParam ( '{ip}' , $request -> getIP ())
-> setParam ( '{url}' , $request -> getHostname () . $route -> getPath ())
2023-11-17 06:34:38 +13:00
-> setParam ( '{method}' , $request -> getMethod ())
-> setParam ( '{chunkId}' , ( int ) ( $start / ( $end + 1 - $start )));
2022-08-12 11:53:52 +12:00
$timeLimitArray [] = $timeLimit ;
}
2021-06-07 17:17:29 +12:00
2022-07-22 18:00:42 +12:00
$closestLimit = null ;
2021-06-07 17:17:29 +12:00
2022-07-22 18:00:42 +12:00
$roles = Authorization :: getRoles ();
$isPrivilegedUser = Auth :: isPrivilegedUser ( $roles );
$isAppUser = Auth :: isAppUser ( $roles );
2021-06-07 17:17:29 +12:00
2022-07-22 18:00:42 +12:00
foreach ( $timeLimitArray as $timeLimit ) {
foreach ( $request -> getParams () as $key => $value ) { // Set request params as potential abuse keys
if ( ! empty ( $value )) {
$timeLimit -> setParam ( '{param-' . $key . '}' , ( \is_array ( $value )) ? \json_encode ( $value ) : $value );
}
2021-11-09 23:21:24 +13:00
}
2021-06-07 17:17:29 +12:00
2022-07-22 18:00:42 +12:00
$abuse = new Abuse ( $timeLimit );
2022-08-13 15:21:50 +12:00
$remaining = $timeLimit -> remaining ();
$limit = $timeLimit -> limit ();
2022-08-31 20:52:55 +12:00
$time = ( new \DateTime ( $timeLimit -> time ())) -> getTimestamp () + $route -> getLabel ( 'abuse-time' , 3600 );
2022-07-22 18:00:42 +12:00
2022-08-13 15:21:50 +12:00
if ( $limit && ( $remaining < $closestLimit || is_null ( $closestLimit ))) {
$closestLimit = $remaining ;
2022-07-22 18:00:42 +12:00
$response
2022-08-13 15:21:50 +12:00
-> addHeader ( 'X-RateLimit-Limit' , $limit )
-> addHeader ( 'X-RateLimit-Remaining' , $remaining )
2024-02-17 07:00:49 +13:00
-> addHeader ( 'X-RateLimit-Reset' , $time )
;
2021-03-01 07:36:13 +13:00
}
2021-06-07 17:17:29 +12:00
2022-08-31 15:50:53 +12:00
$enabled = App :: getEnv ( '_APP_OPTIONS_ABUSE' , 'enabled' ) !== 'disabled' ;
2022-07-22 18:00:42 +12:00
if (
2022-08-31 15:50:53 +12:00
$enabled // Abuse is enabled
&& ! $isAppUser // User is not API key
&& ! $isPrivilegedUser // User is not an admin
&& $abuse -> check () // Route is rate-limited
) {
2022-08-09 02:44:07 +12:00
throw new Exception ( Exception :: GENERAL_RATE_LIMIT_EXCEEDED );
2021-03-01 07:36:13 +13:00
}
2021-11-09 23:21:24 +13:00
}
2021-01-06 01:22:20 +13:00
2022-11-16 18:30:57 +13:00
/*
* Background Jobs
*/
2022-12-21 05:11:30 +13:00
$queueForEvents
2022-07-22 18:00:42 +12:00
-> setEvent ( $route -> getLabel ( 'event' , '' ))
-> setProject ( $project )
2022-08-12 01:47:53 +12:00
-> setUser ( $user );
2022-04-04 18:30:07 +12:00
2022-12-21 05:11:30 +13:00
$queueForAudits
2022-07-22 18:00:42 +12:00
-> setMode ( $mode )
-> setUserAgent ( $request -> getUserAgent ( '' ))
-> setIP ( $request -> getIP ())
2022-09-04 20:23:24 +12:00
-> setEvent ( $route -> getLabel ( 'audits.event' , '' ))
2022-07-22 18:00:42 +12:00
-> setProject ( $project )
2022-08-12 01:47:53 +12:00
-> setUser ( $user );
2021-03-01 07:36:13 +13:00
2022-12-21 05:11:30 +13:00
$queueForDeletes -> setProject ( $project );
$queueForDatabase -> setProject ( $project );
2024-02-21 00:40:55 +13:00
$queueForBuilds -> setProject ( $project );
2024-02-21 01:06:35 +13:00
$queueForMessaging -> setProject ( $project );
2022-08-09 23:57:33 +12:00
2023-10-25 20:39:59 +13:00
$dbForProject
-> on ( Database :: EVENT_DOCUMENT_CREATE , 'calculate-usage' , fn ( $event , $document ) => $databaseListener ( $event , $document , $project , $queueForUsage , $dbForProject ))
2024-02-17 07:00:49 +13:00
-> on ( Database :: EVENT_DOCUMENT_DELETE , 'calculate-usage' , fn ( $event , $document ) => $databaseListener ( $event , $document , $project , $queueForUsage , $dbForProject ))
;
2022-10-17 20:10:18 +13:00
2022-08-09 23:57:33 +12:00
$useCache = $route -> getLabel ( 'cache' , false );
if ( $useCache ) {
2023-09-06 03:29:11 +12:00
$key = md5 ( $request -> getURI () . implode ( '*' , $request -> getParams ()) . '*' . APP_CACHE_BUSTER );
2023-06-01 04:34:12 +12:00
$cacheLog = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'cache' , $key ));
2022-08-16 01:55:11 +12:00
$cache = new Cache (
new Filesystem ( APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project -> getId ())
);
2022-08-09 23:57:33 +12:00
$timestamp = 60 * 60 * 24 * 30 ;
$data = $cache -> load ( $key , $timestamp );
2022-11-10 23:08:01 +13:00
2023-06-01 00:50:19 +12:00
if ( ! empty ( $data ) && ! $cacheLog -> isEmpty ()) {
2023-05-31 23:41:56 +12:00
$parts = explode ( '/' , $cacheLog -> getAttribute ( 'resourceType' ));
2022-11-10 23:08:01 +13:00
$type = $parts [ 0 ] ? ? null ;
if ( $type === 'bucket' ) {
$bucketId = $parts [ 1 ] ? ? null ;
2023-05-31 23:41:56 +12:00
$bucket = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'buckets' , $bucketId ));
2022-11-10 23:08:01 +13:00
2023-08-17 09:58:25 +12:00
$isAPIKey = Auth :: isAppUser ( Authorization :: getRoles ());
$isPrivilegedUser = Auth :: isPrivilegedUser ( Authorization :: getRoles ());
if ( $bucket -> isEmpty () || ( ! $bucket -> getAttribute ( 'enabled' ) && ! $isAPIKey && ! $isPrivilegedUser )) {
2022-11-10 23:08:01 +13:00
throw new Exception ( Exception :: STORAGE_BUCKET_NOT_FOUND );
}
$fileSecurity = $bucket -> getAttribute ( 'fileSecurity' , false );
$validator = new Authorization ( Database :: PERMISSION_READ );
$valid = $validator -> isValid ( $bucket -> getRead ());
2023-05-31 23:41:56 +12:00
2022-11-10 23:08:01 +13:00
if ( ! $fileSecurity && ! $valid ) {
throw new Exception ( Exception :: USER_UNAUTHORIZED );
}
2023-05-31 23:41:56 +12:00
$parts = explode ( '/' , $cacheLog -> getAttribute ( 'resource' ));
2022-11-10 23:08:01 +13:00
$fileId = $parts [ 1 ] ? ? null ;
if ( $fileSecurity && ! $valid ) {
$file = $dbForProject -> getDocument ( 'bucket_' . $bucket -> getInternalId (), $fileId );
} else {
2024-02-17 05:26:50 +13:00
$file = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'bucket_' . $bucket -> getInternalId (), $fileId ));
2022-11-10 23:08:01 +13:00
}
if ( $file -> isEmpty ()) {
throw new Exception ( Exception :: STORAGE_FILE_NOT_FOUND );
}
}
2022-08-10 01:43:37 +12:00
$response
-> addHeader ( 'Expires' , \date ( 'D, d M Y H:i:s' , \time () + $timestamp ) . ' GMT' )
-> addHeader ( 'X-Appwrite-Cache' , 'hit' )
2023-05-31 23:41:56 +12:00
-> setContentType ( $cacheLog -> getAttribute ( 'mimeType' ))
-> send ( $data )
2024-02-17 07:00:49 +13:00
;
2022-08-10 01:43:37 +12:00
} else {
$response -> addHeader ( 'X-Appwrite-Cache' , 'miss' );
}
}
2022-07-22 18:00:42 +12:00
});
2024-02-27 22:08:39 +13:00
App :: init ()
-> groups ([ 'session' ])
-> inject ( 'user' )
-> inject ( 'request' )
-> action ( function ( Document $user , Request $request ) {
if ( \str_contains ( $request -> getURI (), 'oauth2' )) {
return ;
}
if ( ! $user -> isEmpty ()) {
throw new Exception ( Exception :: USER_SESSION_ALREADY_EXISTS );
}
});
2023-05-24 01:43:03 +12:00
/**
* Limit user session
*
* Delete older sessions if the number of sessions have crossed
* the session limit set for the project
*/
App :: shutdown ()
-> groups ([ 'session' ])
-> inject ( 'utopia' )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'dbForProject' )
-> action ( function ( App $utopia , Request $request , Response $response , Document $project , Database $dbForProject ) {
$sessionLimit = $project -> getAttribute ( 'auths' , [])[ 'maxSessions' ] ? ? APP_LIMIT_USER_SESSIONS_DEFAULT ;
$session = $response -> getPayload ();
$userId = $session [ 'userId' ] ? ? '' ;
if ( empty ( $userId )) {
return ;
}
$user = $dbForProject -> getDocument ( 'users' , $userId );
if ( $user -> isEmpty ()) {
return ;
}
$sessions = $user -> getAttribute ( 'sessions' , []);
$count = \count ( $sessions );
if ( $count <= $sessionLimit ) {
return ;
}
for ( $i = 0 ; $i < ( $count - $sessionLimit ); $i ++ ) {
$session = array_shift ( $sessions );
$dbForProject -> deleteDocument ( 'sessions' , $session -> getId ());
}
2024-02-27 22:08:39 +13:00
2023-12-15 02:32:06 +13:00
$dbForProject -> purgeCachedDocument ( 'users' , $userId );
2023-05-24 01:43:03 +12:00
});
2022-07-22 18:00:42 +12:00
App :: shutdown ()
2022-08-02 13:10:48 +12:00
-> groups ([ 'api' ])
2022-07-22 18:00:42 +12:00
-> inject ( 'utopia' )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
2023-07-07 12:12:39 +12:00
-> inject ( 'user' )
2022-12-21 05:11:30 +13:00
-> inject ( 'queueForEvents' )
-> inject ( 'queueForAudits' )
2023-10-25 20:39:59 +13:00
-> inject ( 'queueForUsage' )
2022-12-21 05:11:30 +13:00
-> inject ( 'queueForDeletes' )
-> inject ( 'queueForDatabase' )
2024-02-21 00:40:55 +13:00
-> inject ( 'queueForBuilds' )
2024-02-21 01:06:35 +13:00
-> inject ( 'queueForMessaging' )
2022-07-22 18:00:42 +12:00
-> inject ( 'dbForProject' )
2022-11-16 23:33:11 +13:00
-> inject ( 'queueForFunctions' )
2022-12-15 20:56:06 +13:00
-> inject ( 'mode' )
2023-07-07 12:12:39 +12:00
-> inject ( 'dbForConsole' )
2024-02-21 01:06:35 +13:00
-> action ( function ( App $utopia , Request $request , Response $response , Document $project , Document $user , Event $queueForEvents , Audit $queueForAudits , Usage $queueForUsage , Delete $queueForDeletes , EventDatabase $queueForDatabase , Build $queueForBuilds , Messaging $queueForMessaging , Database $dbForProject , Func $queueForFunctions , string $mode , Database $dbForConsole ) use ( $parseLabel ) {
2022-08-09 23:57:33 +12:00
2022-08-08 03:09:37 +12:00
$responsePayload = $response -> getPayload ();
2022-07-22 18:00:42 +12:00
2022-12-21 05:11:30 +13:00
if ( ! empty ( $queueForEvents -> getEvent ())) {
if ( empty ( $queueForEvents -> getPayload ())) {
$queueForEvents -> setPayload ( $responsePayload );
2021-03-01 07:36:13 +13:00
}
2023-06-02 15:54:34 +12:00
2022-07-22 18:00:42 +12:00
/**
* Trigger functions .
*/
2022-11-16 23:33:11 +13:00
$queueForFunctions
2022-12-21 05:11:30 +13:00
-> from ( $queueForEvents )
2022-07-22 18:00:42 +12:00
-> trigger ();
/**
* Trigger webhooks .
*/
2022-12-21 05:11:30 +13:00
$queueForEvents
2022-07-22 18:00:42 +12:00
-> setClass ( Event :: WEBHOOK_CLASS_NAME )
-> setQueue ( Event :: WEBHOOK_QUEUE_NAME )
-> trigger ();
/**
* Trigger realtime .
*/
if ( $project -> getId () !== 'console' ) {
2022-12-21 05:11:30 +13:00
$allEvents = Event :: generateEvents ( $queueForEvents -> getEvent (), $queueForEvents -> getParams ());
$payload = new Document ( $queueForEvents -> getPayload ());
2022-07-22 18:00:42 +12:00
2022-12-21 05:11:30 +13:00
$db = $queueForEvents -> getContext ( 'database' );
$collection = $queueForEvents -> getContext ( 'collection' );
$bucket = $queueForEvents -> getContext ( 'bucket' );
2022-07-22 18:00:42 +12:00
$target = Realtime :: fromPayload (
2022-08-11 19:02:40 +12:00
// Pass first, most verbose event pattern
2022-07-22 18:00:42 +12:00
event : $allEvents [ 0 ],
payload : $payload ,
project : $project ,
database : $db ,
collection : $collection ,
bucket : $bucket ,
);
Realtime :: send (
projectId : $target [ 'projectId' ] ? ? $project -> getId (),
2022-12-21 05:11:30 +13:00
payload : $queueForEvents -> getPayload (),
2022-07-22 18:00:42 +12:00
events : $allEvents ,
channels : $target [ 'channels' ],
roles : $target [ 'roles' ],
options : [
'permissionsChanged' => $target [ 'permissionsChanged' ],
2022-12-21 05:11:30 +13:00
'userId' => $queueForEvents -> getParam ( 'userId' )
2022-07-22 18:00:42 +12:00
]
);
2021-03-01 07:36:13 +13:00
}
2022-04-19 04:21:45 +12:00
}
2021-03-01 07:36:13 +13:00
2023-02-20 00:04:12 +13:00
$route = $utopia -> getRoute ();
2022-08-15 07:27:43 +12:00
$requestParams = $route -> getParamsValues ();
2022-08-09 00:19:41 +12:00
2022-08-18 02:29:22 +12:00
/**
* Audit labels
*/
2022-08-17 00:28:30 +12:00
$pattern = $route -> getLabel ( 'audits.resource' , null );
if ( ! empty ( $pattern )) {
$resource = $parseLabel ( $pattern , $responsePayload , $requestParams , $user );
if ( ! empty ( $resource ) && $resource !== $pattern ) {
2022-12-21 05:11:30 +13:00
$queueForAudits -> setResource ( $resource );
2022-08-08 02:30:47 +12:00
}
2022-08-08 03:49:30 +12:00
}
2022-08-13 20:02:00 +12:00
2023-07-07 12:12:39 +12:00
if ( ! $user -> isEmpty ()) {
2023-10-02 06:39:26 +13:00
$queueForAudits -> setUser ( $user );
2021-03-12 05:28:03 +13:00
}
2021-10-08 04:35:17 +13:00
2022-12-21 05:11:30 +13:00
if ( ! empty ( $queueForAudits -> getResource ()) && ! empty ( $queueForAudits -> getUser () -> getId ())) {
2022-08-17 00:28:30 +12:00
/**
* audits . payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route -> getLabel ( 'audits.payload' , true );
if ( ! empty ( $pattern )) {
2022-12-21 05:11:30 +13:00
$queueForAudits -> setPayload ( $responsePayload );
2022-08-17 00:28:30 +12:00
}
2022-12-21 05:11:30 +13:00
foreach ( $queueForEvents -> getParams () as $key => $value ) {
$queueForAudits -> setParam ( $key , $value );
2021-03-01 07:36:13 +13:00
}
2022-12-21 05:11:30 +13:00
$queueForAudits -> trigger ();
2022-04-19 04:21:45 +12:00
}
2021-10-08 04:35:17 +13:00
2022-12-21 05:11:30 +13:00
if ( ! empty ( $queueForDeletes -> getType ())) {
$queueForDeletes -> trigger ();
2022-04-14 00:39:31 +12:00
}
2021-10-08 04:35:17 +13:00
2022-12-21 05:11:30 +13:00
if ( ! empty ( $queueForDatabase -> getType ())) {
$queueForDatabase -> trigger ();
2022-07-22 18:00:42 +12:00
}
2021-10-08 04:35:17 +13:00
2024-02-21 00:40:55 +13:00
if ( ! empty ( $queueForBuilds -> getType ())) {
$queueForBuilds -> trigger ();
}
2024-02-21 01:06:35 +13:00
if ( ! empty ( $queueForMessaging -> getType ())) {
2024-02-21 02:20:09 +13:00
$queueForMessaging -> trigger ();
2024-02-21 01:06:35 +13:00
}
2022-08-18 02:29:22 +12:00
/**
* Cache label
*/
2022-08-17 03:02:17 +12:00
$useCache = $route -> getLabel ( 'cache' , false );
if ( $useCache ) {
2022-11-10 23:08:01 +13:00
$resource = $resourceType = null ;
2022-08-10 01:43:37 +12:00
$data = $response -> getPayload ();
2022-08-17 03:02:17 +12:00
if ( ! empty ( $data [ 'payload' ])) {
$pattern = $route -> getLabel ( 'cache.resource' , null );
if ( ! empty ( $pattern )) {
$resource = $parseLabel ( $pattern , $responsePayload , $requestParams , $user );
}
2022-08-15 21:05:41 +12:00
2022-11-10 23:08:01 +13:00
$pattern = $route -> getLabel ( 'cache.resourceType' , null );
if ( ! empty ( $pattern )) {
$resourceType = $parseLabel ( $pattern , $responsePayload , $requestParams , $user );
}
2023-07-23 02:08:28 +12:00
$key = md5 ( $request -> getURI () . '*' . implode ( '*' , $request -> getParams ())) . '*' . APP_CACHE_BUSTER ;
2023-05-31 23:41:56 +12:00
$signature = md5 ( $data [ 'payload' ]);
2023-06-01 04:34:12 +12:00
$cacheLog = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'cache' , $key ));
2022-08-31 20:52:55 +12:00
$accessedAt = $cacheLog -> getAttribute ( 'accessedAt' , '' );
2022-09-01 01:07:27 +12:00
$now = DateTime :: now ();
2022-08-17 03:02:17 +12:00
if ( $cacheLog -> isEmpty ()) {
Authorization :: skip ( fn () => $dbForProject -> createDocument ( 'cache' , new Document ([
2022-08-10 01:43:37 +12:00
'$id' => $key ,
2022-08-15 21:05:41 +12:00
'resource' => $resource ,
2023-05-31 23:41:56 +12:00
'resourceType' => $resourceType ,
'mimeType' => $response -> getContentType (),
2022-09-01 01:07:27 +12:00
'accessedAt' => $now ,
2022-08-15 21:05:41 +12:00
'signature' => $signature ,
2022-08-10 01:43:37 +12:00
])));
2022-09-01 01:11:23 +12:00
} elseif ( DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_CACHE_UPDATE )) > $accessedAt ) {
2022-09-01 01:07:27 +12:00
$cacheLog -> setAttribute ( 'accessedAt' , $now );
2022-08-10 01:43:37 +12:00
Authorization :: skip ( fn () => $dbForProject -> updateDocument ( 'cache' , $cacheLog -> getId (), $cacheLog ));
2022-08-17 03:02:17 +12:00
}
2022-08-15 03:01:34 +12:00
2022-08-17 03:02:17 +12:00
if ( $signature !== $cacheLog -> getAttribute ( 'signature' )) {
$cache = new Cache (
new Filesystem ( APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project -> getId ())
);
2023-05-31 23:41:56 +12:00
$cache -> save ( $key , $data [ 'payload' ]);
2022-08-17 03:02:17 +12:00
}
2022-08-16 00:16:32 +12:00
}
2022-07-24 05:42:42 +12:00
}
2023-10-25 20:39:59 +13:00
if ( $project -> getId () !== 'console' ) {
if ( $mode !== APP_MODE_ADMIN ) {
$fileSize = 0 ;
$file = $request -> getFiles ( 'file' );
if ( ! empty ( $file )) {
$fileSize = ( \is_array ( $file [ 'size' ]) && isset ( $file [ 'size' ][ 0 ])) ? $file [ 'size' ][ 0 ] : $file [ 'size' ];
2022-08-10 14:33:46 +12:00
}
2023-10-25 20:39:59 +13:00
$queueForUsage
-> addMetric ( METRIC_NETWORK_REQUESTS , 1 )
-> addMetric ( METRIC_NETWORK_INBOUND , $request -> getSize () + $fileSize )
-> addMetric ( METRIC_NETWORK_OUTBOUND , $response -> getSize ());
2022-08-17 22:55:01 +12:00
}
2023-10-25 20:39:59 +13:00
$queueForUsage
-> setProject ( $project )
-> trigger ();
2022-07-22 18:00:42 +12:00
}
2023-12-16 10:37:26 +13:00
/**
* Update user last activity
*/
if ( ! $user -> isEmpty ()) {
$accessedAt = $user -> getAttribute ( 'accessedAt' , '' );
if ( DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_USER_ACCCESS )) > $accessedAt ) {
$user -> setAttribute ( 'accessedAt' , DateTime :: now ());
if ( APP_MODE_ADMIN !== $mode ) {
$dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
} else {
$dbForConsole -> updateDocument ( 'users' , $user -> getId (), $user );
}
}
}
2022-07-22 18:00:42 +12:00
});
2023-11-07 10:28:45 +13:00
App :: init ()
-> groups ([ 'usage' ])
-> action ( function () {
if ( App :: getEnv ( '_APP_USAGE_STATS' , 'enabled' ) !== 'enabled' ) {
throw new Exception ( Exception :: GENERAL_USAGE_DISABLED );
}
});