appwrite/app/controllers/general.php

1581 lines
72 KiB
PHP
Raw Normal View History

2019-05-09 06:54:39 +00:00
<?php
2022-05-23 14:54:50 +00:00
require_once __DIR__ . '/../init.php';
2019-10-24 17:53:37 +00:00
2024-08-06 19:20:45 +00:00
use Ahc\Jwt\JWT;
2025-02-10 14:07:01 +00:00
use Ahc\Jwt\JWTException;
2024-07-04 15:02:34 +00:00
use Appwrite\Auth\Auth;
2025-02-21 19:50:43 +00:00
use Appwrite\Auth\Key;
use Appwrite\Event\Certificate;
2024-02-22 09:40:18 +00:00
use Appwrite\Event\Event;
2024-10-09 08:34:08 +00:00
use Appwrite\Event\Func;
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;
2020-06-11 19:36:10 +00:00
use Appwrite\Network\Validator\Origin;
2024-11-18 08:52:16 +00:00
use Appwrite\Platform\Appwrite;
2025-01-17 04:31:39 +00:00
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
2025-02-11 13:26:49 +00:00
use Appwrite\Transformation\Adapter\Preview;
use Appwrite\Transformation\Transformation;
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;
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;
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;
use Swoole\Http\Request as SwooleRequest;
use Utopia\App;
2020-10-29 22:04:53 +00:00
use Utopia\CLI\Console;
2024-03-06 17:34:21 +00:00
use Utopia\Config\Config;
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;
2024-02-24 18:01:00 +00:00
use Utopia\Database\Helpers\ID;
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;
2024-03-06 17:34:21 +00:00
use Utopia\Locale\Locale;
2024-09-05 22:22:28 +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-11-18 08:52:16 +00:00
use Utopia\Platform\Service;
2024-04-01 11:08:46 +00:00
use Utopia\System\System;
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
2025-06-19 14:15:43 +00:00
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey)
2024-04-22 15:58:40 +00:00
{
$host = $request->getHostname() ?? '';
2024-11-06 16:05:58 +00:00
if (!empty($previewHostname)) {
$host = $previewHostname;
}
2024-04-22 15:58:40 +00:00
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
2024-11-28 10:33: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();
}
2025-04-09 04:43:54 +00:00
$errorView = __DIR__ . '/../views/general/error.phtml';
2025-06-16 13:46:20 +00:00
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . '://' . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
2025-04-11 14:52:19 +00:00
2024-11-13 07:48:21 +00:00
if ($rule->isEmpty()) {
2025-04-11 14:52:19 +00:00
$appDomainFunctionsFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
2025-04-25 10:18:04 +00:00
$appDomainFunctions = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$appDomainSites = System::getEnv('_APP_DOMAIN_SITES', '');
2025-04-11 14:52:19 +00:00
if (!empty($appDomainFunctionsFallback) && \str_ends_with($host, $appDomainFunctionsFallback)) {
2025-04-25 10:18:04 +00:00
$appDomainFunctions = $appDomainFunctionsFallback;
2025-04-11 14:52:19 +00:00
}
2025-04-25 10:18:04 +00:00
if ($host === $appDomainFunctions || $host === $appDomainSites) {
2025-04-09 04:43:54 +00:00
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
}
2025-04-25 10:18:04 +00:00
if (\str_ends_with($host, $appDomainFunctions) || \str_ends_with($host, $appDomainSites)) {
2025-04-09 04:43:54 +00:00
$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);
$exception->addCTA('Start with this domain', $url . '/console');
throw $exception;
2024-04-22 15:58:40 +00:00
}
2025-02-19 20:59:04 +00:00
if (System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') {
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL && $host !== System::getEnv('_APP_CONSOLE_DOMAIN', '')) {
2025-04-09 04:43:54 +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);
2025-02-19 20:59:04 +00:00
}
}
2025-02-19 08:11:47 +00:00
2024-04-22 15:58:40 +00:00
// Act as API - no Proxy logic
return false;
}
2024-10-24 17:20:00 +00:00
$projectId = $rule->getAttribute('projectId');
2023-03-10 12:20:24 +00:00
$project = Authorization::skip(
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());
2024-12-17 14:05:37 +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-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-19 14:15:43 +00:00
$log->addTag('projectId', $project->getId());
2024-12-10 11:34:51 +00:00
}
2024-04-22 15:58:40 +00:00
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
$status = $project->getAttribute('services', [])['proxy'];
if (!$status) {
2025-04-09 04:43:54 +00:00
throw new AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED, view: $errorView);
2024-04-22 15:58:40 +00:00
}
}
// Skip Appwrite Router for ACME challenge. Nessessary for certificate generation
2023-08-06 13:11:30 +00:00
$path = ($swooleRequest->server['request_uri'] ?? '/');
2024-04-22 15:58:40 +00:00
if (\str_starts_with($path, '/.well-known/acme-challenge')) {
return false;
}
2025-02-23 20:34:14 +00:00
$type = $rule->getAttribute('type', '');
2024-09-05 10:17:30 +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-02-21 22:45:45 +00:00
if ($request->getProtocol() !== 'https' && $request->getHostname() !== APP_HOSTNAME_INTERNAL) {
2024-04-22 15:58:40 +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);
2024-04-22 15:58:40 +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', ''))) {
$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' ?
Authorization::skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
Authorization::skip(fn () => $dbForProject->getDocument('sites', $resourceId));
// 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
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
}
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()) {
$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' ?
2025-02-24 10:55:59 +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
$isPreview = $type === 'function' ? false : ($rule->getAttribute('trigger', '') !== 'manual');
2024-04-22 15:58:40 +00:00
2023-08-06 13:11:30 +00:00
$path = ($swooleRequest->server['request_uri'] ?? '/');
2023-02-27 12:43:20 +00:00
$query = ($swooleRequest->server['query_string'] ?? '');
2024-04-22 15:58:40 +00:00
if (!empty($query)) {
$path .= '?' . $query;
}
2025-02-10 14:07:01 +00:00
$protocol = $request->getProtocol();
2024-02-22 09:40:18 +00:00
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
*/
$requirePreview = \is_null($apiKey) || !$apiKey->isPreviewAuthDisabled();
if ($isPreview && $requirePreview) {
2025-02-10 14:07:01 +00:00
$cookie = $request->getCookie(Auth::$cookieNamePreview, '');
$authorized = false;
2025-02-10 14:07:01 +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) {
// Authorized remains false
2025-02-10 14:07:01 +00:00
}
2024-04-22 15:58:40 +00:00
$userExists = false;
$userId = $payload['userId'] ?? '';
if (!empty($userId)) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if (!$user->isEmpty() && $user->getAttribute('status', false)) {
$userExists = true;
2025-02-10 14:07:01 +00:00
}
}
2024-04-22 15:58:40 +00:00
$sessionExists = false;
2025-02-10 14:07:01 +00:00
$jwtSessionId = $payload['sessionId'] ?? '';
if (!empty($jwtSessionId) && !empty($user->find('$id', $jwtSessionId, 'sessions'))) {
$sessionExists = true;
}
2024-04-22 15:58:40 +00:00
$membershipExists = false;
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
2025-03-09 17:51:49 +00:00
if (!$project->isEmpty() && isset($user)) {
$teamId = $project->getAttribute('teamId', '');
$membership = $user->find('teamId', $teamId, 'memberships');
if (!empty($membership)) {
$membershipExists = true;
2025-02-10 14:07:01 +00:00
}
}
2024-04-22 15:58:40 +00:00
if ($userExists && $sessionExists && $membershipExists) {
$authorized = true;
}
2025-02-10 14:07:01 +00:00
}
2024-04-22 15:58:40 +00:00
if (!$authorized) {
2025-06-16 13:17:50 +00:00
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . "://" . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
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-04-22 15:58:40 +00:00
}
2024-10-08 07:54:40 +00:00
$body = $swooleRequest->getContent() ?? '';
$method = $swooleRequest->server['request_method'];
2023-10-27 15:25:19 +00:00
$requestHeaders = $request->getHeaders();
2024-04-22 15:58:40 +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
}
2024-04-22 15:58:40 +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-04-22 15:58:40 +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-04-22 15:58:40 +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-04-22 15:58:40 +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-04-22 15:58:40 +00:00
2025-02-28 09:40:28 +00:00
// Static site enforced runtime
if ($deployment->getAttribute('adapter', '') === 'static') {
2025-02-28 09:40:28 +00:00
$runtime = $runtimes['static-1'] ?? null;
2024-04-22 15:58:40 +00:00
}
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-04-22 15:58:40 +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-04-22 15:58:40 +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
}
2024-07-23 12:28:13 +00:00
2024-04-22 15:58:40 +00:00
$headers = \array_merge([], $requestHeaders);
$headers['x-appwrite-user-id'] = '';
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
2025-02-23 20:34:14 +00:00
$jwtExpiry = $resource->getAttribute('timeout', 900);
$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-04-22 15:58:40 +00:00
$ip = $headers['x-real-ip'] ?? '';
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];
}
}
$executionId = ID::unique();
$execution = new Document([
'$id' => $executionId,
'$permissions' => [],
'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-04-22 15:58:40 +00:00
'deploymentId' => $deployment->getId(),
'responseStatusCode' => 0,
'responseHeaders' => [],
'requestPath' => $path,
'requestMethod' => $method,
'requestHeaders' => $headersFiltered,
'errors' => '',
'logs' => '',
'duration' => 0.0,
]);
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);
}
2024-04-22 15:58:40 +00:00
$durationStart = \microtime(true);
$vars = [];
// 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'] ?? ''
]);
}
// Shared vars
2024-10-24 17:20:00 +00:00
foreach ($resource->getAttribute('varsProject', []) as $var) {
2024-04-22 15:58:40 +00:00
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
// Function vars
2024-10-24 17:20:00 +00:00
foreach ($resource->getAttribute('vars', []) as $var) {
2024-04-22 15:58:40 +00:00
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
2025-06-18 05:36:35 +00:00
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
2024-07-23 12:28:13 +00:00
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
2024-04-22 15:58:40 +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-04-22 15:58:40 +00:00
$vars = \array_merge($vars, [
2024-07-15 07:10:11 +00:00
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
'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-04-22 15:58:40 +00:00
]);
2025-02-08 09:36:11 +00:00
// SPA fallbackFile override
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-04-22 15:58:40 +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)) {
$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-04-22 15:58:40 +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-04-22 15:58:40 +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-04-22 15:58:40 +00:00
version: $version,
path: $path,
method: $method,
headers: $headers,
2024-10-23 16:36:26 +00:00
runtimeEntrypoint: $runtimeEntrypoint,
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),
2024-02-22 09:40:18 +00:00
requestTimeout: 30
2024-04-22 15:58:40 +00:00
);
2025-02-08 09:36:11 +00:00
// Branded 404 override
$isResponseBranded = false;
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();
$executionResponse['headers']['content-length'] = \strlen($executionResponse['body']);
$isResponseBranded = true;
2025-02-08 09:36:11 +00:00
}
2025-02-08 14:53:55 +00:00
// Branded banner for previews
if (!$isResponseBranded) {
if (\is_null($apiKey) || $apiKey->isBannerDisabled() === false) {
$transformation = new Transformation();
$transformation->addAdapter(new Preview());
$transformation->setInput($executionResponse['body']);
$transformation->setTraits($executionResponse['headers']);
if ($isPreview && $transformation->transform()) {
$executionResponse['body'] = $transformation->getOutput();
foreach ($executionResponse['headers'] as $key => $value) {
if (\strtolower($key) === 'content-length') {
$executionResponse['headers'][$key] = \strlen($executionResponse['body']);
}
2025-02-21 19:50:43 +00:00
}
2025-02-08 14:53:55 +00:00
}
}
}
2025-04-15 10:48:25 +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();
foreach ($executionResponse['headers'] as $key => $value) {
if (\strtolower($key) === 'content-length') {
$executionResponse['headers'][$key] = \strlen($executionResponse['body']);
2025-04-15 12:16:04 +00:00
} elseif (\strtolower($key) === 'content-type') {
$executionResponse['headers'][$key] = 'text/html';
2025-04-15 09:40:32 +00:00
}
}
}
2025-02-08 14:53:55 +00:00
2024-04-22 15:58:40 +00:00
$headersFiltered = [];
foreach ($executionResponse['headers'] as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) {
$headersFiltered[] = ['name' => $key, 'value' => $value];
}
}
/** Update execution status */
2024-08-08 08:38:15 +00:00
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
2024-04-22 15:58:40 +00:00
$execution->setAttribute('status', $status);
$execution->setAttribute('logs', $executionResponse['logs']);
$execution->setAttribute('errors', $executionResponse['errors']);
2024-02-22 09:40:18 +00:00
$execution->setAttribute('responseStatusCode', $executionResponse['statusCode']);
$execution->setAttribute('responseHeaders', $headersFiltered);
2024-04-22 15:58:40 +00:00
$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-04-22 15:58:40 +00:00
Console::error($th->getMessage());
if ($th instanceof AppwriteException) {
throw $th;
}
2024-04-22 15:58:40 +00:00
} finally {
2024-10-24 17:20:00 +00:00
if ($type === 'function') {
2024-10-22 15:33:33 +00:00
$queueForFunctions
->setType(Func::TYPE_ASYNC_WRITE)
->setExecution($execution)
->setProject($project)
->trigger();
2024-12-02 13:00:06 +00:00
} elseif ($type === 'site') { // TODO: Move it to logs worker later
$dbForProject->createDocument('executions', $execution);
2024-10-22 15:33:33 +00:00
}
2024-04-22 15:58:40 +00:00
}
2023-02-27 12:43:20 +00:00
2024-04-22 15:58:40 +00:00
$execution->setAttribute('logs', '');
$execution->setAttribute('errors', '');
2023-02-27 12:43:20 +00:00
2024-04-22 15:58:40 +00:00
$headers = [];
foreach (($executionResponse['headers'] ?? []) as $key => $value) {
$headers[] = ['name' => $key, 'value' => $value];
}
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
$execution->setAttribute('responseHeaders', $headers);
$body = $execution['responseBody'] ?? '';
$contentType = 'text/plain';
foreach ($execution['responseHeaders'] as $header) {
if (\strtolower($header['name']) === 'content-type') {
$contentType = $header['value'];
}
2023-02-27 12:43:20 +00:00
2024-12-02 12:20:54 +00:00
if (\strtolower($header['name']) === 'transfer-encoding') {
continue;
}
2024-12-02 16:56:52 +00:00
$response->addHeader(\strtolower($header['name']), $header['value']);
2024-04-22 15:58:40 +00:00
}
2023-02-27 12:43:20 +00:00
2024-04-22 15:58:40 +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);
$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);
$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);
$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())
->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
2024-04-22 15:58:40 +00:00
return true;
} elseif ($type === 'api') {
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', '');
$response->redirect($url, \intval($rule->getAttribute('redirectStatusCode', 301)));
2025-02-23 20:34:14 +00:00
return true;
2024-04-22 15:58:40 +00:00
} else {
2025-04-09 04:43:54 +00:00
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type, view: $errorView);
2024-04-22 15:58:40 +00:00
}
2023-02-27 12:43:20 +00:00
return false;
2024-04-22 15:58:40 +00:00
}
2023-02-27 12:43:20 +00:00
2024-04-10 17:46:31 +00:00
App::init()
->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');
}
});
2024-04-11 07:28:16 +00:00
App::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
}
});
2022-07-22 06:00:42 +00:00
App::init()
2025-02-06 10:08:28 +00:00
->groups(['api', 'web'])
2022-07-22 06:00:42 +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('console')
->inject('project')
->inject('dbForPlatform')
2024-04-22 15:58:40 +00:00
->inject('getProjectDB')
2022-07-22 06:00:42 +00:00
->inject('locale')
2023-04-17 02:10:17 +00:00
->inject('localeCodes')
2025-04-14 11:56:42 +00:00
->inject('platforms')
2024-04-22 15:58:40 +00:00
->inject('geodb')
2025-01-30 04:53:53 +00:00
->inject('queueForStatsUsage')
2024-04-22 15:58:40 +00:00
->inject('queueForEvents')
2022-12-20 16:11:30 +00:00
->inject('queueForCertificates')
2024-10-09 08:34:08 +00:00
->inject('queueForFunctions')
->inject('executor')
->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')
->inject('httpReferrer')
->inject('httpReferrerSafe')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $platforms, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, string $httpReferrer, string $httpReferrerSafe) {
/*
* Appwrite Router
*/
2023-07-28 07:56:07 +00:00
$host = $request->getHostname() ?? '';
2024-04-01 11:02:47 +00:00
$mainDomain = System::getEnv('_APP_DOMAIN', '');
2023-02-22 15:07:34 +00:00
// Only run Router when external domain
2024-11-06 16:05:58 +00:00
if ($host !== $mainDomain || !empty($previewHostname)) {
2025-06-19 14:15:43 +00:00
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
2025-02-04 13:16:17 +00:00
$utopia->getRoute()?->label('router', true);
2024-04-22 15:58:40 +00:00
}
}
2023-02-21 10:31:55 +00:00
2022-07-22 06:00:42 +00:00
/*
* Request format
*/
2023-02-19 11:04:12 +00:00
$route = $utopia->getRoute();
2022-07-22 06:00:42 +00:00
Request::setRoute($route);
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());
}
2022-07-22 06:00:42 +00:00
}
2022-07-22 06:00:42 +00:00
$domain = $request->getHostname();
$domains = Config::getParam('domains', []);
if (!array_key_exists($domain, $domains)) {
$domain = new Domain(!empty($domain) ? $domain : '');
2022-07-22 06:00:42 +00:00
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
$domains[$domain->get()] = false;
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
Console::warning('Skipping SSL certificates generation on ACME challenge.');
2022-03-29 10:17:56 +00:00
} else {
2022-07-22 06:00:42 +00:00
Authorization::disable();
2024-04-01 11:02:47 +00:00
$envDomain = System::getEnv('_APP_DOMAIN', '');
2022-07-22 06:00:42 +00:00
$mainDomain = null;
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
2024-11-28 10:33:00 +00:00
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($envDomain));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [Query::orderAsc('$id')]);
}
2024-10-07 02:40:01 +00:00
$mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get();
2022-07-22 06:00:42 +00:00
}
2022-07-22 06:00:42 +00:00
if ($mainDomain !== $domain->get()) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
} else {
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
2024-11-28 10:33:00 +00:00
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($domain->get()));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()])
]);
}
2025-02-26 15:41:44 +00:00
2025-03-14 08:37:44 +00:00
$owner = '';
2025-04-25 10:18:04 +00:00
$functionsDomainFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
2025-03-14 08:37:44 +00:00
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
2025-04-25 10:18:04 +00:00
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($functionsDomainFallback) && \str_ends_with($host, $functionsDomainFallback)) {
$functionsDomain = $functionsDomainFallback;
}
2025-04-25 10:57:45 +00:00
2025-04-25 10:18:04 +00:00
if (
(!empty($functionsDomain) && \str_ends_with($domain->get(), $functionsDomain)) ||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
) {
2025-03-14 08:37:44 +00:00
$owner = 'Appwrite';
}
2024-10-07 02:40:01 +00:00
if ($domainDocument->isEmpty()) {
2025-02-25 09:47:47 +00:00
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
2022-07-22 06:00:42 +00:00
$domainDocument = new Document([
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
2025-02-25 09:47:47 +00:00
'$id' => $ruleId,
2022-07-22 06:00:42 +00:00
'domain' => $domain->get(),
2025-02-27 09:55:49 +00:00
'type' => 'api',
2023-09-05 15:09:54 +00:00
'status' => 'verifying',
2025-03-14 11:07:38 +00:00
'projectId' => $console->getId(),
2025-05-26 05:42:11 +00:00
'projectInternalId' => $console->getSequence(),
2025-02-25 09:47:47 +00:00
'search' => implode(' ', [$ruleId, $domain->get()]),
2025-03-14 08:37:44 +00:00
'owner' => $owner,
2025-03-14 11:07:38 +00:00
'region' => $console->getAttribute('region')
2022-07-22 06:00:42 +00:00
]);
2025-02-26 15:41:44 +00:00
$domainDocument = $dbForPlatform->createDocument('rules', $domainDocument);
2022-07-22 06:00:42 +00:00
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
2022-12-13 11:16:12 +00:00
$queueForCertificates
2022-07-22 06:00:42 +00:00
->setDomain($domainDocument)
2023-09-05 15:09:54 +00:00
->setSkipRenewCheck(true)
2022-07-22 06:00:42 +00:00
->trigger();
}
2022-05-12 10:56:25 +00:00
}
2022-07-22 06:00:42 +00:00
$domains[$domain->get()] = true;
Authorization::reset(); // ensure authorization is re-enabled
}
2022-07-22 06:00:42 +00:00
Config::setParam('domains', $domains);
}
2022-07-22 06:00:42 +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);
}
2022-07-22 06:00:42 +00:00
$origin = \parse_url($request->getOrigin($httpReferrer), 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()) &&
2023-08-06 13:11:30 +00:00
$endDomain->getRegisterable() !== ''
2022-07-22 06:00:42 +00:00
);
2023-07-21 10:08:34 +00:00
$isLocalHost = $request->getHostname() === 'localhost' || $request->getHostname() === 'localhost:' . $request->getPort();
$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
: (
2024-06-07 00:54:51 +00:00
$isConsoleProject && $isConsoleRootSession
2023-07-21 10:08:34 +00:00
? '.' . $selfDomain->getRegisterable()
: '.' . $request->getHostname()
2024-06-07 00:54:51 +00:00
)
2023-07-21 10:08:34 +00:00
);
2022-07-22 06:00:42 +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-02-13 18:57:41 +00:00
$response->addHeader('X-Appwrite-Warning', "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
2020-12-28 17:03:47 +00:00
/*
2022-07-22 06:00:42 +00:00
* Security Headers
*
* As recommended at:
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
2024-04-01 11:02:47 +00:00
if (System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
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-05-31 15:41:12 +00:00
2022-11-21 03:49:45 +00:00
if ($request->getProtocol() === 'https') {
2022-07-22 06:00:42 +00:00
$response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days
2020-12-28 17:03:47 +00:00
}
2024-12-01 17:25:43 +00:00
2022-07-22 06:00:42 +00:00
$response
->addHeader('Server', 'Appwrite')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
2025-05-21 10:32:56 +00:00
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent')
2023-12-12 15:39:24 +00:00
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $httpReferrerSafe)
2023-08-06 13:11:30 +00:00
->addHeader('Access-Control-Allow-Credentials', 'true');
2019-05-09 06:54:39 +00:00
if (!$devKey->isEmpty()) {
$response->addHeader('Access-Control-Allow-Origin', '*');
}
2022-07-22 06:00:42 +00:00
/*
* Validate Client Domain - Check to avoid CSRF attack
* Adding Appwrite API domains to allow XDOMAIN communication
* Skip this check for non-web platforms which are not required to send an origin header
*/
$origin = $request->getOrigin($request->getReferer(''));
2025-04-14 11:56:42 +00:00
$originValidator = new Origin($platforms);
2019-05-09 06:54:39 +00:00
2022-05-31 11:35:59 +00:00
if (
2025-06-30 13:37:49 +00:00
$devKey->isEmpty()
2025-06-30 14:18:05 +00:00
&& !empty($origin)
2025-06-30 13:37:49 +00:00
&& !$originValidator->isValid($origin)
2022-07-22 06:00:42 +00:00
&& \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
&& $route->getLabel('origin', false) !== '*'
&& empty($request->getHeader('x-appwrite-key', ''))
&& \parse_url($httpReferrerSafe, PHP_URL_HOST) === 'localhost'
2022-05-31 11:35:59 +00:00
) {
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
2022-07-22 06:00:42 +00:00
App::options()
2023-07-11 08:53:40 +00:00
->inject('utopia')
2023-02-27 12:43:20 +00:00
->inject('swooleRequest')
2024-04-22 15:58:40 +00:00
->inject('request')
->inject('response')
2025-06-26 07:04:38 +00:00
->inject('log')
->inject('dbForPlatform')
2024-04-22 15:58:40 +00:00
->inject('getProjectDB')
->inject('queueForEvents')
2025-01-30 04:53:53 +00:00
->inject('queueForStatsUsage')
2024-10-09 08:34:08 +00:00
->inject('queueForFunctions')
->inject('executor')
2024-04-22 15:58:40 +00:00
->inject('geodb')
->inject('isResourceBlocked')
2024-11-06 16:05:58 +00:00
->inject('previewHostname')
->inject('project')
2024-11-22 05:55:39 +00:00
->inject('devKey')
2025-02-21 19:50:43 +00:00
->inject('apiKey')
2025-06-26 07:37:11 +00:00
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey) {
2024-04-22 15:58:40 +00:00
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
2024-11-06 16:05:58 +00:00
if ($host !== $mainDomain || !empty($previewHostname)) {
2025-06-19 14:15:43 +00:00
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
2025-02-04 13:16:17 +00:00
$utopia->getRoute()?->label('router', true);
2024-04-22 15:58:40 +00:00
}
}
$origin = $request->getOrigin();
$response
->addHeader('Server', 'Appwrite')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
2025-05-21 10:32:56 +00:00
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent')
2024-04-22 15:58:40 +00:00
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
->noContent();
if (!$devKey->isEmpty()) {
$response->addHeader('Access-Control-Allow-Origin', '*');
}
/** 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();
2024-04-22 15:58:40 +00:00
});
2022-07-22 06:00:42 +00:00
App::error()
->inject('error')
->inject('utopia')
2024-04-15 05:29:32 +00:00
->inject('request')
->inject('response')
->inject('project')
->inject('logger')
->inject('log')
2025-01-30 04:53:53 +00:00
->inject('queueForStatsUsage')
2024-11-22 04:21:03 +00:00
->inject('devKey')
2025-01-30 04:53:53 +00:00
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage) {
2024-04-15 05:29:32 +00:00
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
2023-02-19 11:04:12 +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();
2025-03-18 17:29:40 +00:00
2024-03-13 11:41:55 +00:00
if (php_sapi_name() === 'cli') {
Console::error('[Error] Timestamp: ' . date('c', time()));
if ($route) {
Console::error('[Error] Method: ' . $route->getMethod());
Console::error('[Error] URL: ' . $route->getPath());
}
Console::error('[Error] Type: ' . get_class($error));
Console::error('[Error] Message: ' . $message);
Console::error('[Error] File: ' . $file);
Console::error('[Error] Line: ' . $line);
}
switch ($class) {
case 'Utopia\Exception':
$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;
case 'Utopia\Database\Exception\Authorization':
$error = new AppwriteException(AppwriteException::USER_UNAUTHORIZED);
break;
case 'Utopia\Database\Exception\Timeout':
$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();
2023-11-16 18:21:09 +00:00
2024-04-15 05:29:32 +00:00
if ($error instanceof AppwriteException) {
$publish = $error->isPublishable();
} else {
$publish = $error->getCode() === 0 || $error->getCode() >= 500;
}
2022-07-22 06:00:42 +00:00
$providerConfig = System::getEnv('_APP_EXPERIMENT_LOGGING_CONFIG', '');
if (!empty($providerConfig) && $error->getCode() >= 400 && $error->getCode() < 500) {
2024-09-05 22:14:06 +00:00
// Register error logger
2024-08-26 11:05:50 +00:00
try {
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
2024-08-26 11:05:50 +00:00
if (!empty($providerName) && $providerName === 'sentry') {
$key = $loggingProvider->getPassword();
$projectId = $loggingProvider->getUser() ?? '';
$host = 'https://' . $loggingProvider->getHost();
$sampleRate = $loggingProvider->getParam('sample', 0.01);
$adapter = new Sentry($projectId, $key, $host);
$logger = new Logger($adapter);
$logger->setSample($sampleRate);
$publish = true;
} else {
throw new \Exception('Invalid experimental logging provider');
2024-09-05 22:14:06 +00:00
}
} catch (\Throwable $th) {
Console::warning('Failed to initialize logging provider: ' . $th->getMessage());
2024-09-05 22:14:06 +00:00
}
}
2024-04-15 05:29:32 +00:00
/**
* If its not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
*/
if (!$publish && $project->getId() !== 'console') {
2024-05-23 13:39:23 +00:00
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
2025-01-30 04:53:53 +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();
}
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
2024-03-13 11:41:55 +00:00
} catch (\Throwable) {
// All good, user is optional information for logger
}
2022-07-22 06:00:42 +00:00
2024-04-15 05:29: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())));
2024-04-15 05:29:32 +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-04-15 05:29:32 +00:00
$log->setNamespace("http");
2024-11-26 13:54:27 +00:00
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
2024-04-15 05:29:32 +00:00
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
2024-05-08 04:25:12 +00:00
$log->addTag('database', $dsn->getHost());
2024-04-15 05:29:32 +00:00
$log->addTag('method', $route->getMethod());
2025-01-14 01:50:40 +00:00
$log->addTag('url', $request->getURI());
2024-04-15 05:29: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();
if (!isset($tags['projectId'])) {
2025-06-19 14:15:43 +00:00
$log->addTag('projectId', $project->getId());
}
2024-04-15 05:29:32 +00:00
$log->addTag('hostname', $request->getHostname());
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
2024-04-15 05:29:32 +00:00
2025-01-17 04:38:38 +00:00
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
/** @var Appwrite\SDK\Method $sdk */
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
}
2024-04-15 05:29:32 +00:00
$log->setAction($action);
2024-11-26 13:54:27 +00:00
$log->addTag('service', $action);
2024-04-15 05:29:32 +00:00
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
2024-09-26 10:44:07 +00:00
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
2024-09-26 10:44:07 +00:00
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
2024-09-26 10:44:07 +00:00
}
2024-04-15 05:29:32 +00:00
}
/** Wrap all exceptions inside Appwrite\Extend\Exception */
if (!($error instanceof AppwriteException)) {
2022-08-08 14:44:07 +00:00
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
2024-04-15 05:29:32 +00:00
}
2024-04-15 04:29:12 +00:00
2024-04-15 05:29:32 +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
case 408: // Error allowed publicly
case 409: // Error allowed publicly
case 412: // Error allowed publicly
case 416: // Error allowed publicly
case 429: // Error allowed publicly
case 451: // Error allowed publicly
case 501: // Error allowed publicly
case 503: // Error allowed publicly
break;
default:
$code = 500; // All other errors get the generic 500 server error status code
2022-07-22 06:00:42 +00:00
$message = 'Server Error';
2024-04-15 05:29:32 +00:00
}
2024-04-15 04:29:12 +00:00
2024-04-15 05:29:32 +00:00
//$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised
$type = $error->getType();
2025-03-24 05:06:18 +00:00
$output = App::isDevelopment() ? [
2024-04-15 05:29:32 +00:00
'message' => $message,
'code' => $code,
'file' => $file,
'line' => $line,
'trace' => \json_encode($trace, JSON_UNESCAPED_UNICODE) === false ? [] : $trace, // check for failing encode
'version' => APP_VERSION_STABLE,
2024-04-15 05:29:32 +00:00
'type' => $type,
] : [
'message' => $message,
'code' => $code,
'version' => APP_VERSION_STABLE,
2024-04-15 05:29:32 +00:00
'type' => $type,
];
2024-04-15 04:29:12 +00:00
2024-04-15 05:29:32 +00:00
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->setStatusCode($code);
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-04-15 10:48:25 +00:00
if (!\str_starts_with($route->getPath(), '/v1')) {
2025-04-15 09:40:32 +00:00
$template = __DIR__ . '/../views/general/error.phtml';
}
2024-04-15 05:29:32 +00:00
2025-04-15 09:40:32 +00:00
if (!empty($template)) {
2024-04-15 05:29:32 +00:00
$layout = new View($template);
$layout
->setParam('title', $project->getAttribute('name') . ' - Error')
2022-07-22 06:00:42 +00:00
->setParam('development', App::isDevelopment())
2024-04-15 05:29:32 +00:00
->setParam('projectName', $project->getAttribute('name'))
->setParam('projectURL', $project->getAttribute('url'))
->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);
2024-04-15 05:29:32 +00:00
$response->html($layout->render());
return;
2024-04-15 05:29:32 +00:00
}
2024-04-15 04:29:12 +00:00
2024-04-15 05:29:32 +00:00
$response->dynamic(
new Document($output),
2022-07-22 06:00:42 +00:00
$utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR
2024-04-15 05:29:32 +00:00
);
});
2019-10-24 17:53:37 +00:00
2020-06-28 17:31:21 +00:00
App::get('/robots.txt')
2019-10-24 17:53:37 +00:00
->desc('Robots.txt File')
->label('scope', 'public')
->label('docs', false)
->inject('utopia')
->inject('swooleRequest')
->inject('request')
2020-12-26 12:19:46 +00:00
->inject('response')
2025-06-26 07:04:38 +00:00
->inject('log')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
2025-01-30 04:53:53 +00:00
->inject('queueForStatsUsage')
2024-10-09 08:34:08 +00:00
->inject('queueForFunctions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
2024-11-06 16:05:58 +00:00
->inject('previewHostname')
2025-02-21 19:50:43 +00:00
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? '';
2025-08-01 03:58:07 +00:00
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
2025-08-01 03:58:07 +00:00
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
2025-06-19 14:15:43 +00:00
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
2025-02-04 13:16:17 +00:00
$utopia->getRoute()?->label('router', true);
2024-11-20 12:48:42 +00:00
}
}
2020-12-26 12:19:46 +00:00
});
2019-10-24 17:53:37 +00:00
2020-06-28 17:31:21 +00:00
App::get('/humans.txt')
2019-10-24 17:53:37 +00:00
->desc('Humans.txt File')
->label('scope', 'public')
->label('docs', false)
2024-05-23 10:24:12 +00:00
->inject('utopia')
->inject('swooleRequest')
->inject('request')
2020-12-26 12:19:46 +00:00
->inject('response')
2025-06-26 07:04:38 +00:00
->inject('log')
->inject('dbForPlatform')
2024-05-23 10:24:12 +00:00
->inject('getProjectDB')
->inject('queueForEvents')
2025-01-30 04:53:53 +00:00
->inject('queueForStatsUsage')
2024-10-09 08:34:08 +00:00
->inject('queueForFunctions')
->inject('executor')
2024-05-23 10:24:12 +00:00
->inject('geodb')
->inject('isResourceBlocked')
2024-11-06 16:05:58 +00:00
->inject('previewHostname')
2025-02-21 19:50:43 +00:00
->inject('apiKey')
2025-06-26 07:04:38 +00:00
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
2024-05-23 10:24:12 +00:00
$host = $request->getHostname() ?? '';
2025-08-01 03:58:07 +00:00
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
2024-05-23 10:24:12 +00:00
2025-08-01 03:58:07 +00:00
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
2024-05-23 10:24:12 +00:00
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
2025-06-19 14:15:43 +00:00
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
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
2023-06-02 11:18:34 +00:00
App::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) {
$uriChunks = \explode('/', $request->getURI());
$token = $uriChunks[\count($uriChunks) - 1];
2023-03-01 12:00:36 +00:00
$validator = new Text(100, allowList: [
...Text::NUMBERS,
...Text::ALPHABET_LOWER,
...Text::ALPHABET_UPPER,
'-',
'_'
]);
2022-01-31 15:04:30 +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
2020-07-29 07:29:34 +00:00
include_once __DIR__ . '/shared/api.php';
2022-11-24 07:53:52 +00:00
include_once __DIR__ . '/shared/api/auth.php';
2020-06-25 19:53:36 +00:00
2024-10-04 22:23:18 +00:00
App::get('/v1/ping')
->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')
->inject('dbForPlatform')
2024-10-04 22:23:18 +00:00
->inject('queueForEvents')
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) {
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
Authorization::skip(function () use ($dbForPlatform, $project) {
$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
App::get('/_appwrite/authorize')
->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
->addCookie(Auth::$cookieNamePreview, $jwt, (new \DateTime($expire))->getTimestamp(), '/', $host, ('https' === $protocol), true, null)
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($protocol . '://' . $host . $path);
});
2023-02-15 14:50:18 +00:00
App::wildcard()
2024-07-18 19:31:24 +00:00
->groups(['api'])
->label('scope', 'global')
->action(function () {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
});
2023-02-15 14:50:18 +00:00
2020-10-27 19:46:15 +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'];
}
}
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);