2019-05-09 06:54:39 +00:00
< ? php
2024-10-08 07:54:40 +00:00
require_once __DIR__ . '/../init.php' ;
2024-07-23 12:28:13 +00:00
use Ahc\Jwt\JWT ;
2025-02-10 14:07:01 +00:00
use Ahc\Jwt\JWTException ;
2025-02-21 19:50:43 +00:00
use Appwrite\Auth\Key ;
2022-04-13 12:39:31 +00:00
use Appwrite\Event\Certificate ;
2026-01-16 09:36:35 +00:00
use Appwrite\Event\Delete as DeleteEvent ;
2024-02-22 09:40:18 +00:00
use Appwrite\Event\Event ;
2026-02-09 19:24:57 +00:00
use Appwrite\Event\Execution ;
2025-01-30 04:53:53 +00:00
use Appwrite\Event\StatsUsage ;
2024-03-06 17:34:21 +00:00
use Appwrite\Extend\Exception as AppwriteException ;
2025-12-07 20:29:45 +00:00
use Appwrite\Network\Cors ;
2024-10-22 11:00:10 +00:00
use Appwrite\Platform\Appwrite ;
2025-01-17 04:31:39 +00:00
use Appwrite\SDK\Method ;
2025-03-13 09:20:45 +00:00
use Appwrite\SDK\Response as SDKResponse ;
2025-02-11 13:26:49 +00:00
use Appwrite\Transformation\Adapter\Preview ;
use Appwrite\Transformation\Transformation ;
2025-11-04 07:12:42 +00:00
use Appwrite\Utopia\Database\Documents\User as DBUser ;
2024-03-06 17:34:21 +00:00
use Appwrite\Utopia\Request ;
use Appwrite\Utopia\Request\Filters\V16 as RequestV16 ;
use Appwrite\Utopia\Request\Filters\V17 as RequestV17 ;
2024-07-17 05:52:17 +00:00
use Appwrite\Utopia\Request\Filters\V18 as RequestV18 ;
2025-03-12 11:10:10 +00:00
use Appwrite\Utopia\Request\Filters\V19 as RequestV19 ;
2025-06-13 11:17:40 +00:00
use Appwrite\Utopia\Request\Filters\V20 as RequestV20 ;
2025-09-18 06:57:24 +00:00
use Appwrite\Utopia\Request\Filters\V21 as RequestV21 ;
2024-03-06 17:34:21 +00:00
use Appwrite\Utopia\Response ;
2023-08-22 18:13:37 +00:00
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16 ;
2024-01-16 11:10:07 +00:00
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17 ;
2024-07-17 05:52:17 +00:00
use Appwrite\Utopia\Response\Filters\V18 as ResponseV18 ;
2025-03-12 11:10:10 +00:00
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19 ;
2024-03-06 17:34:21 +00:00
use Appwrite\Utopia\View ;
use Executor\Executor ;
use MaxMind\Db\Reader ;
2024-10-08 07:54:40 +00:00
use Swoole\Http\Request as SwooleRequest ;
2024-03-06 17:34:21 +00:00
use Utopia\Config\Config ;
2026-02-10 05:04:24 +00:00
use Utopia\Console ;
2022-05-26 14:46:08 +00:00
use Utopia\Database\Database ;
2024-10-04 22:23:18 +00:00
use Utopia\Database\DateTime ;
2021-05-16 09:18:34 +00:00
use Utopia\Database\Document ;
2025-12-07 20:29:45 +00:00
use Utopia\Database\Exception\Duplicate ;
2024-02-24 18:01:00 +00:00
use Utopia\Database\Helpers\ID ;
2024-11-22 17:16:51 +00:00
use Utopia\Database\Query ;
2021-07-25 14:51:04 +00:00
use Utopia\Database\Validator\Authorization ;
2024-03-06 17:34:21 +00:00
use Utopia\Domains\Domain ;
2024-05-08 04:25:12 +00:00
use Utopia\DSN\DSN ;
2026-02-10 05:04:24 +00:00
use Utopia\Http\Http ;
2024-03-06 17:34:21 +00:00
use Utopia\Locale\Locale ;
2024-08-26 10:42:48 +00:00
use Utopia\Logger\Adapter\Sentry ;
2024-03-06 17:34:21 +00:00
use Utopia\Logger\Log ;
use Utopia\Logger\Log\User ;
use Utopia\Logger\Logger ;
2024-10-22 11:00:10 +00:00
use Utopia\Platform\Service ;
2026-02-13 13:29:54 +00:00
use Utopia\Span\Span ;
2024-04-01 11:08:46 +00:00
use Utopia\System\System ;
2025-12-07 20:29:45 +00:00
use Utopia\Validator ;
2024-10-08 07:54:40 +00:00
use Utopia\Validator\Text ;
2019-05-09 06:54:39 +00:00
2020-06-28 20:45:36 +00:00
Config :: setParam ( 'domainVerification' , false );
2020-07-01 06:35:57 +00:00
Config :: setParam ( 'cookieDomain' , 'localhost' );
Config :: setParam ( 'cookieSamesite' , Response :: COOKIE_SAMESITE_NONE );
2020-06-29 21:43:34 +00:00
2026-02-09 19:24:57 +00:00
function router ( Http $utopia , Database $dbForPlatform , callable $getProjectDB , SwooleRequest $swooleRequest , Request $request , Response $response , Log $log , Event $queueForEvents , StatsUsage $queueForStatsUsage , Execution $queueForExecutions , Executor $executor , Reader $geodb , callable $isResourceBlocked , array $platform , string $previewHostname , Authorization $authorization , ? Key $apiKey , DeleteEvent $queueForDeletes , int $executionsRetentionCount )
2023-03-14 19:31:23 +00:00
{
2023-07-28 07:56:07 +00:00
$host = $request -> getHostname () ? ? '' ;
2024-11-06 16:05:58 +00:00
if ( ! empty ( $previewHostname )) {
$host = $previewHostname ;
2024-11-06 10:52:43 +00:00
}
2023-03-13 13:35:34 +00:00
2025-12-07 20:29:45 +00:00
// TODO: (@Meldiron) Remove after 1.7.x migration
2026-01-14 15:08:00 +00:00
if ( System :: getEnv ( '_APP_RULES_FORMAT' ) === 'md5' ) {
$rule = $authorization -> skip ( fn () => $dbForPlatform -> getDocument ( 'rules' , md5 ( $host )));
} else {
$rule = $authorization -> skip (
fn () => $dbForPlatform -> find ( 'rules' , [
Query :: equal ( 'domain' , [ $host ]),
Query :: limit ( 1 )
])
)[ 0 ] ? ? new Document ();
}
2023-02-27 12:43:20 +00:00
2025-04-09 04:43:54 +00:00
$errorView = __DIR__ . '/../views/general/error.phtml' ;
2025-12-07 20:29:45 +00:00
$protocol = System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' ) == 'disabled' ? 'http' : 'https' ;
2025-12-11 18:36:11 +00:00
$url = $protocol . '://' . $platform [ 'consoleHostname' ];
$platformHostnames = $platform [ 'hostnames' ] ? ? [];
2025-04-11 14:52:19 +00:00
2024-11-13 07:48:21 +00:00
if ( $rule -> isEmpty ()) {
2026-01-30 15:20:46 +00:00
$denyDomains = [];
$denyEnvVars = [
System :: getEnv ( '_APP_DOMAIN_FUNCTIONS_FALLBACK' , '' ),
System :: getEnv ( '_APP_DOMAIN_FUNCTIONS' , '' ),
System :: getEnv ( '_APP_DOMAIN_SITES' , '' ),
];
foreach ( $denyEnvVars as $denyEnvVar ) {
foreach ( \explode ( ',' , $denyEnvVar ) as $denyDomain ) {
if ( empty ( $denyDomain )) {
continue ;
}
$denyDomains [] = $denyDomain ;
}
2023-10-02 14:02:48 +00:00
}
2026-01-30 15:20:46 +00:00
foreach ( $denyDomains as $denyDomain ) {
if ( $host === $denyDomain ) {
throw new AppwriteException ( AppwriteException :: GENERAL_ACCESS_FORBIDDEN , 'This domain cannot be used for security reasons. Please use any subdomain instead.' , view : $errorView );
}
2024-04-22 15:58:40 +00:00
2026-01-30 15:20:46 +00:00
if ( \str_ends_with ( $host , $denyDomain )) {
$exception = new AppwriteException ( AppwriteException :: RULE_NOT_FOUND , 'This domain is not connected to any Appwrite resources. Visit domains tab under function/site settings to configure it.' , view : $errorView );
2025-04-09 04:43:54 +00:00
2026-01-30 15:20:46 +00:00
$exception -> addCTA ( 'Start with this domain' , $url . '/console' );
throw $exception ;
}
2023-10-02 14:02:48 +00:00
}
2025-12-11 18:36:11 +00:00
if ( ! in_array ( $host , $platformHostnames )) {
2025-12-07 20:29:45 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_ACCESS_FORBIDDEN , 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.' , view : $errorView );
2023-10-02 14:02:48 +00:00
}
2023-09-05 15:02:07 +00:00
// Act as API - no Proxy logic
return false ;
2023-02-27 12:43:20 +00:00
}
2024-10-24 17:20:00 +00:00
$projectId = $rule -> getAttribute ( 'projectId' );
2026-01-14 15:08:00 +00:00
$project = $authorization -> skip (
2024-12-12 10:30:26 +00:00
fn () => $dbForPlatform -> getDocument ( 'projects' , $projectId )
2024-04-22 15:58:40 +00:00
);
2024-12-10 11:34:51 +00:00
if ( ! $project -> isEmpty () && $project -> getId () !== 'console' ) {
2025-04-17 14:13:09 +00:00
$accessedAt = $project -> getAttribute ( 'accessedAt' , 0 );
2024-12-10 11:34:51 +00:00
if ( DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_PROJECT_ACCESS )) > $accessedAt ) {
$project -> setAttribute ( 'accessedAt' , DateTime :: now ());
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForPlatform -> updateDocument ( 'projects' , $project -> getId (), $project ));
2024-12-10 11:34:51 +00:00
}
2025-06-19 14:15:43 +00:00
2025-06-15 08:30:57 +00:00
/**
2025-06-19 14:15:43 +00:00
* Set projectId to update the Error hook logger , since x - appwrite - project is not available when executing custom domain function
2025-06-15 08:30:57 +00:00
*/
2025-06-19 14:15:43 +00:00
$log -> addTag ( 'projectId' , $project -> getId ());
2024-12-10 11:34:51 +00:00
}
2023-03-14 19:31:23 +00:00
if ( array_key_exists ( 'proxy' , $project -> getAttribute ( 'services' , []))) {
2023-03-10 12:20:24 +00:00
$status = $project -> getAttribute ( 'services' , [])[ 'proxy' ];
2023-03-14 19:31:23 +00:00
if ( ! $status ) {
2025-04-09 04:43:54 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_SERVICE_DISABLED , view : $errorView );
2023-03-10 12:20:24 +00:00
}
}
2023-03-13 13:35:34 +00:00
// Skip Appwrite Router for ACME challenge. Nessessary for certificate generation
2024-10-08 07:54:40 +00:00
$path = ( $swooleRequest -> server [ 'request_uri' ] ? ? '/' );
2023-03-14 19:31:23 +00:00
if ( \str_starts_with ( $path , '/.well-known/acme-challenge' )) {
2023-03-13 13:35:34 +00:00
return false ;
}
2025-02-23 20:34:14 +00:00
$type = $rule -> getAttribute ( 'type' , '' );
2023-02-27 12:43:20 +00:00
2025-02-23 20:34:14 +00:00
if ( $type === 'deployment' ) {
2025-04-16 08:43:02 +00:00
if ( System :: getEnv ( '_APP_OPTIONS_ROUTER_FORCE_HTTPS' , 'disabled' ) === 'enabled' ) { // Force HTTPS
2025-12-07 20:29:45 +00:00
if ( $request -> getProtocol () !== 'https' ) {
2023-10-02 14:02:48 +00:00
if ( $request -> getMethod () !== Request :: METHOD_GET ) {
2025-04-09 04:43:54 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_PROTOCOL_UNSUPPORTED , 'Method unsupported over HTTP. Please use HTTPS instead.' , view : $errorView );
2023-10-02 14:02:48 +00:00
}
return $response -> redirect ( 'https://' . $request -> getHostname () . $request -> getURI ());
}
}
2025-02-23 20:34:14 +00:00
/** @var Database $dbForProject */
$dbForProject = $getProjectDB ( $project );
2025-04-03 13:19:41 +00:00
/** @var Document $deployment */
2025-04-29 09:56:51 +00:00
if ( ! empty ( $rule -> getAttribute ( 'deploymentId' , '' ))) {
2026-01-14 15:08:00 +00:00
$deployment = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'deployments' , $rule -> getAttribute ( 'deploymentId' )));
2025-04-29 09:31:06 +00:00
} else {
// 1.6.x DB schema compatibility
// TODO: Make sure deploymentId is never empty, and remove this code
2025-04-29 09:56:51 +00:00
// Check if site or function; should never be site, but better safe than sorry
// Attempts to use attribute from both schemas (1.6 and 1.7)
$resourceType = $rule -> getAttribute ( 'deploymentResourceType' , $rule -> getAttribute ( 'resourceType' , '' ));
// ID of site or function
$resourceId = $rule -> getAttribute ( 'deploymentResourceId' , '' );
// Document of site or function
$resource = $resourceType === 'function' ?
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> getDocument ( 'functions' , $resourceId )) :
$authorization -> skip ( fn () => $dbForProject -> getDocument ( 'sites' , $resourceId ));
2025-04-29 09:56:51 +00:00
// ID of active deployments
// Attempts to use attribute from both schemas (1.6 and 1.7)
$activeDeploymentId = $resource -> getAttribute ( 'deploymentId' , $resource -> getAttribute ( 'deployment' , '' ));
// Get deployment document, as intended originally
2026-01-14 15:08:00 +00:00
$deployment = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'deployments' , $activeDeploymentId ));
2025-04-29 09:56:51 +00:00
}
2025-02-23 20:34:14 +00:00
2025-02-24 10:55:59 +00:00
if ( $deployment -> getAttribute ( 'resourceType' , '' ) === 'functions' ) {
2025-02-23 20:34:14 +00:00
$type = 'function' ;
2025-02-24 10:55:59 +00:00
} elseif ( $deployment -> getAttribute ( 'resourceType' , '' ) === 'sites' ) {
2025-02-23 20:34:14 +00:00
$type = 'site' ;
}
2025-04-03 13:19:41 +00:00
if ( $deployment -> isEmpty ()) {
2025-04-09 17:50:06 +00:00
$resourceType = $rule -> getAttribute ( 'deploymentResourceType' , '' );
$resourceId = $rule -> getAttribute ( 'deploymentResourceId' , '' );
$type = ( $resourceType === 'site' ) ? 'sites' : 'functions' ;
2025-04-09 04:43:54 +00:00
$exception = new AppwriteException ( AppwriteException :: DEPLOYMENT_NOT_FOUND , view : $errorView );
2025-06-12 06:13:11 +00:00
$exception -> addCTA ( 'View deployments' , $url . '/console/project-' . $project -> getAttribute ( 'region' , 'default' ) . '-' . $projectId . '/' . $type . '/' . $resourceType . '-' . $resourceId );
2025-04-09 04:43:54 +00:00
throw $exception ;
2025-04-03 13:19:41 +00:00
}
2025-02-23 20:34:14 +00:00
$resource = $type === 'function' ?
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> getDocument ( 'functions' , $deployment -> getAttribute ( 'resourceId' , '' ))) :
$authorization -> skip ( fn () => $dbForProject -> getDocument ( 'sites' , $deployment -> getAttribute ( 'resourceId' , '' )));
2025-02-23 20:34:14 +00:00
2025-03-13 09:33:28 +00:00
$isPreview = $type === 'function' ? false : ( $rule -> getAttribute ( 'trigger' , '' ) !== 'manual' );
2023-02-27 12:43:20 +00:00
2024-10-08 07:54:40 +00:00
$path = ( $swooleRequest -> server [ 'request_uri' ] ? ? '/' );
$query = ( $swooleRequest -> server [ 'query_string' ] ? ? '' );
2023-03-14 19:31:23 +00:00
if ( ! empty ( $query )) {
2023-02-27 12:43:20 +00:00
$path .= '?' . $query ;
}
2025-02-10 14:07:01 +00:00
$protocol = $request -> getProtocol ();
2025-03-08 17:45:38 +00:00
/**
Ensure preview authorization
- Authorization is skippable for tests , and build screenshot
- If cookie is not sent by client -> not authorized
- If JWT in cookie is invalid or expired -> not authorized
- If user is blocked or removed -> not authorized
- If user ' s session is removed or expired -> not authorized
- If user is not member of team of this deployment -> not authorized
- If not authorized , redirect to Console redirect UI
- If authorized , continue as if auth was not required
*/
2025-03-08 11:57:55 +00:00
$requirePreview = \is_null ( $apiKey ) || ! $apiKey -> isPreviewAuthDisabled ();
if ( $isPreview && $requirePreview ) {
2025-11-05 07:09:18 +00:00
$cookie = $request -> getCookie ( COOKIE_NAME_PREVIEW , '' );
2025-03-08 11:57:55 +00:00
$authorized = false ;
2025-02-10 14:07:01 +00:00
2025-03-08 11:57:55 +00:00
// Security checks to mark authorized true
if ( ! empty ( $cookie )) {
2025-02-10 14:07:01 +00:00
$jwt = new JWT ( System :: getEnv ( '_APP_OPENSSL_KEY_V1' ), 'HS256' , 3600 , 0 );
$payload = [];
try {
$payload = $jwt -> decode ( $cookie );
} catch ( JWTException $error ) {
2025-03-08 11:57:55 +00:00
// Authorized remains false
2025-02-10 14:07:01 +00:00
}
2025-03-08 11:57:55 +00:00
$userExists = false ;
$userId = $payload [ 'userId' ] ? ? '' ;
if ( ! empty ( $userId )) {
2026-01-14 15:08:00 +00:00
$user = $authorization -> skip ( fn () => $dbForPlatform -> getDocument ( 'users' , $userId ));
2025-03-08 11:57:55 +00:00
if ( ! $user -> isEmpty () && $user -> getAttribute ( 'status' , false )) {
$userExists = true ;
2025-02-10 14:07:01 +00:00
}
}
2025-03-08 11:57:55 +00:00
$sessionExists = false ;
2025-02-10 14:07:01 +00:00
$jwtSessionId = $payload [ 'sessionId' ] ? ? '' ;
2025-03-08 11:57:55 +00:00
if ( ! empty ( $jwtSessionId ) && ! empty ( $user -> find ( '$id' , $jwtSessionId , 'sessions' ))) {
$sessionExists = true ;
}
$membershipExists = false ;
2026-01-14 15:08:00 +00:00
$project = $authorization -> skip ( fn () => $dbForPlatform -> getDocument ( 'projects' , $projectId ));
2025-03-09 17:51:49 +00:00
if ( ! $project -> isEmpty () && isset ( $user )) {
2025-03-08 11:57:55 +00:00
$teamId = $project -> getAttribute ( 'teamId' , '' );
$membership = $user -> find ( 'teamId' , $teamId , 'memberships' );
if ( ! empty ( $membership )) {
$membershipExists = true ;
2025-02-10 14:07:01 +00:00
}
}
2025-03-08 11:57:55 +00:00
if ( $userExists && $sessionExists && $membershipExists ) {
$authorized = true ;
}
2025-02-10 14:07:01 +00:00
}
2025-03-08 11:57:55 +00:00
if ( ! $authorized ) {
2025-12-11 18:36:11 +00:00
$url = $protocol . " :// " . $platform [ 'consoleHostname' ];
2025-02-10 14:07:01 +00:00
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
-> redirect ( $url . '/console/auth/preview?'
. \http_build_query ([
'projectId' => $projectId ,
'origin' => $protocol . '://' . $host ,
'path' => $path
]));
return true ;
}
}
2024-10-08 07:54:40 +00:00
$body = $swooleRequest -> getContent () ? ? '' ;
$method = $swooleRequest -> server [ 'request_method' ];
2024-02-22 09:40:18 +00:00
2023-10-27 15:25:19 +00:00
$requestHeaders = $request -> getHeaders ();
2023-10-27 08:26:46 +00:00
2024-10-24 17:20:00 +00:00
if ( $resource -> isEmpty () || ! $resource -> getAttribute ( 'enabled' )) {
2025-04-03 13:19:41 +00:00
if ( $type === 'functions' ) {
2025-04-09 04:43:54 +00:00
throw new AppwriteException ( AppwriteException :: FUNCTION_NOT_FOUND , view : $errorView );
2025-04-03 13:19:41 +00:00
} else {
2025-04-09 04:43:54 +00:00
throw new AppwriteException ( AppwriteException :: SITE_NOT_FOUND , view : $errorView );
2025-04-03 13:19:41 +00:00
}
2024-02-22 09:40:18 +00:00
}
2025-02-23 20:34:14 +00:00
if ( $isResourceBlocked ( $project , $type === 'function' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES , $resource -> getId ())) {
2025-04-09 04:43:54 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_RESOURCE_BLOCKED , view : $errorView );
2024-10-17 11:32:42 +00:00
}
2025-02-13 18:57:41 +00:00
$version = match ( $type ) {
2024-10-26 16:43:30 +00:00
'function' => $resource -> getAttribute ( 'version' , 'v2' ),
2025-03-11 17:19:25 +00:00
'site' => 'v5' ,
2024-10-26 16:43:30 +00:00
};
2024-02-22 09:40:18 +00:00
$runtimes = Config :: getParam ( $version === 'v2' ? 'runtimes-v2' : 'runtimes' , []);
2025-03-07 21:36:13 +00:00
$spec = Config :: getParam ( 'specifications' )[ $resource -> getAttribute ( 'specification' , APP_COMPUTE_SPECIFICATION_DEFAULT )];
2024-02-22 09:40:18 +00:00
2025-02-13 18:57:41 +00:00
$runtime = match ( $type ) {
2024-10-26 16:43:30 +00:00
'function' => $runtimes [ $resource -> getAttribute ( 'runtime' )] ? ? null ,
2024-12-02 12:20:54 +00:00
'site' => $runtimes [ $resource -> getAttribute ( 'buildRuntime' )] ? ? null ,
2024-10-24 17:20:00 +00:00
default => null
};
2024-02-22 09:40:18 +00:00
2025-02-28 09:40:28 +00:00
// Static site enforced runtime
2025-03-24 14:34:02 +00:00
if ( $deployment -> getAttribute ( 'adapter' , '' ) === 'static' ) {
2025-02-28 09:40:28 +00:00
$runtime = $runtimes [ 'static-1' ] ? ? null ;
}
2024-10-24 17:20:00 +00:00
if ( \is_null ( $runtime )) {
2025-04-09 04:43:54 +00:00
throw new AppwriteException ( AppwriteException :: FUNCTION_RUNTIME_UNSUPPORTED , 'Runtime "' . $resource -> getAttribute ( 'runtime' , '' ) . '" is not supported' , view : $errorView );
2024-10-23 16:36:26 +00:00
}
2024-10-24 17:20:00 +00:00
2025-03-26 11:59:05 +00:00
$allowAnyStatus = ! \is_null ( $apiKey ) && $apiKey -> isDeploymentStatusIgnored ();
if ( ! $allowAnyStatus && $deployment -> getAttribute ( 'status' ) !== 'ready' ) {
2025-04-09 04:43:54 +00:00
$status = $deployment -> getAttribute ( 'status' );
2025-06-12 06:13:11 +00:00
$region = $project -> getAttribute ( 'region' , 'default' );
2025-04-09 04:43:54 +00:00
switch ( $status ) {
case 'failed' :
$exception = new AppwriteException ( AppwriteException :: BUILD_FAILED , view : $errorView );
2025-06-12 06:13:11 +00:00
$ctaUrl = '/console/project-' . $region . '-' . $project -> getId () . '/sites/site-' . $resource -> getId () . '/deployments/deployment-' . $deployment -> getId ();
2025-04-09 04:43:54 +00:00
$exception -> addCTA ( 'View logs' , $url . $ctaUrl );
break ;
case 'canceled' :
$exception = new AppwriteException ( AppwriteException :: BUILD_CANCELED , view : $errorView );
2025-06-12 06:13:11 +00:00
$ctaUrl = '/console/project-' . $region . '-' . $project -> getId () . '/sites/site-' . $resource -> getId () . '/deployments' ;
2025-04-09 04:43:54 +00:00
$exception -> addCTA ( 'View deployments' , $url . $ctaUrl );
break ;
default :
$exception = new AppwriteException ( AppwriteException :: BUILD_NOT_READY , view : $errorView );
2025-06-12 06:13:11 +00:00
$ctaUrl = '/console/project-' . $region . '-' . $project -> getId () . '/sites/site-' . $resource -> getId () . '/deployments/deployment-' . $deployment -> getId ();
2025-04-09 04:43:54 +00:00
$exception -> addCTA ( 'Reload' , '/' );
$exception -> addCTA ( 'View logs' , $url . $ctaUrl );
break ;
2025-03-26 11:22:32 +00:00
}
2025-04-09 04:43:54 +00:00
throw $exception ;
2024-02-22 09:40:18 +00:00
}
2024-10-24 17:20:00 +00:00
if ( $type === 'function' ) {
$permissions = $resource -> getAttribute ( 'execute' );
2024-10-23 16:36:26 +00:00
if ( ! ( \in_array ( 'any' , $permissions )) && ! ( \in_array ( 'guests' , $permissions ))) {
2025-04-15 12:16:04 +00:00
$exception = new AppwriteException ( AppwriteException :: FUNCTION_EXECUTE_PERMISSION_MISSING , view : $errorView );
2025-06-12 06:13:11 +00:00
$exception -> addCTA ( 'View settings' , $url . '/console/project-' . $project -> getAttribute ( 'region' , 'default' ) . '-' . $project -> getId () . '/functions/function-' . $resource -> getId () . '/settings' );
2025-04-15 12:16:04 +00:00
throw $exception ;
2024-10-23 16:36:26 +00:00
}
2024-02-22 09:40:18 +00:00
}
2025-08-20 11:05:40 +00:00
$executionId = ID :: unique ();
2024-02-22 09:40:18 +00:00
$headers = \array_merge ([], $requestHeaders );
2025-08-20 11:05:40 +00:00
$headers [ 'x-appwrite-execution-id' ] = $executionId ? ? '' ;
2024-02-22 09:40:18 +00:00
$headers [ 'x-appwrite-user-id' ] = '' ;
$headers [ 'x-appwrite-country-code' ] = '' ;
$headers [ 'x-appwrite-continent-code' ] = '' ;
$headers [ 'x-appwrite-continent-eu' ] = 'false' ;
2025-08-26 08:29:46 +00:00
$ip = $request -> getIP ();
$headers [ 'x-appwrite-client-ip' ] = $ip ;
2024-02-22 09:40:18 +00:00
2025-08-15 10:29:34 +00:00
$jwtExpiry = $resource -> getAttribute ( 'timeout' , 900 ) + 60 ; // 1min extra to account for possible cold-starts
2025-02-23 20:34:14 +00:00
$jwtObj = new JWT ( System :: getEnv ( '_APP_OPENSSL_KEY_V1' ), 'HS256' , $jwtExpiry , 0 );
$jwtKey = $jwtObj -> encode ([
'projectId' => $project -> getId (),
'scopes' => $resource -> getAttribute ( 'scopes' , [])
]);
$headers [ 'x-appwrite-key' ] = API_KEY_DYNAMIC . '_' . $jwtKey ;
$headers [ 'x-appwrite-trigger' ] = 'http' ;
$headers [ 'x-appwrite-user-jwt' ] = '' ;
2024-10-24 17:20:00 +00:00
2024-02-22 09:40:18 +00:00
if ( ! empty ( $ip )) {
$record = $geodb -> get ( $ip );
if ( $record ) {
$eu = Config :: getParam ( 'locale-eu' );
$headers [ 'x-appwrite-country-code' ] = $record [ 'country' ][ 'iso_code' ] ? ? '' ;
$headers [ 'x-appwrite-continent-code' ] = $record [ 'continent' ][ 'code' ] ? ? '' ;
$headers [ 'x-appwrite-continent-eu' ] = ( \in_array ( $record [ 'country' ][ 'iso_code' ], $eu )) ? 'true' : 'false' ;
}
}
$headersFiltered = [];
foreach ( $headers as $key => $value ) {
if ( \in_array ( \strtolower ( $key ), FUNCTION_ALLOWLIST_HEADERS_REQUEST )) {
$headersFiltered [] = [ 'name' => $key , 'value' => $value ];
}
}
$execution = new Document ([
'$id' => $executionId ,
'$permissions' => [],
2025-05-27 01:36:23 +00:00
'resourceInternalId' => $resource -> getSequence (),
2024-12-02 13:00:06 +00:00
'resourceId' => $resource -> getId (),
2025-05-26 05:42:11 +00:00
'deploymentInternalId' => $deployment -> getSequence (),
2024-02-22 09:40:18 +00:00
'deploymentId' => $deployment -> getId (),
'responseStatusCode' => 0 ,
'responseHeaders' => [],
'requestPath' => $path ,
'requestMethod' => $method ,
'requestHeaders' => $headersFiltered ,
'errors' => '' ,
'logs' => '' ,
'duration' => 0.0 ,
2023-02-27 12:43:20 +00:00
]);
2024-12-02 13:00:06 +00:00
if ( $type === 'function' ) {
$execution -> setAttribute ( 'resourceType' , 'functions' );
$execution -> setAttribute ( 'trigger' , 'http' ); // http / schedule / event
$execution -> setAttribute ( 'status' , 'processing' ); // waiting / processing / completed / failed
2025-02-23 20:34:14 +00:00
$queueForEvents
2025-02-25 11:53:12 +00:00
-> setParam ( 'functionId' , $resource -> getId ())
-> setParam ( 'executionId' , $execution -> getId ())
-> setContext ( 'function' , $resource );
2024-12-02 13:00:06 +00:00
} elseif ( $type === 'site' ) {
$execution -> setAttribute ( 'resourceType' , 'sites' );
2025-02-23 20:34:14 +00:00
$queueForEvents
-> setParam ( 'siteId' , $resource -> getId ())
-> setParam ( 'executionId' , $execution -> getId ())
-> setContext ( 'site' , $resource );
}
2023-02-27 12:43:20 +00:00
2024-02-22 09:40:18 +00:00
$durationStart = \microtime ( true );
2023-02-27 12:43:20 +00:00
2024-02-22 09:40:18 +00:00
$vars = [];
2023-02-27 12:43:20 +00:00
2024-02-22 09:40:18 +00:00
// V2 vars
if ( $version === 'v2' ) {
$vars = \array_merge ( $vars , [
'APPWRITE_FUNCTION_TRIGGER' => $headers [ 'x-appwrite-trigger' ] ? ? '' ,
'APPWRITE_FUNCTION_DATA' => $body ? ? '' ,
'APPWRITE_FUNCTION_USER_ID' => $headers [ 'x-appwrite-user-id' ] ? ? '' ,
'APPWRITE_FUNCTION_JWT' => $headers [ 'x-appwrite-user-jwt' ] ? ? ''
]);
}
2023-02-27 12:43:20 +00:00
2024-02-22 09:40:18 +00:00
// Shared vars
2024-10-24 17:20:00 +00:00
foreach ( $resource -> getAttribute ( 'varsProject' , []) as $var ) {
2024-02-22 09:40:18 +00:00
$vars [ $var -> getAttribute ( 'key' )] = $var -> getAttribute ( 'value' , '' );
2023-02-27 12:43:20 +00:00
}
2024-02-22 09:40:18 +00:00
// Function vars
2024-10-24 17:20:00 +00:00
foreach ( $resource -> getAttribute ( 'vars' , []) as $var ) {
2024-02-22 09:40:18 +00:00
$vars [ $var -> getAttribute ( 'key' )] = $var -> getAttribute ( 'value' , '' );
2023-02-27 12:43:20 +00:00
}
2025-12-16 22:35:39 +00:00
$protocol = System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' ) == 'disabled' ? 'http' : 'https' ;
$endpoint = " $protocol :// { $platform [ 'apiHostname' ] } /v1 " ;
2024-07-23 12:28:13 +00:00
2024-02-22 09:40:18 +00:00
// Appwrite vars
2025-05-18 21:53:56 +00:00
if ( $type === 'function' ) {
2025-05-18 21:53:43 +00:00
$vars = \array_merge ( $vars , [
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint ,
'APPWRITE_FUNCTION_ID' => $resource -> getId (),
'APPWRITE_FUNCTION_NAME' => $resource -> getAttribute ( 'name' ),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment -> getId (),
'APPWRITE_FUNCTION_PROJECT_ID' => $project -> getId (),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime [ 'name' ] ? ? '' ,
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime [ 'version' ] ? ? '' ,
'APPWRITE_FUNCTION_CPUS' => $spec [ 'cpus' ] ? ? APP_COMPUTE_CPUS_DEFAULT ,
'APPWRITE_FUNCTION_MEMORY' => $spec [ 'memory' ] ? ? APP_COMPUTE_MEMORY_DEFAULT ,
]);
2025-05-18 21:53:56 +00:00
} elseif ( $type === 'site' ) {
2025-05-18 21:53:43 +00:00
$vars = \array_merge ( $vars , [
'APPWRITE_SITE_API_ENDPOINT' => $endpoint ,
'APPWRITE_SITE_ID' => $resource -> getId (),
'APPWRITE_SITE_NAME' => $resource -> getAttribute ( 'name' ),
'APPWRITE_SITE_DEPLOYMENT' => $deployment -> getId (),
'APPWRITE_SITE_PROJECT_ID' => $project -> getId (),
'APPWRITE_SITE_RUNTIME_NAME' => $runtime [ 'name' ] ? ? '' ,
'APPWRITE_SITE_RUNTIME_VERSION' => $runtime [ 'version' ] ? ? '' ,
'APPWRITE_SITE_CPUS' => $spec [ 'cpus' ] ? ? APP_COMPUTE_CPUS_DEFAULT ,
'APPWRITE_SITE_MEMORY' => $spec [ 'memory' ] ? ? APP_COMPUTE_MEMORY_DEFAULT ,
]);
}
2025-05-18 21:53:56 +00:00
2024-02-22 09:40:18 +00:00
$vars = \array_merge ( $vars , [
2024-07-15 07:10:11 +00:00
'APPWRITE_VERSION' => APP_VERSION_STABLE ,
'APPWRITE_REGION' => $project -> getAttribute ( 'region' ),
2024-09-06 10:50:29 +00:00
'APPWRITE_DEPLOYMENT_TYPE' => $deployment -> getAttribute ( 'type' , '' ),
'APPWRITE_VCS_REPOSITORY_ID' => $deployment -> getAttribute ( 'providerRepositoryId' , '' ),
'APPWRITE_VCS_REPOSITORY_NAME' => $deployment -> getAttribute ( 'providerRepositoryName' , '' ),
'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment -> getAttribute ( 'providerRepositoryOwner' , '' ),
'APPWRITE_VCS_REPOSITORY_URL' => $deployment -> getAttribute ( 'providerRepositoryUrl' , '' ),
'APPWRITE_VCS_REPOSITORY_BRANCH' => $deployment -> getAttribute ( 'providerBranch' , '' ),
'APPWRITE_VCS_REPOSITORY_BRANCH_URL' => $deployment -> getAttribute ( 'providerBranchUrl' , '' ),
'APPWRITE_VCS_COMMIT_HASH' => $deployment -> getAttribute ( 'providerCommitHash' , '' ),
'APPWRITE_VCS_COMMIT_MESSAGE' => $deployment -> getAttribute ( 'providerCommitMessage' , '' ),
'APPWRITE_VCS_COMMIT_URL' => $deployment -> getAttribute ( 'providerCommitUrl' , '' ),
'APPWRITE_VCS_COMMIT_AUTHOR_NAME' => $deployment -> getAttribute ( 'providerCommitAuthor' , '' ),
'APPWRITE_VCS_COMMIT_AUTHOR_URL' => $deployment -> getAttribute ( 'providerCommitAuthorUrl' , '' ),
'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment -> getAttribute ( 'providerRootDirectory' , '' ),
2024-02-22 09:40:18 +00:00
]);
2023-02-27 12:43:20 +00:00
2025-02-08 09:36:11 +00:00
// SPA fallbackFile override
2025-03-24 14:34:02 +00:00
if ( $deployment -> getAttribute ( 'adapter' , '' ) === 'static' && $deployment -> getAttribute ( 'fallbackFile' , '' ) !== '' ) {
$vars [ 'OPEN_RUNTIMES_STATIC_FALLBACK' ] = $deployment -> getAttribute ( 'fallbackFile' , '' );
2025-02-08 09:36:11 +00:00
}
2024-02-22 09:40:18 +00:00
/** Execute function */
try {
2025-02-13 18:57:41 +00:00
$version = match ( $type ) {
2024-10-24 17:20:00 +00:00
'function' => $resource -> getAttribute ( 'version' , 'v2' ),
2025-03-11 17:19:25 +00:00
'site' => 'v5' ,
2024-10-24 17:20:00 +00:00
};
2025-02-13 18:57:41 +00:00
$entrypoint = match ( $type ) {
2024-10-24 17:20:00 +00:00
'function' => $deployment -> getAttribute ( 'entrypoint' , '' ),
2024-10-27 20:10:53 +00:00
'site' => '' ,
2024-10-24 17:20:00 +00:00
};
2025-06-13 10:15:32 +00:00
$source = $deployment -> getAttribute ( 'buildPath' , '' );
$extension = str_ends_with ( $source , '.tar' ) ? 'tar' : 'tar.gz' ;
2024-11-27 15:27:40 +00:00
2025-06-13 10:15:32 +00:00
$startCommand = $runtime [ 'startCommand' ];
if ( $type === 'site' ) {
2024-11-27 15:27:40 +00:00
$frameworks = Config :: getParam ( 'frameworks' , []);
$framework = $frameworks [ $resource -> getAttribute ( 'framework' , '' )] ? ? null ;
2024-12-02 12:45:03 +00:00
if ( ! is_null ( $framework )) {
2025-03-24 14:34:02 +00:00
$adapter = ( $framework [ 'adapters' ] ? ? [])[ $deployment -> getAttribute ( 'adapter' , '' )] ? ? null ;
2024-12-02 12:45:03 +00:00
if ( ! is_null ( $adapter ) && isset ( $adapter [ 'startCommand' ])) {
2024-12-02 12:20:54 +00:00
$startCommand = $adapter [ 'startCommand' ];
}
2024-11-27 15:27:40 +00:00
}
}
2025-06-13 10:15:32 +00:00
$runtimeEntrypoint = match ( $version ) {
'v2' => '' ,
default => " cp /tmp/code. $extension /mnt/code/code. $extension && nohup helpers/start.sh \" $startCommand\ " " ,
};
2025-02-13 18:57:41 +00:00
$entrypoint = match ( $type ) {
2024-11-27 15:27:40 +00:00
'function' => $deployment -> getAttribute ( 'entrypoint' , '' ),
'site' => '' ,
2024-10-24 17:20:00 +00:00
};
2024-02-22 09:40:18 +00:00
$executionResponse = $executor -> createExecution (
projectId : $project -> getId (),
deploymentId : $deployment -> getId (),
body : \strlen ( $body ) > 0 ? $body : null ,
variables : $vars ,
2024-10-24 17:20:00 +00:00
timeout : $resource -> getAttribute ( 'timeout' , 30 ),
2024-02-22 09:40:18 +00:00
image : $runtime [ 'image' ],
2025-06-13 10:15:32 +00:00
source : $source ,
2024-10-23 16:36:26 +00:00
entrypoint : $entrypoint ,
2024-02-22 09:40:18 +00:00
version : $version ,
path : $path ,
method : $method ,
headers : $headers ,
2024-10-23 16:36:26 +00:00
runtimeEntrypoint : $runtimeEntrypoint ,
2024-10-27 16:22:45 +00:00
cpus : $spec [ 'cpus' ] ? ? APP_COMPUTE_CPUS_DEFAULT ,
memory : $spec [ 'memory' ] ? ? APP_COMPUTE_MEMORY_DEFAULT ,
2024-10-24 17:20:00 +00:00
logging : $resource -> getAttribute ( 'logging' , true ),
2025-09-03 15:32:15 +00:00
requestTimeout : 30 ,
responseFormat : Executor :: RESPONSE_FORMAT_ARRAY_HEADERS
2024-02-22 09:40:18 +00:00
);
2025-09-03 15:32:15 +00:00
$headerOverrides = [];
2025-02-08 09:36:11 +00:00
// Branded 404 override
2025-03-31 16:51:33 +00:00
$isResponseBranded = false ;
2025-03-24 14:34:02 +00:00
if ( $executionResponse [ 'statusCode' ] === 404 && $deployment -> getAttribute ( 'adapter' , '' ) === 'static' ) {
2025-02-08 09:36:11 +00:00
$layout = new View ( __DIR__ . '/../views/general/404.phtml' );
$executionResponse [ 'body' ] = $layout -> render ();
2025-09-03 15:32:15 +00:00
$headerOverrides [ 'content-length' ] = \strlen ( $executionResponse [ 'body' ]);
2025-03-31 16:51:33 +00:00
$isResponseBranded = true ;
2025-02-08 09:36:11 +00:00
}
2025-02-08 14:53:55 +00:00
// Branded banner for previews
2025-03-31 16:51:33 +00:00
if ( ! $isResponseBranded ) {
if ( \is_null ( $apiKey ) || $apiKey -> isBannerDisabled () === false ) {
$transformation = new Transformation ();
$transformation -> addAdapter ( new Preview ());
$transformation -> setInput ( $executionResponse [ 'body' ]);
2025-09-03 15:32:15 +00:00
$simpleHeaders = [];
foreach ( $executionResponse [ 'headers' ] as $key => $value ) {
$simpleHeaders [ $key ] = \is_array ( $value ) ? \implode ( ', ' , $value ) : $value ;
}
$transformation -> setTraits ( $simpleHeaders );
2025-03-31 16:51:33 +00:00
if ( $isPreview && $transformation -> transform ()) {
$executionResponse [ 'body' ] = $transformation -> getOutput ();
2025-09-03 15:32:15 +00:00
$headerOverrides [ 'content-length' ] = \strlen ( $executionResponse [ 'body' ]);
2025-02-08 14:53:55 +00:00
}
}
}
2025-04-15 09:40:32 +00:00
// Branded error pages (when developer left body empty)
if ( $executionResponse [ 'statusCode' ] >= 400 && empty ( $executionResponse [ 'body' ])) {
$layout = new View ( $errorView );
$layout
-> setParam ( 'title' , $project -> getAttribute ( 'name' ) . ' - Error' )
2025-04-15 13:17:43 +00:00
-> setParam ( 'type' , 'proxy_error_override' )
2025-04-15 09:40:32 +00:00
-> setParam ( 'code' , $executionResponse [ 'statusCode' ]);
$executionResponse [ 'body' ] = $layout -> render ();
2025-09-03 15:32:15 +00:00
$headerOverrides [ 'content-length' ] = \strlen ( $executionResponse [ 'body' ]);
$headerOverrides [ 'content-type' ] = 'text/html' ;
2025-04-15 09:40:32 +00:00
}
2025-02-08 14:53:55 +00:00
2025-08-26 13:04:43 +00:00
if ( $deployment -> getAttribute ( 'resourceType' ) === 'functions' ) {
2025-09-03 15:32:15 +00:00
$headerOverrides [ 'x-appwrite-execution-id' ] = $execution -> getId ();
2025-08-26 13:04:43 +00:00
} elseif ( $deployment -> getAttribute ( 'resourceType' ) === 'sites' ) {
2025-09-03 15:32:15 +00:00
$headerOverrides [ 'x-appwrite-log-id' ] = $execution -> getId ();
}
2026-02-05 12:40:59 +00:00
// Headers that must have single values (RFC 7230)
$singleValueHeaders = [ 'content-length' , 'content-type' ];
2025-09-03 15:32:15 +00:00
foreach ( $headerOverrides as $key => $value ) {
2026-02-05 12:40:59 +00:00
$keyLower = \strtolower ( $key );
if ( \in_array ( $keyLower , $singleValueHeaders )) {
// Single-value headers must replace, not append
$executionResponse [ 'headers' ][ $key ] = $value ;
} elseif ( \array_key_exists ( $key , $executionResponse [ 'headers' ])) {
2025-09-03 15:32:15 +00:00
if ( \is_array ( $executionResponse [ 'headers' ][ $key ])) {
$executionResponse [ 'headers' ][ $key ][] = $value ;
} else {
$executionResponse [ 'headers' ][ $key ] = [ $executionResponse [ 'headers' ][ $key ], $value ];
2025-04-15 09:40:32 +00:00
}
2025-09-03 15:32:15 +00:00
} else {
$executionResponse [ 'headers' ][ $key ] = $value ;
2025-04-15 09:40:32 +00:00
}
}
2025-02-08 14:53:55 +00:00
2024-02-22 09:40:18 +00:00
$headersFiltered = [];
foreach ( $executionResponse [ 'headers' ] as $key => $value ) {
if ( \in_array ( \strtolower ( $key ), FUNCTION_ALLOWLIST_HEADERS_RESPONSE )) {
2025-09-03 15:32:15 +00:00
$headersFiltered [] = [ 'name' => $key , 'value' => \is_array ( $value ) ? \implode ( ', ' , $value ) : $value ];
2024-02-22 09:40:18 +00:00
}
2023-08-06 13:42:18 +00:00
}
2025-08-20 13:23:55 +00:00
// Truncate logs if they exceed the limit
$maxLogLength = APP_FUNCTION_LOG_LENGTH_LIMIT ;
2025-05-19 15:25:41 +00:00
$logs = $executionResponse [ 'logs' ] ? ? '' ;
if ( \is_string ( $logs ) && \strlen ( $logs ) > $maxLogLength ) {
2025-08-21 07:19:31 +00:00
$warningMessage = " [WARNING] Logs truncated. The output exceeded { $maxLogLength } characters. \n " ;
2025-08-20 13:23:55 +00:00
$warningLength = \strlen ( $warningMessage );
2025-08-22 07:24:01 +00:00
$maxContentLength = max ( 0 , $maxLogLength - $warningLength );
$logs = $warningMessage . ( $maxContentLength > 0 ? \substr ( $logs , - $maxContentLength ) : '' );
2025-08-21 07:19:31 +00:00
}
// Truncate errors if they exceed the limit
$maxErrorLength = APP_FUNCTION_ERROR_LENGTH_LIMIT ;
$errors = $executionResponse [ 'errors' ] ? ? '' ;
if ( \is_string ( $errors ) && \strlen ( $errors ) > $maxErrorLength ) {
$warningMessage = " [WARNING] Errors truncated. The output exceeded { $maxErrorLength } characters. \n " ;
$warningLength = \strlen ( $warningMessage );
2025-08-22 07:24:01 +00:00
$maxContentLength = max ( 0 , $maxErrorLength - $warningLength );
$errors = $warningMessage . ( $maxContentLength > 0 ? \substr ( $errors , - $maxContentLength ) : '' );
2025-05-19 15:25:41 +00:00
}
2024-02-22 09:40:18 +00:00
/** Update execution status */
2024-08-08 08:38:15 +00:00
$status = $executionResponse [ 'statusCode' ] >= 500 ? 'failed' : 'completed' ;
2025-02-23 20:34:14 +00:00
$execution -> setAttribute ( 'status' , $status );
2025-05-19 15:25:41 +00:00
$execution -> setAttribute ( 'logs' , $logs );
2025-08-21 07:19:31 +00:00
$execution -> setAttribute ( 'errors' , $errors );
2024-02-22 09:40:18 +00:00
$execution -> setAttribute ( 'responseStatusCode' , $executionResponse [ 'statusCode' ]);
$execution -> setAttribute ( 'responseHeaders' , $headersFiltered );
$execution -> setAttribute ( 'duration' , $executionResponse [ 'duration' ]);
} catch ( \Throwable $th ) {
$durationEnd = \microtime ( true );
$execution
-> setAttribute ( 'duration' , $durationEnd - $durationStart )
2024-12-02 13:00:06 +00:00
-> setAttribute ( 'responseStatusCode' , 500 );
if ( $type === 'function' ) {
$execution
2025-02-13 18:57:41 +00:00
-> setAttribute ( 'status' , 'failed' )
-> setAttribute ( 'errors' , $th -> getMessage () . '\nError Code: ' . $th -> getCode ());
2024-12-02 13:00:06 +00:00
}
2024-02-22 09:40:18 +00:00
Console :: error ( $th -> getMessage ());
2024-05-20 16:16:47 +00:00
if ( $th instanceof AppwriteException ) {
throw $th ;
}
2024-02-22 09:40:18 +00:00
} finally {
2026-02-09 19:24:57 +00:00
if ( $type === 'function' || $type === 'site' ) {
$queueForExecutions
2024-10-22 15:33:33 +00:00
-> setExecution ( $execution )
-> setProject ( $project )
-> trigger ();
}
2024-02-21 14:51:21 +00:00
}
2024-02-22 09:40:18 +00:00
$execution -> setAttribute ( 'logs' , '' );
$execution -> setAttribute ( 'errors' , '' );
$headers = [];
foreach (( $executionResponse [ 'headers' ] ? ? []) as $key => $value ) {
2025-09-03 15:32:15 +00:00
$headers [] = [ 'name' => $key , 'value' => \is_array ( $value ) ? \implode ( ', ' , $value ) : $value ];
2024-02-22 09:40:18 +00:00
}
$execution -> setAttribute ( 'responseBody' , $executionResponse [ 'body' ] ? ? '' );
$execution -> setAttribute ( 'responseHeaders' , $headers );
2023-07-30 13:12:55 +00:00
$body = $execution [ 'responseBody' ] ? ? '' ;
2023-02-27 12:43:20 +00:00
2024-02-22 09:40:18 +00:00
$contentType = 'text/plain' ;
2025-09-03 15:32:15 +00:00
foreach ( $executionResponse [ 'headers' ] as $name => $values ) {
if ( \strtolower ( $name ) === 'content-type' ) {
2025-09-03 16:04:58 +00:00
$contentType = \is_array ( $values ) ? $values [ 0 ] : $values ;
2025-09-03 15:57:12 +00:00
continue ;
2024-02-22 09:40:18 +00:00
}
2025-09-03 15:32:15 +00:00
if ( \strtolower ( $name ) === 'transfer-encoding' ) {
2024-12-02 12:20:54 +00:00
continue ;
}
2025-09-07 10:13:11 +00:00
if ( \is_array ( $values )) {
2025-09-07 18:44:30 +00:00
$count = 0 ;
2025-09-07 10:13:11 +00:00
foreach ( $values as $value ) {
2025-09-07 18:44:30 +00:00
$override = $count === 0 ;
$response -> addHeader ( $name , $value , override : $override );
$count ++ ;
2025-09-07 10:13:11 +00:00
}
} else {
$response -> addHeader ( $name , $values );
}
2024-02-22 09:40:18 +00:00
}
2023-08-06 13:42:18 +00:00
$response
-> setContentType ( $contentType )
-> setStatusCode ( $execution [ 'responseStatusCode' ] ? ? 200 )
-> send ( $body );
2024-11-06 11:29:24 +00:00
$fileSize = 0 ;
$file = $request -> getFiles ( 'file' );
if ( ! empty ( $file )) {
$fileSize = ( \is_array ( $file [ 'size' ]) && isset ( $file [ 'size' ][ 0 ])) ? $file [ 'size' ][ 0 ] : $file [ 'size' ];
}
2025-04-01 07:31:53 +00:00
if ( ! empty ( $apiKey ) && ! empty ( $apiKey -> getDisabledMetrics ())) {
foreach ( $apiKey -> getDisabledMetrics () as $key ) {
$queueForStatsUsage -> disableMetric ( $key );
}
}
2025-04-02 05:05:24 +00:00
$metricTypeExecutions = str_replace ([ '{resourceType}' ], [ $deployment -> getAttribute ( 'resourceType' )], METRIC_RESOURCE_TYPE_EXECUTIONS );
2025-05-27 01:36:23 +00:00
$metricTypeIdExecutions = str_replace ([ '{resourceType}' , '{resourceInternalId}' ], [ $deployment -> getAttribute ( 'resourceType' ), $resource -> getSequence ()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS );
2025-04-02 05:05:24 +00:00
$metricTypeExecutionsCompute = str_replace ([ '{resourceType}' ], [ $deployment -> getAttribute ( 'resourceType' )], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE );
2025-05-27 01:36:23 +00:00
$metricTypeIdExecutionsCompute = str_replace ([ '{resourceType}' , '{resourceInternalId}' ], [ $deployment -> getAttribute ( 'resourceType' ), $resource -> getSequence ()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE );
2025-04-02 05:05:24 +00:00
$metricTypeExecutionsMbSeconds = str_replace ([ '{resourceType}' ], [ $deployment -> getAttribute ( 'resourceType' )], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS );
2025-05-27 01:36:23 +00:00
$metricTypeIdExecutionsMBSeconds = str_replace ([ '{resourceType}' , '{resourceInternalId}' ], [ $deployment -> getAttribute ( 'resourceType' ), $resource -> getSequence ()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS );
2025-04-02 05:05:24 +00:00
if ( $deployment -> getAttribute ( 'resourceType' ) === 'sites' ) {
$queueForStatsUsage
-> disableMetric ( METRIC_NETWORK_REQUESTS )
-> disableMetric ( METRIC_NETWORK_INBOUND )
-> disableMetric ( METRIC_NETWORK_OUTBOUND );
if ( $resource -> getAttribute ( 'adapter' ) !== 'ssr' ) {
$queueForStatsUsage
-> disableMetric ( METRIC_EXECUTIONS )
-> disableMetric ( METRIC_EXECUTIONS_COMPUTE )
-> disableMetric ( METRIC_EXECUTIONS_MB_SECONDS )
-> disableMetric ( $metricTypeExecutions )
-> disableMetric ( $metricTypeIdExecutions )
-> disableMetric ( $metricTypeExecutionsCompute )
-> disableMetric ( $metricTypeIdExecutionsCompute )
-> disableMetric ( $metricTypeExecutionsMbSeconds )
-> disableMetric ( $metricTypeIdExecutionsMBSeconds );
}
2025-04-02 05:10:12 +00:00
$queueForStatsUsage
-> addMetric ( METRIC_SITES_REQUESTS , 1 )
-> addMetric ( METRIC_SITES_INBOUND , $request -> getSize () + $fileSize )
-> addMetric ( METRIC_SITES_OUTBOUND , $response -> getSize ())
2025-05-27 01:36:23 +00:00
-> addMetric ( str_replace ( '{siteInternalId}' , $resource -> getSequence (), METRIC_SITES_ID_REQUESTS ), 1 )
-> addMetric ( str_replace ( '{siteInternalId}' , $resource -> getSequence (), METRIC_SITES_ID_INBOUND ), $request -> getSize () + $fileSize )
-> addMetric ( str_replace ( '{siteInternalId}' , $resource -> getSequence (), METRIC_SITES_ID_OUTBOUND ), $response -> getSize ())
2025-04-02 05:10:12 +00:00
;
2025-04-02 05:05:24 +00:00
}
$compute = ( int )( $execution -> getAttribute ( 'duration' ) * 1000 );
$mbSeconds = ( int )(( $spec [ 'memory' ] ? ? APP_COMPUTE_MEMORY_DEFAULT ) * $execution -> getAttribute ( 'duration' , 0 ) * ( $spec [ 'cpus' ] ? ? APP_COMPUTE_CPUS_DEFAULT ));
2025-01-30 04:53:53 +00:00
$queueForStatsUsage
2024-11-06 11:29:24 +00:00
-> addMetric ( METRIC_NETWORK_REQUESTS , 1 )
-> addMetric ( METRIC_NETWORK_INBOUND , $request -> getSize () + $fileSize )
-> addMetric ( METRIC_NETWORK_OUTBOUND , $response -> getSize ())
-> addMetric ( METRIC_EXECUTIONS , 1 )
2025-04-02 05:05:24 +00:00
-> addMetric ( $metricTypeExecutions , 1 )
-> addMetric ( $metricTypeIdExecutions , 1 )
-> addMetric ( METRIC_EXECUTIONS_COMPUTE , $compute ) // per project
-> addMetric ( $metricTypeExecutionsCompute , $compute ) // per function
-> addMetric ( $metricTypeIdExecutionsCompute , $compute ) // per function
-> addMetric ( METRIC_EXECUTIONS_MB_SECONDS , $mbSeconds )
-> addMetric ( $metricTypeExecutionsMbSeconds , $mbSeconds )
-> addMetric ( $metricTypeIdExecutionsMBSeconds , $mbSeconds )
2024-11-06 11:29:24 +00:00
-> setProject ( $project )
2025-02-13 18:57:41 +00:00
-> trigger ();
2024-11-06 11:29:24 +00:00
2026-01-16 09:36:35 +00:00
/* cleanup */
2026-01-16 10:41:38 +00:00
if ( $executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE ) {
2026-01-16 10:30:59 +00:00
$resourceType = $type === 'function'
? RESOURCE_TYPE_FUNCTIONS
: RESOURCE_TYPE_SITES ;
2026-01-16 09:36:35 +00:00
$queueForDeletes
-> setProject ( $project )
2026-01-16 10:30:59 +00:00
-> setResourceType ( $resourceType )
2026-01-16 09:36:35 +00:00
-> setResource ( $resource -> getSequence ())
-> setType ( DELETE_TYPE_EXECUTIONS_LIMIT )
-> trigger ();
}
2023-02-27 12:43:20 +00:00
return true ;
2023-03-14 19:31:23 +00:00
} elseif ( $type === 'api' ) {
2023-03-10 12:20:24 +00:00
return false ;
2025-02-23 20:34:14 +00:00
} elseif ( $type === 'redirect' ) {
2025-03-07 17:00:20 +00:00
$url = $rule -> getAttribute ( 'redirectUrl' , '' );
2025-03-07 09:14:45 +00:00
$response -> redirect ( $url , \intval ( $rule -> getAttribute ( 'redirectStatusCode' , 301 )));
2025-02-23 20:34:14 +00:00
return true ;
2023-02-27 12:43:20 +00:00
} else {
2025-04-09 04:43:54 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_SERVER_ERROR , 'Unknown resource type ' . $type , view : $errorView );
2023-02-27 12:43:20 +00:00
}
}
2026-02-04 05:30:22 +00:00
Http :: init ()
2024-04-10 17:46:31 +00:00
-> groups ([ 'api' ])
-> inject ( 'project' )
-> inject ( 'mode' )
2024-04-10 17:49:16 +00:00
-> action ( function ( Document $project , string $mode ) {
2024-04-10 17:46:31 +00:00
if ( $mode === APP_MODE_ADMIN && $project -> getId () === 'console' ) {
throw new AppwriteException ( AppwriteException :: GENERAL_BAD_REQUEST , 'Admin mode is not allowed for console project' );
}
});
2026-02-04 05:30:22 +00:00
Http :: init ()
2025-03-03 15:42:29 +00:00
-> groups ([ 'database' , 'functions' , 'sites' , 'messaging' ])
2024-04-11 07:28:16 +00:00
-> inject ( 'project' )
2024-04-17 13:17:44 +00:00
-> inject ( 'request' )
2024-04-29 08:16:09 +00:00
-> action ( function ( Document $project , Request $request ) {
2024-04-11 07:28:16 +00:00
if ( $project -> getId () === 'console' ) {
2024-04-18 06:26:11 +00:00
$message = empty ( $request -> getHeader ( 'x-appwrite-project' )) ?
2024-04-29 04:52:46 +00:00
'No Appwrite project was specified. Please specify your project ID when initializing your Appwrite SDK.' :
2024-04-18 06:26:11 +00:00
'This endpoint is not available for the console project. The Appwrite Console is a reserved project ID and cannot be used with the Appwrite SDKs and APIs. Please check if your project ID is correct.' ;
2024-04-18 08:37:20 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_ACCESS_FORBIDDEN , $message );
2024-04-11 07:28:16 +00:00
}
});
2026-02-04 05:30:22 +00:00
Http :: init ()
2023-08-06 13:42:18 +00:00
-> groups ([ 'api' , 'web' ])
2024-10-08 07:54:40 +00:00
-> inject ( 'utopia' )
-> inject ( 'swooleRequest' )
2022-07-22 06:00:42 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2025-06-19 14:15:43 +00:00
-> inject ( 'log' )
2022-07-22 06:00:42 +00:00
-> inject ( 'project' )
2024-12-12 10:30:26 +00:00
-> inject ( 'dbForPlatform' )
2024-02-22 09:40:18 +00:00
-> inject ( 'getProjectDB' )
2022-07-22 06:00:42 +00:00
-> inject ( 'locale' )
2023-04-17 02:10:17 +00:00
-> inject ( 'localeCodes' )
2024-02-22 09:40:18 +00:00
-> inject ( 'geodb' )
2025-01-30 04:53:53 +00:00
-> inject ( 'queueForStatsUsage' )
2024-03-05 10:31:15 +00:00
-> inject ( 'queueForEvents' )
2026-02-09 19:24:57 +00:00
-> inject ( 'queueForExecutions' )
2025-03-25 11:33:03 +00:00
-> inject ( 'executor' )
2025-12-07 20:29:45 +00:00
-> inject ( 'platform' )
2024-10-17 11:32:42 +00:00
-> inject ( 'isResourceBlocked' )
2024-11-06 16:05:58 +00:00
-> inject ( 'previewHostname' )
2024-11-22 05:55:39 +00:00
-> inject ( 'devKey' )
2025-02-21 19:50:43 +00:00
-> inject ( 'apiKey' )
2025-12-07 20:29:45 +00:00
-> inject ( 'cors' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2026-01-16 09:36:35 +00:00
-> inject ( 'queueForDeletes' )
-> inject ( 'executionsRetentionCount' )
2026-02-09 19:24:57 +00:00
-> action ( function ( Http $utopia , SwooleRequest $swooleRequest , Request $request , Response $response , Log $log , Document $project , Database $dbForPlatform , callable $getProjectDB , Locale $locale , array $localeCodes , Reader $geodb , StatsUsage $queueForStatsUsage , Event $queueForEvents , Execution $queueForExecutions , Executor $executor , array $platform , callable $isResourceBlocked , string $previewHostname , Document $devKey , ? Key $apiKey , Cors $cors , Authorization $authorization , DeleteEvent $queueForDeletes , int $executionsRetentionCount ) {
2023-02-09 14:27:11 +00:00
/*
* Appwrite Router
*/
2025-12-06 22:40:20 +00:00
$hostname = $request -> getHostname () ? ? '' ;
2025-12-11 18:36:11 +00:00
$platformHostnames = $platform [ 'hostnames' ] ? ? [];
2023-02-22 15:07:34 +00:00
// Only run Router when external domain
2026-01-14 15:08:00 +00:00
if ( ! \in_array ( $hostname , $platformHostnames ) || ! empty ( $previewHostname )) {
2026-02-09 19:24:57 +00:00
if ( router ( $utopia , $dbForPlatform , $getProjectDB , $swooleRequest , $request , $response , $log , $queueForEvents , $queueForStatsUsage , $queueForExecutions , $executor , $geodb , $isResourceBlocked , $platform , $previewHostname , $authorization , $apiKey , $queueForDeletes , $executionsRetentionCount )) {
2025-02-04 13:16:17 +00:00
$utopia -> getRoute () ? -> label ( 'router' , true );
2023-02-21 10:30:16 +00:00
}
}
2023-02-21 10:31:55 +00:00
2022-07-22 06:00:42 +00:00
/*
* Request format
*/
2024-10-08 07:54:40 +00:00
$route = $utopia -> getRoute ();
Request :: setRoute ( $route );
2022-07-22 06:00:42 +00:00
2023-02-21 10:31:55 +00:00
if ( $route === null ) {
2023-11-27 02:22:05 +00:00
return $response
-> setStatusCode ( 404 )
-> send ( 'Not Found' );
2023-02-14 13:58:13 +00:00
}
2024-04-01 12:41:17 +00:00
$requestFormat = $request -> getHeader ( 'x-appwrite-response-format' , System :: getEnv ( '_APP_SYSTEM_RESPONSE_FORMAT' , '' ));
2022-07-22 06:00:42 +00:00
if ( $requestFormat ) {
2024-03-07 13:48:36 +00:00
if ( version_compare ( $requestFormat , '1.4.0' , '<' )) {
2024-03-07 14:41:20 +00:00
$request -> addFilter ( new RequestV16 ());
2024-03-07 13:48:36 +00:00
}
if ( version_compare ( $requestFormat , '1.5.0' , '<' )) {
2024-03-07 14:41:20 +00:00
$request -> addFilter ( new RequestV17 ());
2022-07-22 06:00:42 +00:00
}
2024-07-17 05:52:17 +00:00
if ( version_compare ( $requestFormat , '1.6.0' , '<' )) {
$request -> addFilter ( new RequestV18 ());
}
2025-03-12 11:10:10 +00:00
if ( version_compare ( $requestFormat , '1.7.0' , '<' )) {
$request -> addFilter ( new RequestV19 ());
}
2025-06-23 05:14:25 +00:00
if ( version_compare ( $requestFormat , '1.8.0' , '<' )) {
2025-05-02 14:18:39 +00:00
$dbForProject = $getProjectDB ( $project );
2025-08-27 04:09:56 +00:00
$request -> addFilter ( new RequestV20 ( $dbForProject , $route -> getPathValues ( $request )));
2025-06-23 05:14:25 +00:00
}
2025-09-23 12:30:14 +00:00
if ( version_compare ( $requestFormat , '1.9.0' , '<' )) {
2025-09-18 06:57:24 +00:00
$request -> addFilter ( new RequestV21 ());
}
2022-07-22 06:00:42 +00:00
}
2021-05-11 10:47:02 +00:00
2024-10-08 07:54:40 +00:00
$localeParam = ( string ) $request -> getParam ( 'locale' , $request -> getHeader ( 'x-appwrite-locale' , '' ));
2023-04-17 02:10:17 +00:00
if ( \in_array ( $localeParam , $localeCodes )) {
2022-07-22 06:00:42 +00:00
$locale -> setDefault ( $localeParam );
2021-05-11 10:47:02 +00:00
}
2022-07-22 06:00:42 +00:00
2025-12-07 20:29:45 +00:00
$origin = \parse_url ( $request -> getOrigin ( $request -> getReferer ( '' )), PHP_URL_HOST );
2022-07-22 06:00:42 +00:00
$selfDomain = new Domain ( $request -> getHostname ());
$endDomain = new Domain (( string ) $origin );
Config :: setParam (
'domainVerification' ,
( $selfDomain -> getRegisterable () === $endDomain -> getRegisterable ()) &&
2024-10-08 07:54:40 +00:00
$endDomain -> getRegisterable () !== ''
2022-07-22 06:00:42 +00:00
);
2026-02-05 08:06:11 +00:00
$localHosts = [ 'localhost' , 'localhost:' . $request -> getPort ()];
2026-02-05 06:52:12 +00:00
2026-02-05 08:06:11 +00:00
$migrationHost = System :: getEnv ( '_APP_MIGRATION_HOST' );
if ( ! empty ( $migrationHost )) {
$localHosts [] = $migrationHost ;
$localHosts [] = $migrationHost . ':' . $request -> getPort ();
}
$isLocalHost = in_array ( $request -> getHostname (), $localHosts );
2023-07-21 10:08:34 +00:00
$isIpAddress = filter_var ( $request -> getHostname (), FILTER_VALIDATE_IP ) !== false ;
$isConsoleProject = $project -> getAttribute ( '$id' , '' ) === 'console' ;
2024-04-01 11:02:47 +00:00
$isConsoleRootSession = System :: getEnv ( '_APP_CONSOLE_ROOT_SESSION' , 'disabled' ) === 'enabled' ;
2023-07-21 10:08:34 +00:00
Config :: setParam (
'cookieDomain' ,
$isLocalHost || $isIpAddress
? null
2024-03-06 17:34:21 +00:00
: (
$isConsoleProject && $isConsoleRootSession
2024-10-08 07:54:40 +00:00
? '.' . $selfDomain -> getRegisterable ()
: '.' . $request -> getHostname ()
2023-07-21 10:08:34 +00:00
)
);
2022-07-22 06:00:42 +00:00
2025-06-24 09:24:15 +00:00
$warnings = [];
2022-07-22 06:00:42 +00:00
/*
2025-12-07 20:29:45 +00:00
* Response format
*/
2024-04-01 12:41:17 +00:00
$responseFormat = $request -> getHeader ( 'x-appwrite-response-format' , System :: getEnv ( '_APP_SYSTEM_RESPONSE_FORMAT' , '' ));
2022-07-22 06:00:42 +00:00
if ( $responseFormat ) {
2024-03-07 13:48:36 +00:00
if ( version_compare ( $responseFormat , '1.4.0' , '<' )) {
2024-03-07 14:41:20 +00:00
$response -> addFilter ( new ResponseV16 ());
2024-03-07 13:48:36 +00:00
}
if ( version_compare ( $responseFormat , '1.5.0' , '<' )) {
2024-03-07 14:41:20 +00:00
$response -> addFilter ( new ResponseV17 ());
2019-10-24 17:53:37 +00:00
}
2024-07-17 05:52:17 +00:00
if ( version_compare ( $responseFormat , '1.6.0' , '<' )) {
$response -> addFilter ( new ResponseV18 ());
}
2025-03-12 11:10:10 +00:00
if ( version_compare ( $responseFormat , '1.7.0' , '<' )) {
$response -> addFilter ( new ResponseV19 ());
}
2024-05-24 15:27:13 +00:00
if ( version_compare ( $responseFormat , APP_VERSION_STABLE , '>' )) {
2025-06-24 09:24:15 +00:00
$warnings [] = " The current SDK is built for Appwrite " . $responseFormat . " . However, the current Appwrite server version is " . APP_VERSION_STABLE . " . Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks " ;
2024-05-15 09:46:31 +00:00
}
2019-10-24 17:53:37 +00:00
}
2021-08-18 10:20:49 +00:00
2025-12-07 20:29:45 +00:00
// Add Appwrite warning headers
if ( ! empty ( $warnings )) {
$response -> addHeader ( 'X-Appwrite-Warning' , implode ( ';' , $warnings ));
}
2024-04-01 11:02:47 +00:00
if ( System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' , 'disabled' ) === 'enabled' ) { // Force HTTPS
2026-02-05 10:09:54 +00:00
if ( $request -> getProtocol () !== 'https' && ! in_array (( $swooleRequest -> header [ 'host' ] ? ? '' ), $localHosts )) { // localhost allowed for proxy
2022-07-22 06:00:42 +00:00
if ( $request -> getMethod () !== Request :: METHOD_GET ) {
2023-10-02 14:02:48 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_PROTOCOL_UNSUPPORTED , 'Method unsupported over HTTP. Please use HTTPS instead.' );
2022-07-22 06:00:42 +00:00
}
return $response -> redirect ( 'https://' . $request -> getHostname () . $request -> getURI ());
2022-06-03 09:12:19 +00:00
}
2022-11-21 03:49:45 +00:00
}
2022-07-22 06:00:42 +00:00
});
2022-05-31 15:41:12 +00:00
2025-12-06 22:40:20 +00:00
/**
2025-12-07 20:29:45 +00:00
* Security headers
*
* @ see https :// www . owasp . org / index . php / List_of_useful_HTTP_headers
2025-12-06 22:40:20 +00:00
*/
2026-02-04 05:30:22 +00:00
Http :: init ()
2025-12-06 22:40:20 +00:00
-> groups ([ 'api' , 'web' ])
-> inject ( 'request' )
2025-12-07 20:29:45 +00:00
-> inject ( 'response' )
-> inject ( 'cors' )
-> inject ( 'devKey' )
-> inject ( 'originValidator' )
-> action ( function ( Request $request , Response $response , Cors $cors , Document $devKey , Validator $originValidator ) {
// CORS headers
foreach ( $cors -> headers ( $request -> getOrigin ()) as $name => $value ) {
$response -> addHeader ( $name , $value );
2020-12-28 17:03:47 +00:00
}
2019-05-09 06:54:39 +00:00
2025-12-07 20:29:45 +00:00
// Security headers
2022-07-22 06:00:42 +00:00
$response
-> addHeader ( 'Server' , 'Appwrite' )
2025-12-07 20:29:45 +00:00
-> addHeader ( 'X-Content-Type-Options' , 'nosniff' );
2025-04-15 12:02:55 +00:00
2025-12-07 20:29:45 +00:00
if ( $request -> getProtocol () === 'https' ) {
$maxAge = 60 * 60 * 24 * 126 ; // 126 days
$response -> addHeader ( 'Strict-Transport-Security' , " max-age= $maxAge " );
2025-06-24 09:24:15 +00:00
}
2025-12-07 20:29:45 +00:00
// Application level CSRF protection
$origin = $request -> getOrigin ();
if ( empty ( $origin ) || ! $devKey -> isEmpty () || ! empty ( $request -> getHeader ( 'x-appwrite-key' ))) {
2025-12-06 22:40:20 +00:00
return ;
}
2025-12-07 20:29:45 +00:00
$route = $request -> getRoute ();
if ( $route -> getLabel ( 'origin' , false ) === '*' ) {
2025-12-06 22:40:20 +00:00
return ;
}
2025-12-07 20:29:45 +00:00
if ( ! $originValidator -> isValid ( $origin )) {
2022-08-14 06:56:12 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_UNKNOWN_ORIGIN , $originValidator -> getDescription ());
2021-07-29 10:28:17 +00:00
}
2022-07-22 06:00:42 +00:00
});
2022-03-01 14:19:47 +00:00
2025-12-07 20:29:45 +00:00
/**
* Automatic certificate generation
*/
2026-02-04 05:30:22 +00:00
Http :: init ()
2025-12-07 20:29:45 +00:00
-> groups ([ 'api' , 'web' ])
-> inject ( 'request' )
-> inject ( 'console' )
-> inject ( 'dbForPlatform' )
-> inject ( 'queueForCertificates' )
2025-12-11 18:36:11 +00:00
-> inject ( 'platform' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( Request $request , Document $console , Database $dbForPlatform , Certificate $queueForCertificates , array $platform , Authorization $authorization ) {
2025-12-07 20:29:45 +00:00
$hostname = $request -> getHostname ();
2025-12-11 18:36:11 +00:00
$cache = Config :: getParam ( 'hostnames' , []);
$platformHostnames = $platform [ 'hostnames' ] ? ? [];
2025-12-07 20:29:45 +00:00
// 1. Cache hit
if ( array_key_exists ( $hostname , $cache )) {
return ;
}
// 2. Domain validation
$domain = new Domain ( ! empty ( $hostname ) ? $hostname : '' );
if ( empty ( $domain -> get ()) || ! $domain -> isKnown () || $domain -> isTest ()) {
$cache [ $domain -> get ()] = false ;
2025-12-11 18:36:11 +00:00
Config :: setParam ( 'hostnames' , $cache );
2025-12-07 20:29:45 +00:00
return ;
}
if ( str_starts_with ( $request -> getURI (), '/.well-known/acme-challenge' )) {
return ;
}
// 3. Check if domain is a main domain
2025-12-11 18:36:11 +00:00
if ( ! in_array ( $domain -> get (), $platformHostnames )) {
2025-12-07 20:29:45 +00:00
return ;
}
// 4. Check/create rule (requires DB access)
2026-01-14 15:08:00 +00:00
$authorization -> skip ( function () use ( $dbForPlatform , $domain , $console , $queueForCertificates , & $cache ) {
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System :: getEnv ( '_APP_RULES_FORMAT' ) === 'md5' ;
$document = $isMd5
? $dbForPlatform -> getDocument ( 'rules' , md5 ( $domain -> get ()))
: $dbForPlatform -> findOne ( 'rules' , [
Query :: equal ( 'domain' , [ $domain -> get ()]),
]);
if ( ! $document -> isEmpty ()) {
return ;
}
// 5. Create new rule
$owner = '' ;
2026-01-30 20:30:00 +00:00
// Mark owner as Appwrite if its appwrite-owned domain
2026-01-30 15:20:46 +00:00
$appwriteDomains = [];
$appwriteDomainEnvs = [
System :: getEnv ( '_APP_DOMAIN_FUNCTIONS_FALLBACK' , '' ),
System :: getEnv ( '_APP_DOMAIN_FUNCTIONS' , '' ),
System :: getEnv ( '_APP_DOMAIN_SITES' , '' ),
];
foreach ( $appwriteDomainEnvs as $appwriteDomainEnv ) {
foreach ( \explode ( ',' , $appwriteDomainEnv ) as $appwriteDomain ) {
if ( empty ( $appwriteDomain )) {
continue ;
}
$appwriteDomains [] = $appwriteDomain ;
}
2026-01-14 15:08:00 +00:00
}
2026-01-30 15:20:46 +00:00
foreach ( $appwriteDomains as $appwriteDomain ) {
if ( \str_ends_with ( $domain -> get (), $appwriteDomain )) {
$owner = 'Appwrite' ;
break ;
}
2026-01-14 15:08:00 +00:00
}
$ruleId = $isMd5 ? md5 ( $domain -> get ()) : ID :: unique ();
$document = new Document ([
'$id' => $ruleId ,
'domain' => $domain -> get (),
'type' => 'api' ,
'status' => 'verifying' ,
'projectId' => $console -> getId (),
'projectInternalId' => $console -> getSequence (),
'search' => implode ( ' ' , [ $ruleId , $domain -> get ()]),
'owner' => $owner ,
'region' => $console -> getAttribute ( 'region' )
2025-12-07 20:29:45 +00:00
]);
2026-01-14 15:08:00 +00:00
$dbForPlatform -> createDocument ( 'rules' , $document );
Console :: info ( 'Issuing a TLS certificate for the main domain (' . $domain -> get () . ') in a few seconds...' );
$queueForCertificates
-> setDomain ( $document )
-> setSkipRenewCheck ( true )
-> trigger ();
} catch ( Duplicate $e ) {
Console :: info ( 'Certificate already exists' );
} finally {
$cache [ $domain -> get ()] = true ;
Config :: setParam ( 'hostnames' , $cache );
2025-12-07 20:29:45 +00:00
}
2026-01-14 15:08:00 +00:00
});
2025-12-07 20:29:45 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: options ()
2024-10-08 07:54:40 +00:00
-> inject ( 'utopia' )
-> inject ( 'swooleRequest' )
2022-07-22 06:00:42 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2025-06-26 07:04:38 +00:00
-> inject ( 'log' )
2024-12-12 10:30:26 +00:00
-> inject ( 'dbForPlatform' )
2024-02-22 09:40:18 +00:00
-> inject ( 'getProjectDB' )
-> inject ( 'queueForEvents' )
2025-01-30 04:53:53 +00:00
-> inject ( 'queueForStatsUsage' )
2026-02-09 19:24:57 +00:00
-> inject ( 'queueForExecutions' )
2025-03-25 11:33:03 +00:00
-> inject ( 'executor' )
2024-02-22 09:40:18 +00:00
-> inject ( 'geodb' )
2024-10-17 11:32:42 +00:00
-> inject ( 'isResourceBlocked' )
2025-12-07 20:29:45 +00:00
-> inject ( 'platform' )
2024-11-06 16:05:58 +00:00
-> inject ( 'previewHostname' )
2025-02-23 06:04:28 +00:00
-> inject ( 'project' )
2024-11-22 05:55:39 +00:00
-> inject ( 'devKey' )
2025-02-21 19:50:43 +00:00
-> inject ( 'apiKey' )
2025-12-07 20:29:45 +00:00
-> inject ( 'cors' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2026-01-16 09:36:35 +00:00
-> inject ( 'queueForDeletes' )
-> inject ( 'executionsRetentionCount' )
2026-02-09 19:24:57 +00:00
-> action ( function ( Http $utopia , SwooleRequest $swooleRequest , Request $request , Response $response , Log $log , Database $dbForPlatform , callable $getProjectDB , Event $queueForEvents , StatsUsage $queueForStatsUsage , Execution $queueForExecutions , Executor $executor , Reader $geodb , callable $isResourceBlocked , array $platform , string $previewHostname , Document $project , Document $devKey , ? Key $apiKey , Cors $cors , Authorization $authorization , DeleteEvent $queueForDeletes , int $executionsRetentionCount ) {
2023-02-27 12:43:20 +00:00
/*
* Appwrite Router
*/
2025-12-11 18:36:11 +00:00
$platformHostnames = $platform [ 'hostnames' ] ? ? [];
2023-02-27 12:43:20 +00:00
// Only run Router when external domain
2025-12-11 18:36:11 +00:00
if ( ! in_array ( $request -> getHostname (), $platformHostnames ) || ! empty ( $previewHostname )) {
2026-02-09 19:24:57 +00:00
if ( router ( $utopia , $dbForPlatform , $getProjectDB , $swooleRequest , $request , $response , $log , $queueForEvents , $queueForStatsUsage , $queueForExecutions , $executor , $geodb , $isResourceBlocked , $platform , $previewHostname , $authorization , $apiKey , $queueForDeletes , $executionsRetentionCount )) {
2025-02-04 13:16:17 +00:00
$utopia -> getRoute () ? -> label ( 'router' , true );
2023-02-27 12:43:20 +00:00
}
}
2022-07-22 06:00:42 +00:00
2025-12-07 20:29:45 +00:00
foreach ( $cors -> headers ( $request -> getOrigin ()) as $name => $value ) {
$response -> addHeader ( $name , $value );
}
2022-07-22 06:00:42 +00:00
$response
-> addHeader ( 'Server' , 'Appwrite' )
-> noContent ();
2025-02-23 06:04:28 +00:00
/** OPTIONS requests in utopia do not execute shutdown handlers , as a result we need to track the OPTIONS requests explicitly
* @ see https :// github . com / utopia - php / http / blob / 0.33 . 16 / src / App . php #L825-L855
*/
$queueForStatsUsage
-> addMetric ( METRIC_NETWORK_REQUESTS , 1 )
-> addMetric ( METRIC_NETWORK_INBOUND , $request -> getSize ())
-> addMetric ( METRIC_NETWORK_OUTBOUND , $response -> getSize ())
-> setProject ( $project )
-> trigger ();
2022-07-22 06:00:42 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: error ()
2022-07-22 06:00:42 +00:00
-> inject ( 'error' )
2024-10-08 07:54:40 +00:00
-> inject ( 'utopia' )
2022-07-22 06:00:42 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'logger' )
2024-01-23 15:40:32 +00:00
-> inject ( 'log' )
2025-01-30 04:53:53 +00:00
-> inject ( 'queueForStatsUsage' )
2024-11-22 04:21:03 +00:00
-> inject ( 'devKey' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2026-02-04 05:30:22 +00:00
-> action ( function ( Throwable $error , Http $utopia , Request $request , Response $response , Document $project , ? Logger $logger , Log $log , StatsUsage $queueForStatsUsage , Document $devKey , Authorization $authorization ) {
2024-04-01 11:02:47 +00:00
$version = System :: getEnv ( '_APP_VERSION' , 'UNKNOWN' );
2024-10-08 07:54:40 +00:00
$route = $utopia -> getRoute ();
2024-03-13 11:41:55 +00:00
$class = \get_class ( $error );
$code = $error -> getCode ();
$message = $error -> getMessage ();
$file = $error -> getFile ();
$line = $error -> getLine ();
$trace = $error -> getTrace ();
if ( php_sapi_name () === 'cli' ) {
2026-02-13 13:29:54 +00:00
Span :: error ( $error );
2024-03-13 11:41:55 +00:00
}
2024-10-08 07:54:40 +00:00
2024-03-13 11:41:55 +00:00
switch ( $class ) {
2026-02-10 07:11:59 +00:00
case Utopia\Http\Exception :: class :
2024-03-13 11:41:55 +00:00
$error = new AppwriteException ( AppwriteException :: GENERAL_UNKNOWN , $message , $code , $error );
switch ( $code ) {
case 400 :
$error -> setType ( AppwriteException :: GENERAL_ARGUMENT_INVALID );
break ;
case 404 :
$error -> setType ( AppwriteException :: GENERAL_ROUTE_NOT_FOUND );
break ;
}
break ;
2026-01-27 11:58:34 +00:00
case Utopia\Database\Exception\Authorization :: class :
2024-03-13 11:41:55 +00:00
$error = new AppwriteException ( AppwriteException :: USER_UNAUTHORIZED );
break ;
2026-01-27 11:58:34 +00:00
case Utopia\Database\Exception\Timeout :: class :
2025-05-15 11:33:00 +00:00
$error = new AppwriteException ( AppwriteException :: DATABASE_TIMEOUT , previous : $error );
2025-01-15 09:16:10 +00:00
break ;
2024-03-13 11:41:55 +00:00
}
$code = $error -> getCode ();
$message = $error -> getMessage ();
2022-07-22 06:00:42 +00:00
2023-11-16 18:21:09 +00:00
if ( $error instanceof AppwriteException ) {
2023-11-17 18:34:57 +00:00
$publish = $error -> isPublishable ();
2024-02-02 13:33:52 +00:00
} else {
$publish = $error -> getCode () === 0 || $error -> getCode () >= 500 ;
2023-11-16 18:21:09 +00:00
}
2024-10-08 07:54:40 +00:00
2025-03-07 20:59:11 +00:00
$providerConfig = System :: getEnv ( '_APP_EXPERIMENT_LOGGING_CONFIG' , '' );
if ( ! empty ( $providerConfig ) && $error -> getCode () >= 400 && $error -> getCode () < 500 ) {
2024-03-11 09:23:52 +00:00
// Register error logger
2024-10-08 07:54:40 +00:00
try {
2025-03-22 08:17:09 +00:00
$loggingProvider = new DSN ( $providerConfig );
2024-10-08 07:54:40 +00:00
$providerName = $loggingProvider -> getScheme ();
if ( ! empty ( $providerName ) && $providerName === 'sentry' ) {
$key = $loggingProvider -> getPassword ();
$projectId = $loggingProvider -> getUser () ? ? '' ;
$host = 'https://' . $loggingProvider -> getHost ();
2025-03-22 08:17:09 +00:00
$sampleRate = $loggingProvider -> getParam ( 'sample' , 0.01 );
2024-10-08 07:54:40 +00:00
$adapter = new Sentry ( $projectId , $key , $host );
$logger = new Logger ( $adapter );
2025-03-22 08:17:09 +00:00
$logger -> setSample ( $sampleRate );
2024-10-08 07:54:40 +00:00
$publish = true ;
} else {
throw new \Exception ( 'Invalid experimental logging provider' );
2024-03-11 09:23:52 +00:00
}
2024-10-08 07:54:40 +00:00
} catch ( \Throwable $th ) {
Console :: warning ( 'Failed to initialize logging provider: ' . $th -> getMessage ());
2024-03-11 09:23:52 +00:00
}
}
2025-02-23 06:07:54 +00:00
/**
2025-09-03 11:54:21 +00:00
* If not a publishable error , track usage stats . Publishable errors are >= 500 or those explicitly marked as publish = true in errors . php
2025-02-23 06:07:54 +00:00
*/
if ( ! $publish && $project -> getId () !== 'console' ) {
2026-01-14 15:08:00 +00:00
if ( ! DBUser :: isPrivileged ( $authorization -> getRoles ())) {
2024-05-23 13:39:23 +00:00
$fileSize = 0 ;
$file = $request -> getFiles ( 'file' );
if ( ! empty ( $file )) {
$fileSize = ( \is_array ( $file [ 'size' ]) && isset ( $file [ 'size' ][ 0 ])) ? $file [ 'size' ][ 0 ] : $file [ 'size' ];
}
2025-02-12 11:19:51 +00:00
$queueForStatsUsage
2024-05-23 13:39:23 +00:00
-> addMetric ( METRIC_NETWORK_REQUESTS , 1 )
-> addMetric ( METRIC_NETWORK_INBOUND , $request -> getSize () + $fileSize )
-> addMetric ( METRIC_NETWORK_OUTBOUND , $response -> getSize ());
}
2025-01-30 04:53:53 +00:00
$queueForStatsUsage
2024-05-23 13:39:23 +00:00
-> setProject ( $project )
-> trigger ();
}
2024-10-08 07:54:40 +00:00
if ( $logger && $publish ) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia -> getResource ( 'user' );
} catch ( \Throwable ) {
// All good, user is optional information for logger
}
2024-01-23 15:40:32 +00:00
if ( isset ( $user ) && ! $user -> isEmpty ()) {
$log -> setUser ( new User ( $user -> getId ()));
2024-11-26 13:54:27 +00:00
} else {
$log -> setUser ( new User ( 'guest-' . hash ( 'sha256' , $request -> getIP ())));
2022-07-22 06:00:42 +00:00
}
2024-05-08 04:25:12 +00:00
try {
$dsn = new DSN ( $project -> getAttribute ( 'database' , 'console' ));
} catch ( \InvalidArgumentException ) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN ( 'mysql://' . $project -> getAttribute ( 'database' , 'console' ));
}
2024-01-23 15:40:32 +00:00
$log -> setNamespace ( " http " );
2024-11-26 13:54:27 +00:00
$log -> setServer ( System :: getEnv ( '_APP_LOGGING_SERVICE_IDENTIFIER' , \gethostname ()));
2024-01-23 15:40:32 +00:00
$log -> setVersion ( $version );
$log -> setType ( Log :: TYPE_ERROR );
$log -> setMessage ( $error -> getMessage ());
2022-07-22 06:00:42 +00:00
2024-05-08 04:25:12 +00:00
$log -> addTag ( 'database' , $dsn -> getHost ());
2024-01-23 15:40:32 +00:00
$log -> addTag ( 'method' , $route -> getMethod ());
2025-01-14 01:50:40 +00:00
$log -> addTag ( 'url' , $request -> getURI ());
2024-01-23 15:40:32 +00:00
$log -> addTag ( 'verboseType' , get_class ( $error ));
$log -> addTag ( 'code' , $error -> getCode ());
2025-06-19 14:15:43 +00:00
$tags = $log -> getTags ();
2025-06-23 16:50:59 +00:00
if ( ! isset ( $tags [ 'projectId' ])) {
2025-06-19 14:15:43 +00:00
$log -> addTag ( 'projectId' , $project -> getId ());
}
2024-01-23 15:40:32 +00:00
$log -> addTag ( 'hostname' , $request -> getHostname ());
$log -> addTag ( 'locale' , ( string ) $request -> getParam ( 'locale' , $request -> getHeader ( 'x-appwrite-locale' , '' )));
2022-07-22 06:00:42 +00:00
2024-01-23 15:40:32 +00:00
$log -> addExtra ( 'file' , $error -> getFile ());
$log -> addExtra ( 'line' , $error -> getLine ());
$log -> addExtra ( 'trace' , $error -> getTraceAsString ());
2026-01-14 15:08:00 +00:00
$log -> addExtra ( 'roles' , $authorization -> getRoles ());
2024-01-23 15:40:32 +00:00
2026-01-07 10:30:52 +00:00
try {
/* add queries to log */
$queries = $request -> getParam ( 'queries' , []);
if ( ! empty ( $queries ) && is_array ( $queries )) {
$parsedQueries = Query :: parseQueries ( $queries );
// format query by removing sensitive values
$formatQuery = function ( array $queryArray ) use ( & $formatQuery ) : ? array {
$method = $queryArray [ 'method' ] ? ? '' ;
$values = $queryArray [ 'values' ] ? ? [];
$attribute = $queryArray [ 'attribute' ] ? ? '' ;
if ( ! is_string ( $method ) || $method === '' ) {
return null ;
}
// logical queries - recursively format nested queries
if ( in_array ( $method , [ Query :: TYPE_AND , Query :: TYPE_OR ], true )) {
$nested = [];
foreach ( $values as $nestedArray ) {
if ( is_array ( $nestedArray )) {
$formatted = $formatQuery ( $nestedArray );
if ( $formatted !== null ) {
$nested [] = $formatted ;
}
}
}
return empty ( $nested ) ? null : [ $method => $nested ];
}
// select - show selected attributes
if ( $method === Query :: TYPE_SELECT ) {
$attributes = array_values ( array_filter ( $values , 'is_string' ));
return [ $method => $attributes ];
}
// pagination
if ( in_array ( $method , [
Query :: TYPE_LIMIT ,
Query :: TYPE_OFFSET ,
Query :: TYPE_CURSOR_AFTER ,
Query :: TYPE_CURSOR_BEFORE
], true )) {
return [ $method => []];
}
// orders
if ( in_array ( $method , [
Query :: TYPE_ORDER_DESC ,
Query :: TYPE_ORDER_ASC ,
Query :: TYPE_ORDER_RANDOM
], true )) {
return [ $method => ! empty ( $attribute ) ? [ $attribute ] : []];
}
// filter
if ( ! empty ( $attribute )) {
return [ $method => [ $attribute ]];
}
// fallback
return [ $method => []];
};
$formattedQueries = [];
foreach ( $parsedQueries as $query ) {
$formatted = $formatQuery ( $query -> toArray ());
if ( $formatted !== null ) {
$formattedQueries [] = $formatted ;
}
}
if ( ! empty ( $formattedQueries )) {
$log -> addExtra ( 'queries' , $formattedQueries );
}
}
} catch ( Throwable $_ ) {
// don't fail the error handler
}
2026-01-07 10:07:56 +00:00
2025-01-17 04:38:38 +00:00
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD' ;
if ( ! empty ( $sdk )) {
2025-06-24 09:24:15 +00:00
/** @var \Appwrite\SDK\Method $sdk */
2025-01-17 04:38:38 +00:00
$action = $sdk -> getNamespace () . '.' . $sdk -> getMethodName ();
}
2024-01-23 15:40:32 +00:00
$log -> setAction ( $action );
2024-11-26 13:54:27 +00:00
$log -> addTag ( 'service' , $action );
2024-01-23 15:40:32 +00:00
2024-04-01 11:02:47 +00:00
$isProduction = System :: getEnv ( '_APP_ENV' , 'development' ) === 'production' ;
2024-01-23 15:40:32 +00:00
$log -> setEnvironment ( $isProduction ? Log :: ENVIRONMENT_PRODUCTION : Log :: ENVIRONMENT_STAGING );
2024-09-26 10:44:07 +00:00
try {
$responseCode = $logger -> addLog ( $log );
2024-09-26 10:51:51 +00:00
Console :: info ( 'Error log pushed with status code: ' . $responseCode );
2024-09-26 10:44:07 +00:00
} catch ( Throwable $th ) {
2024-09-26 10:51:51 +00:00
Console :: error ( 'Error pushing log: ' . $th -> getMessage ());
2024-09-26 10:44:07 +00:00
}
2022-07-22 06:00:42 +00:00
}
/** Wrap all exceptions inside Appwrite\Extend\Exception */
if ( ! ( $error instanceof AppwriteException )) {
2024-10-08 07:54:40 +00:00
$error = new AppwriteException ( AppwriteException :: GENERAL_UNKNOWN , $message , $code , $error );
2022-07-22 06:00:42 +00:00
}
switch ( $code ) { // Don't show 500 errors!
case 400 : // Error allowed publicly
case 401 : // Error allowed publicly
case 402 : // Error allowed publicly
case 403 : // Error allowed publicly
case 404 : // Error allowed publicly
2023-10-19 04:06:57 +00:00
case 408 : // Error allowed publicly
2022-07-22 06:00:42 +00:00
case 409 : // Error allowed publicly
case 412 : // Error allowed publicly
case 416 : // Error allowed publicly
2025-09-03 11:54:21 +00:00
case 422 : // Error allowed publicly
2022-07-22 06:00:42 +00:00
case 429 : // Error allowed publicly
2024-01-31 06:53:45 +00:00
case 451 : // Error allowed publicly
2022-07-22 06:00:42 +00:00
case 501 : // Error allowed publicly
case 503 : // Error allowed publicly
2022-02-13 09:12:16 +00:00
break ;
2022-07-22 06:00:42 +00:00
default :
$code = 500 ; // All other errors get the generic 500 server error status code
2024-10-08 07:54:40 +00:00
$message = 'Server Error' ;
2022-02-13 09:12:16 +00:00
}
2019-10-24 17:53:37 +00:00
2022-07-22 06:00:42 +00:00
//$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised
$type = $error -> getType ();
2026-02-04 05:30:22 +00:00
$output = Http :: isDevelopment () ? [
2022-07-22 06:00:42 +00:00
'message' => $message ,
'code' => $code ,
'file' => $file ,
'line' => $line ,
2024-01-24 08:37:28 +00:00
'trace' => \json_encode ( $trace , JSON_UNESCAPED_UNICODE ) === false ? [] : $trace , // check for failing encode
2024-04-29 22:50:20 +00:00
'version' => APP_VERSION_STABLE ,
2022-07-22 06:00:42 +00:00
'type' => $type ,
] : [
'message' => $message ,
'code' => $code ,
2024-04-29 22:50:20 +00:00
'version' => APP_VERSION_STABLE ,
2022-07-22 06:00:42 +00:00
'type' => $type ,
];
$response
-> addHeader ( 'Cache-Control' , 'no-cache, no-store, must-revalidate' )
-> addHeader ( 'Expires' , '0' )
-> addHeader ( 'Pragma' , 'no-cache' )
2023-08-06 13:11:30 +00:00
-> setStatusCode ( $code );
2019-10-24 17:53:37 +00:00
2025-04-15 09:40:32 +00:00
$template = $error -> getView () ? ? (( $route ) ? $route -> getLabel ( 'error' , null ) : null );
2024-04-15 05:29:32 +00:00
2025-04-15 09:40:32 +00:00
// TODO: Ideally use group 'api' here, but all wildcard routes seem to have 'api' at the moment
2025-12-27 18:28:08 +00:00
if ( empty ( $route ) || ! \str_starts_with ( $route -> getPath (), '/v1' )) {
2025-04-15 09:40:32 +00:00
$template = __DIR__ . '/../views/general/error.phtml' ;
}
2022-07-22 06:00:42 +00:00
2025-04-15 09:40:32 +00:00
if ( ! empty ( $template )) {
2022-11-17 12:37:59 +00:00
$layout = new View ( $template );
2022-07-22 06:00:42 +00:00
2022-11-17 12:37:59 +00:00
$layout
-> setParam ( 'title' , $project -> getAttribute ( 'name' ) . ' - Error' )
2026-02-04 05:30:22 +00:00
-> setParam ( 'development' , Http :: isDevelopment ())
2022-07-22 06:00:42 +00:00
-> setParam ( 'projectName' , $project -> getAttribute ( 'name' ))
-> setParam ( 'projectURL' , $project -> getAttribute ( 'url' ))
2024-02-24 18:12:27 +00:00
-> setParam ( 'message' , $output [ 'message' ] ? ? '' )
-> setParam ( 'type' , $output [ 'type' ] ? ? '' )
-> setParam ( 'code' , $output [ 'code' ] ? ? '' )
2025-04-09 04:43:54 +00:00
-> setParam ( 'trace' , $output [ 'trace' ] ? ? [])
-> setParam ( 'exception' , $error );
2022-07-22 06:00:42 +00:00
$response -> html ( $layout -> render ());
2025-02-08 11:11:09 +00:00
return ;
2022-07-22 06:00:42 +00:00
}
2019-10-24 17:53:37 +00:00
2022-07-22 06:00:42 +00:00
$response -> dynamic (
new Document ( $output ),
2024-10-08 07:54:40 +00:00
$utopia -> isDevelopment () ? Response :: MODEL_ERROR_DEV : Response :: MODEL_ERROR
2022-07-22 06:00:42 +00:00
);
});
2019-10-24 17:53:37 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/robots.txt' )
2019-10-24 17:53:37 +00:00
-> desc ( 'Robots.txt File' )
-> label ( 'scope' , 'public' )
-> label ( 'docs' , false )
2024-10-08 07:54:40 +00:00
-> inject ( 'utopia' )
-> inject ( 'swooleRequest' )
2024-05-23 09:17:59 +00:00
-> inject ( 'request' )
2020-12-26 12:19:46 +00:00
-> inject ( 'response' )
2025-06-26 07:04:38 +00:00
-> inject ( 'log' )
2024-12-12 10:30:26 +00:00
-> inject ( 'dbForPlatform' )
2024-05-23 09:17:59 +00:00
-> inject ( 'getProjectDB' )
-> inject ( 'queueForEvents' )
2025-01-30 04:53:53 +00:00
-> inject ( 'queueForStatsUsage' )
2026-02-09 19:24:57 +00:00
-> inject ( 'queueForExecutions' )
2025-03-25 11:33:03 +00:00
-> inject ( 'executor' )
2024-05-23 09:17:59 +00:00
-> inject ( 'geodb' )
2024-10-17 11:32:42 +00:00
-> inject ( 'isResourceBlocked' )
2025-12-07 20:29:45 +00:00
-> inject ( 'platform' )
2024-11-06 16:05:58 +00:00
-> inject ( 'previewHostname' )
2025-02-21 19:50:43 +00:00
-> inject ( 'apiKey' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2026-01-16 09:36:35 +00:00
-> inject ( 'queueForDeletes' )
-> inject ( 'executionsRetentionCount' )
2026-02-09 19:24:57 +00:00
-> action ( function ( Http $utopia , SwooleRequest $swooleRequest , Request $request , Response $response , Log $log , Database $dbForPlatform , callable $getProjectDB , Event $queueForEvents , StatsUsage $queueForStatsUsage , Execution $queueForExecutions , Executor $executor , Reader $geodb , callable $isResourceBlocked , array $platform , string $previewHostname , ? Key $apiKey , Authorization $authorization , DeleteEvent $queueForDeletes , int $executionsRetentionCount ) {
2025-12-11 18:36:11 +00:00
$platformHostnames = $platform [ 'hostnames' ] ? ? [];
if ( in_array ( $request -> getHostname (), $platformHostnames ) || ! empty ( $previewHostname )) {
2024-05-23 09:17:59 +00:00
$template = new View ( __DIR__ . '/../views/general/robots.phtml' );
$response -> text ( $template -> render ( false ));
} else {
2026-02-09 19:24:57 +00:00
if ( router ( $utopia , $dbForPlatform , $getProjectDB , $swooleRequest , $request , $response , $log , $queueForEvents , $queueForStatsUsage , $queueForExecutions , $executor , $geodb , $isResourceBlocked , $platform , $previewHostname , $authorization , $apiKey , $queueForDeletes , $executionsRetentionCount )) {
2025-02-04 13:16:17 +00:00
$utopia -> getRoute () ? -> label ( 'router' , true );
2024-11-20 12:48:42 +00:00
}
2024-05-23 09:17:59 +00:00
}
2020-12-26 12:19:46 +00:00
});
2019-10-24 17:53:37 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/humans.txt' )
2019-10-24 17:53:37 +00:00
-> desc ( 'Humans.txt File' )
-> label ( 'scope' , 'public' )
-> label ( 'docs' , false )
2024-10-08 07:54:40 +00:00
-> inject ( 'utopia' )
-> inject ( 'swooleRequest' )
2024-05-23 10:24:12 +00:00
-> inject ( 'request' )
2020-12-26 12:19:46 +00:00
-> inject ( 'response' )
2025-06-26 07:04:38 +00:00
-> inject ( 'log' )
2024-12-12 10:30:26 +00:00
-> inject ( 'dbForPlatform' )
2024-05-23 10:24:12 +00:00
-> inject ( 'getProjectDB' )
-> inject ( 'queueForEvents' )
2025-01-30 04:53:53 +00:00
-> inject ( 'queueForStatsUsage' )
2026-02-09 19:24:57 +00:00
-> inject ( 'queueForExecutions' )
2025-03-25 11:33:03 +00:00
-> inject ( 'executor' )
2024-05-23 10:24:12 +00:00
-> inject ( 'geodb' )
2024-10-17 11:32:42 +00:00
-> inject ( 'isResourceBlocked' )
2025-12-07 20:29:45 +00:00
-> inject ( 'platform' )
2024-11-06 16:05:58 +00:00
-> inject ( 'previewHostname' )
2025-02-21 19:50:43 +00:00
-> inject ( 'apiKey' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2026-01-16 09:36:35 +00:00
-> inject ( 'queueForDeletes' )
-> inject ( 'executionsRetentionCount' )
2026-02-09 19:24:57 +00:00
-> action ( function ( Http $utopia , SwooleRequest $swooleRequest , Request $request , Response $response , Log $log , Database $dbForPlatform , callable $getProjectDB , Event $queueForEvents , StatsUsage $queueForStatsUsage , Execution $queueForExecutions , Executor $executor , Reader $geodb , callable $isResourceBlocked , array $platform , string $previewHostname , ? Key $apiKey , Authorization $authorization , DeleteEvent $queueForDeletes , int $executionsRetentionCount ) {
2025-12-11 18:36:11 +00:00
$platformHostnames = $platform [ 'hostnames' ] ? ? [];
if ( in_array ( $request -> getHostname (), $platformHostnames ) || ! empty ( $previewHostname )) {
2024-05-23 10:24:12 +00:00
$template = new View ( __DIR__ . '/../views/general/humans.phtml' );
$response -> text ( $template -> render ( false ));
} else {
2026-02-09 19:24:57 +00:00
if ( router ( $utopia , $dbForPlatform , $getProjectDB , $swooleRequest , $request , $response , $log , $queueForEvents , $queueForStatsUsage , $queueForExecutions , $executor , $geodb , $isResourceBlocked , $platform , $previewHostname , $authorization , $apiKey , $queueForDeletes , $executionsRetentionCount )) {
2025-02-04 13:16:17 +00:00
$utopia -> getRoute () ? -> label ( 'router' , true );
2024-11-20 12:48:42 +00:00
}
2024-05-23 10:24:12 +00:00
}
2020-12-26 12:19:46 +00:00
});
2019-10-24 17:53:37 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/.well-known/acme-challenge/*' )
2020-02-18 22:13:18 +00:00
-> desc ( 'SSL Verification' )
-> label ( 'scope' , 'public' )
-> label ( 'docs' , false )
2020-12-26 12:19:46 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2022-05-26 14:46:08 +00:00
-> action ( function ( Request $request , Response $response ) {
2022-02-11 08:44:04 +00:00
$uriChunks = \explode ( '/' , $request -> getURI ());
$token = $uriChunks [ \count ( $uriChunks ) - 1 ];
2023-03-01 12:00:36 +00:00
$validator = new Text ( 100 , allowList : [
2022-02-11 08:44:04 +00:00
... Text :: NUMBERS ,
... Text :: ALPHABET_LOWER ,
... Text :: ALPHABET_UPPER ,
'-' ,
'_'
]);
2022-01-31 15:04:30 +00:00
2022-02-11 08:44:04 +00:00
if ( ! $validator -> isValid ( $token ) || \count ( $uriChunks ) !== 4 ) {
2022-08-08 14:44:07 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_ARGUMENT_INVALID , 'Invalid challenge token.' );
2022-01-31 15:04:30 +00:00
}
2020-06-29 21:43:34 +00:00
$base = \realpath ( APP_STORAGE_CERTIFICATES );
2022-05-23 14:54:50 +00:00
$absolute = \realpath ( $base . '/.well-known/acme-challenge/' . $token );
2020-02-23 08:55:57 +00:00
2020-10-27 19:46:15 +00:00
if ( ! $base ) {
2022-08-08 14:44:07 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_SERVER_ERROR , 'Storage error' );
2020-06-29 21:43:34 +00:00
}
2020-02-18 22:13:18 +00:00
2020-10-27 19:46:15 +00:00
if ( ! $absolute ) {
2022-08-08 14:44:07 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_ROUTE_NOT_FOUND , 'Unknown path' );
2020-06-29 21:43:34 +00:00
}
2020-02-23 08:55:57 +00:00
2020-10-27 19:46:15 +00:00
if ( ! \substr ( $absolute , 0 , \strlen ( $base )) === $base ) {
2022-08-08 14:44:07 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_UNAUTHORIZED_SCOPE , 'Invalid path' );
2020-06-29 21:43:34 +00:00
}
2020-02-23 17:45:51 +00:00
2020-10-27 19:46:15 +00:00
if ( ! \file_exists ( $absolute )) {
2022-08-08 14:44:07 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_ROUTE_NOT_FOUND , 'Unknown path' );
2020-06-29 21:43:34 +00:00
}
2020-02-23 08:55:57 +00:00
2020-06-29 21:43:34 +00:00
$content = @ \file_get_contents ( $absolute );
2020-02-18 22:13:18 +00:00
2020-10-27 19:46:15 +00:00
if ( ! $content ) {
2022-08-08 14:44:07 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_SERVER_ERROR , 'Failed to get contents' );
2020-02-18 22:13:18 +00:00
}
2020-06-29 21:43:34 +00:00
$response -> text ( $content );
2020-12-26 12:19:46 +00:00
});
2020-02-18 22:13:18 +00:00
2024-10-08 07:54:40 +00:00
include_once __DIR__ . '/shared/api.php' ;
include_once __DIR__ . '/shared/api/auth.php' ;
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/ping' )
2024-10-04 22:23:18 +00:00
-> groups ([ 'api' , 'general' ])
-> desc ( 'Test the connection between the Appwrite and the SDK.' )
2024-10-07 14:58:34 +00:00
-> label ( 'scope' , 'global' )
2024-10-04 22:23:18 +00:00
-> label ( 'event' , 'projects.[projectId].ping' )
2025-03-13 09:20:45 +00:00
-> label ( 'sdk' , new Method (
namespace : 'ping' ,
2025-04-25 10:26:10 +00:00
group : null ,
2025-03-13 09:20:45 +00:00
name : 'get' ,
hide : true ,
description : <<< EOT
Send a ping to project as part of onboarding .
EOT ,
auth : [],
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_ANY ,
)
],
))
2024-10-04 22:23:18 +00:00
-> inject ( 'response' )
2024-10-07 14:58:34 +00:00
-> inject ( 'project' )
2024-12-12 10:30:26 +00:00
-> inject ( 'dbForPlatform' )
2024-10-04 22:23:18 +00:00
-> inject ( 'queueForEvents' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( Response $response , Document $project , Database $dbForPlatform , Event $queueForEvents , Authorization $authorization ) {
2025-02-14 15:40:08 +00:00
if ( $project -> isEmpty () || $project -> getId () === 'console' ) {
2024-10-04 22:23:18 +00:00
throw new AppwriteException ( AppwriteException :: PROJECT_NOT_FOUND );
}
$pingCount = $project -> getAttribute ( 'pingCount' , 0 ) + 1 ;
$pingedAt = DateTime :: now ();
$project
-> setAttribute ( 'pingCount' , $pingCount )
2024-10-07 13:00:19 +00:00
-> setAttribute ( 'pingedAt' , $pingedAt );
2024-10-04 22:23:18 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> skip ( function () use ( $dbForPlatform , $project ) {
2024-12-12 10:30:26 +00:00
$dbForPlatform -> updateDocument ( 'projects' , $project -> getId (), $project );
2024-10-04 22:23:18 +00:00
});
$queueForEvents
2024-10-07 14:58:34 +00:00
-> setParam ( 'projectId' , $project -> getId ())
2024-10-04 22:23:18 +00:00
-> setPayload ( $response -> output ( $project , Response :: MODEL_PROJECT ));
$response -> text ( 'Pong!' );
});
2025-03-08 12:03:23 +00:00
// Preview authorization
2026-02-04 05:30:22 +00:00
Http :: get ( '/_appwrite/authorize' )
2025-03-08 12:03:23 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'previewHostname' )
-> action ( function ( Request $request , Response $response , string $previewHostname ) {
$host = $request -> getHostname () ? ? '' ;
if ( ! empty ( $previewHostname )) {
$host = $previewHostname ;
}
$referrer = $request -> getReferer ();
$protocol = \parse_url ( $request -> getOrigin ( $referrer ), PHP_URL_SCHEME );
$jwt = $request -> getParam ( 'jwt' , '' );
$path = $request -> getParam ( 'path' , '' );
$duration = 60 * 60 * 24 ; // 1 day in seconds
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
$response
2025-11-05 07:09:18 +00:00
-> addCookie ( COOKIE_NAME_PREVIEW , $jwt , ( new \DateTime ( $expire )) -> getTimestamp (), '/' , $host , ( 'https' === $protocol ), true , null )
2025-03-08 12:03:23 +00:00
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
-> redirect ( $protocol . '://' . $host . $path );
});
2026-02-04 05:30:22 +00:00
Http :: wildcard ()
2023-08-06 13:11:30 +00:00
-> groups ([ 'api' ])
2023-09-07 04:57:23 +00:00
-> label ( 'scope' , 'global' )
2025-02-24 17:42:20 +00:00
-> action ( function () {
2023-02-15 14:50:18 +00:00
throw new AppwriteException ( AppwriteException :: GENERAL_ROUTE_NOT_FOUND );
});
2024-10-08 07:54:40 +00:00
foreach ( Config :: getParam ( 'services' , []) as $service ) {
2025-02-03 10:38:36 +00:00
if ( ! empty ( $service [ 'controller' ])) {
2025-02-03 09:32:01 +00:00
include_once $service [ 'controller' ];
}
2024-10-08 07:54:40 +00:00
}
2024-10-22 11:00:10 +00:00
2025-01-17 04:31:39 +00:00
// Check for any errors found while we were initialising the SDK Methods.
if ( ! empty ( Method :: getErrors ())) {
throw new \Exception ( 'Errors found during SDK initialization:' . PHP_EOL . implode ( PHP_EOL , Method :: getErrors ()));
2025-01-17 04:39:16 +00:00
}
2025-02-04 16:56:14 +00:00
2025-04-18 16:06:02 +00:00
// Modules
2024-11-18 08:52:16 +00:00
$platform = new Appwrite ();
2024-12-05 05:40:52 +00:00
$platform -> init ( Service :: TYPE_HTTP );