diff --git a/app/controllers/general.php b/app/controllers/general.php index 63f3571997..b81238a19b 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -628,7 +628,64 @@ Http::init() Http::error() ->inject('error') - ->action(function($error) { + ->inject('user') + ->inject('route') + ->inject('request') + ->inject('response') + ->inject('project') + ->inject('logger') + ->inject('log') + ->inject('authorization') + ->inject('connections') + ->action(function (Throwable $error, Document $user, ?Route $route, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Authorization $authorization, Connections $connections) { + $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); + + if(is_null($route)) { + $route = new Route($request->getMethod(), $request->getURI()); + } + + if ($error instanceof AppwriteException) { + $publish = $error->isPublishable(); + } else { + $publish = $error->getCode() === 0 || $error->getCode() >= 500; + } + + if ($logger && ($publish || $error->getCode() === 0)) { + if (isset($user) && !$user->isEmpty()) { + $log->setUser(new User($user->getId())); + } + + $log->setNamespace("http"); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); + + $log->addTag('database', $project->getAttribute('database', 'console')); + $log->addTag('method', $route->getMethod()); + $log->addTag('url', $route->getPath()); + $log->addTag('verboseType', get_class($error)); + $log->addTag('code', $error->getCode()); + $log->addTag('projectId', $project->getId()); + $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('detailedTrace', $error->getTrace()); + $log->addExtra('roles', $authorization->getRoles()); + + $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); + $log->setAction($action); + + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + $responseCode = $logger->addLog($log); + Console::info('Log pushed with status code: ' . $responseCode); + } + $code = $error->getCode(); $message = $error->getMessage(); $file = $error->getFile(); @@ -639,193 +696,115 @@ Http::error() Console::error('[Error] ------------------'); Console::error('[Error] Timestamp: ' . date('c', time())); + if ($route) { + Console::error('[Error] Method: ' . $route->getMethod()); + Console::error('[Error] URL: ' . $route->getPath()); + } + Console::error('[Error] Code: ' . $code); Console::error('[Error] Type: ' . get_class($error)); Console::error('[Error] Message: ' . $message); Console::error('[Error] File: ' . $file); Console::error('[Error] Line: ' . $line); } + + /** Handle Utopia Errors */ + if ($error instanceof Utopia\Http\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; + } + } elseif ($error instanceof Utopia\Database\Exception\Conflict) { + $error = new AppwriteException(AppwriteException::DOCUMENT_UPDATE_CONFLICT, previous: $error); + $code = $error->getCode(); + $message = $error->getMessage(); + } elseif ($error instanceof Utopia\Database\Exception\Timeout) { + $error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error); + $code = $error->getCode(); + $message = $error->getMessage(); + } + + /** Wrap all exceptions inside Appwrite\Extend\Exception */ + if (!($error instanceof AppwriteException)) { + $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, (int)$code, $error); + } + + 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 + $message = (Http::getMode() === Http::MODE_TYPE_DEVELOPMENT) ? $message : 'Server Error'; + } + + //$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised + + $type = $error->getType(); + + $output = ((Http::isDevelopment())) ? [ + 'message' => $message, + 'code' => $code, + 'file' => $file, + 'line' => $line, + 'trace' => \json_encode($trace, JSON_UNESCAPED_UNICODE) === false ? [] : $trace, // check for failing encode + 'version' => $version, + 'type' => $type, + ] : [ + 'message' => $message, + 'code' => $code, + 'version' => $version, + 'type' => $type, + ]; + + $response + ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') + ->addHeader('Expires', '0') + ->addHeader('Pragma', 'no-cache') + ->setStatusCode($code); + + $template = ($route) ? $route->getLabel('error', null) : null; + + if ($template) { + $layout = new View($template); + + $layout + ->setParam('title', $project->getAttribute('name') . ' - Error') + ->setParam('development', Http::isDevelopment()) + ->setParam('projectName', $project->getAttribute('name')) + ->setParam('projectURL', $project->getAttribute('url')) + ->setParam('message', $output['message'] ?? '') + ->setParam('type', $output['type'] ?? '') + ->setParam('code', $output['code'] ?? '') + ->setParam('trace', $output['trace'] ?? []); + + $response->html($layout->render()); + } + + $connections->reclaim(); + + $response->dynamic( + new Document($output), + Http::isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR + ); }); -// Http::error() -// ->inject('error') -// ->inject('user') -// ->inject('route') -// ->inject('request') -// ->inject('response') -// ->inject('project') -// ->inject('logger') -// ->inject('log') -// ->inject('authorization') -// ->inject('connections') -// ->action(function (Throwable $error, Document $user, ?Route $route, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Authorization $authorization, Connections $connections) { -// $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - -// if(is_null($route)) { -// $route = new Route($request->getMethod(), $request->getURI()); -// } - -// if ($error instanceof AppwriteException) { -// $publish = $error->isPublishable(); -// } else { -// $publish = $error->getCode() === 0 || $error->getCode() >= 500; -// } - -// if ($logger && ($publish || $error->getCode() === 0)) { -// if (isset($user) && !$user->isEmpty()) { -// $log->setUser(new User($user->getId())); -// } - -// $log->setNamespace("http"); -// $log->setServer(\gethostname()); -// $log->setVersion($version); -// $log->setType(Log::TYPE_ERROR); -// $log->setMessage($error->getMessage()); - -// $log->addTag('database', $project->getAttribute('database', 'console')); -// $log->addTag('method', $route->getMethod()); -// $log->addTag('url', $route->getPath()); -// $log->addTag('verboseType', get_class($error)); -// $log->addTag('code', $error->getCode()); -// $log->addTag('projectId', $project->getId()); -// $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('detailedTrace', $error->getTrace()); -// $log->addExtra('roles', $authorization->getRoles()); - -// $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); -// $log->setAction($action); - -// $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; -// $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - -// $responseCode = $logger->addLog($log); -// Console::info('Log pushed with status code: ' . $responseCode); -// } - -// $code = $error->getCode(); -// $message = $error->getMessage(); -// $file = $error->getFile(); -// $line = $error->getLine(); -// $trace = $error->getTrace(); - -// if (php_sapi_name() === 'cli') { -// Console::error('[Error] ------------------'); -// Console::error('[Error] Timestamp: ' . date('c', time())); - -// if ($route) { -// Console::error('[Error] Method: ' . $route->getMethod()); -// Console::error('[Error] URL: ' . $route->getPath()); -// } - -// Console::error('[Error] Code: ' . $code); -// Console::error('[Error] Type: ' . get_class($error)); -// Console::error('[Error] Message: ' . $message); -// Console::error('[Error] File: ' . $file); -// Console::error('[Error] Line: ' . $line); -// } - -// /** Handle Utopia Errors */ -// if ($error instanceof Utopia\Http\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; -// } -// } elseif ($error instanceof Utopia\Database\Exception\Conflict) { -// $error = new AppwriteException(AppwriteException::DOCUMENT_UPDATE_CONFLICT, previous: $error); -// $code = $error->getCode(); -// $message = $error->getMessage(); -// } elseif ($error instanceof Utopia\Database\Exception\Timeout) { -// $error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error); -// $code = $error->getCode(); -// $message = $error->getMessage(); -// } - -// /** Wrap all exceptions inside Appwrite\Extend\Exception */ -// if (!($error instanceof AppwriteException)) { -// $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, (int)$code, $error); -// } - -// 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 -// $message = (Http::getMode() === Http::MODE_TYPE_DEVELOPMENT) ? $message : 'Server Error'; -// } - -// //$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised - -// $type = $error->getType(); - -// $output = ((Http::isDevelopment())) ? [ -// 'message' => $message, -// 'code' => $code, -// 'file' => $file, -// 'line' => $line, -// 'trace' => \json_encode($trace, JSON_UNESCAPED_UNICODE) === false ? [] : $trace, // check for failing encode -// 'version' => $version, -// 'type' => $type, -// ] : [ -// 'message' => $message, -// 'code' => $code, -// 'version' => $version, -// 'type' => $type, -// ]; - -// $response -// ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') -// ->addHeader('Expires', '0') -// ->addHeader('Pragma', 'no-cache') -// ->setStatusCode($code); - -// $template = ($route) ? $route->getLabel('error', null) : null; - -// if ($template) { -// $layout = new View($template); - -// $layout -// ->setParam('title', $project->getAttribute('name') . ' - Error') -// ->setParam('development', Http::isDevelopment()) -// ->setParam('projectName', $project->getAttribute('name')) -// ->setParam('projectURL', $project->getAttribute('url')) -// ->setParam('message', $output['message'] ?? '') -// ->setParam('type', $output['type'] ?? '') -// ->setParam('code', $output['code'] ?? '') -// ->setParam('trace', $output['trace'] ?? []); - -// $response->html($layout->render()); -// } - -// $connections->reclaim(); - -// $response->dynamic( -// new Document($output), -// Http::isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR -// ); -// }); - Http::get('/robots.txt') ->desc('Robots.txt File') ->label('scope', 'public') diff --git a/app/http.php b/app/http.php index 8b9fae8396..c4b5bcd779 100644 --- a/app/http.php +++ b/app/http.php @@ -12,8 +12,11 @@ use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\Audit\Audit; +use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Adapter\MariaDB; +use Utopia\Database\Adapter\MySQL; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; @@ -61,156 +64,184 @@ global $global, $container; http::onStart() ->inject('authorization') - ->inject('dbForConsole') + ->inject('cache') + ->inject('pools') ->inject('connections') - ->action(function (Authorization $authorization, Database $dbForConsole, Connections $connections) { - // wait for database to be ready - $attempts = 0; - $max = 10; - $sleep = 1; - - do { - try { - $attempts++; - $dbForConsole->ping(); - break; // leave the do-while if successful - } catch (\Throwable $e) { - Console::warning("Database not ready. Retrying connection ({$attempts})..."); - if ($attempts >= $max) { - throw new \Exception('Failed to connect to database: ' . $e->getMessage()); - } - sleep($sleep); - } - } while ($attempts < $max); - - Console::success('[Setup] - Server database init started...'); - + ->action(function (Authorization $authorization, Cache $cache, array $pools, Connections $connections) { try { - Console::success('[Setup] - Creating database: appwrite...'); - $dbForConsole->create(); + // wait for database to be ready + $attempts = 0; + $max = 15; + $sleep = 2; + + do { + try { + $attempts++; + $pool = $pools['pools-console-main']['pool']; + $dsn = $pools['pools-console-main']['dsn']; + $connection = $pool->get(); + $connections->add($connection, $pool); + + $adapter = match ($dsn->getScheme()) { + 'mariadb' => new MariaDB($connection), + 'mysql' => new MySQL($connection), + default => null + }; + + $adapter->setDatabase($dsn->getPath()); + + $dbForConsole = new Database($adapter, $cache); + $dbForConsole->setAuthorization($authorization); + + $dbForConsole + ->setNamespace('_console') + ->setMetadata('host', \gethostname()) + ->setMetadata('project', 'console') + ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS); + + $dbForConsole->ping(); + break; // leave the do-while if successful + } catch (\Throwable $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= $max) { + throw new \Exception('Failed to connect to database: ' . $e->getMessage()); + } + sleep($sleep); + } + } while ($attempts < $max); + + Console::success('[Setup] - Server database init started...'); + + try { + Console::success('[Setup] - Creating database: appwrite...'); + $dbForConsole->create(); + } catch (\Throwable $e) { + Console::success('[Setup] - Skip: metadata table already exists'); + return true; + } + + if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) { + $audit = new Audit($dbForConsole, $authorization); + $audit->setup(); + } + + if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) { + $abuse = new TimeLimit("", 0, 1, $dbForConsole, $authorization); + $abuse->setup(); + } + + /** @var array $collections */ + $collections = Config::getParam('collections', []); + $consoleCollections = $collections['console']; + foreach ($consoleCollections as $key => $collection) { + if (($collection['$collection'] ?? '') !== Database::METADATA) { + continue; + } + if (!$dbForConsole->getCollection($key)->isEmpty()) { + continue; + } + + Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...'); + + $attributes = []; + $indexes = []; + + foreach ($collection['attributes'] as $attribute) { + $attributes[] = new Document([ + '$id' => ID::custom($attribute['$id']), + 'type' => $attribute['type'], + 'size' => $attribute['size'], + 'required' => $attribute['required'], + 'signed' => $attribute['signed'], + 'array' => $attribute['array'], + 'filters' => $attribute['filters'], + 'default' => $attribute['default'] ?? null, + 'format' => $attribute['format'] ?? '' + ]); + } + + foreach ($collection['indexes'] as $index) { + $indexes[] = new Document([ + '$id' => ID::custom($index['$id']), + 'type' => $index['type'], + 'attributes' => $index['attributes'], + 'lengths' => $index['lengths'], + 'orders' => $index['orders'], + ]); + } + + $dbForConsole->createCollection($key, $attributes, $indexes); + } + + if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) { + Console::success('[Setup] - Creating default bucket...'); + $dbForConsole->createDocument('buckets', new Document([ + '$id' => ID::custom('default'), + '$collection' => ID::custom('buckets'), + 'name' => 'Default', + 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB + 'allowedFileExtensions' => [], + 'enabled' => true, + 'compression' => 'gzip', + 'encryption' => true, + 'antivirus' => true, + 'fileSecurity' => true, + '$permissions' => [ + Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'search' => 'buckets Default', + ])); + + $bucket = $dbForConsole->getDocument('buckets', 'default'); + + Console::success('[Setup] - Creating files collection for default bucket...'); + + $files = $collections['buckets']['files'] ?? []; + if (empty($files)) { + throw new Exception('Files collection is not configured.'); + } + + $attributes = []; + $indexes = []; + + foreach ($files['attributes'] as $attribute) { + $attributes[] = new Document([ + '$id' => ID::custom($attribute['$id']), + 'type' => $attribute['type'], + 'size' => $attribute['size'], + 'required' => $attribute['required'], + 'signed' => $attribute['signed'], + 'array' => $attribute['array'], + 'filters' => $attribute['filters'], + 'default' => $attribute['default'] ?? null, + 'format' => $attribute['format'] ?? '' + ]); + } + + foreach ($files['indexes'] as $index) { + $indexes[] = new Document([ + '$id' => ID::custom($index['$id']), + 'type' => $index['type'], + 'attributes' => $index['attributes'], + 'lengths' => $index['lengths'], + 'orders' => $index['orders'], + ]); + } + + $dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); + } + + $connections->reclaim(); + + Console::success('[Setup] - Server database init completed...'); + Console::success('Server started successfully'); } catch (\Throwable $e) { - Console::success('[Setup] - Skip: metadata table already exists'); - return true; + Console::warning('Database not ready: ' . $e->getMessage()); + exit(1); } - - if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) { - $audit = new Audit($dbForConsole, $authorization); - $audit->setup(); - } - - if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) { - $abuse = new TimeLimit("", 0, 1, $dbForConsole, $authorization); - $abuse->setup(); - } - - /** @var array $collections */ - $collections = Config::getParam('collections', []); - $consoleCollections = $collections['console']; - foreach ($consoleCollections as $key => $collection) { - if (($collection['$collection'] ?? '') !== Database::METADATA) { - continue; - } - if (!$dbForConsole->getCollection($key)->isEmpty()) { - continue; - } - - Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...'); - - $attributes = []; - $indexes = []; - - foreach ($collection['attributes'] as $attribute) { - $attributes[] = new Document([ - '$id' => ID::custom($attribute['$id']), - 'type' => $attribute['type'], - 'size' => $attribute['size'], - 'required' => $attribute['required'], - 'signed' => $attribute['signed'], - 'array' => $attribute['array'], - 'filters' => $attribute['filters'], - 'default' => $attribute['default'] ?? null, - 'format' => $attribute['format'] ?? '' - ]); - } - - foreach ($collection['indexes'] as $index) { - $indexes[] = new Document([ - '$id' => ID::custom($index['$id']), - 'type' => $index['type'], - 'attributes' => $index['attributes'], - 'lengths' => $index['lengths'], - 'orders' => $index['orders'], - ]); - } - - $dbForConsole->createCollection($key, $attributes, $indexes); - } - - if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) { - Console::success('[Setup] - Creating default bucket...'); - $dbForConsole->createDocument('buckets', new Document([ - '$id' => ID::custom('default'), - '$collection' => ID::custom('buckets'), - 'name' => 'Default', - 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB - 'allowedFileExtensions' => [], - 'enabled' => true, - 'compression' => 'gzip', - 'encryption' => true, - 'antivirus' => true, - 'fileSecurity' => true, - '$permissions' => [ - Permission::create(Role::any()), - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'search' => 'buckets Default', - ])); - - $bucket = $dbForConsole->getDocument('buckets', 'default'); - - Console::success('[Setup] - Creating files collection for default bucket...'); - - $files = $collections['buckets']['files'] ?? []; - if (empty($files)) { - throw new Exception('Files collection is not configured.'); - } - - $attributes = []; - $indexes = []; - - foreach ($files['attributes'] as $attribute) { - $attributes[] = new Document([ - '$id' => ID::custom($attribute['$id']), - 'type' => $attribute['type'], - 'size' => $attribute['size'], - 'required' => $attribute['required'], - 'signed' => $attribute['signed'], - 'array' => $attribute['array'], - 'filters' => $attribute['filters'], - 'default' => $attribute['default'] ?? null, - 'format' => $attribute['format'] ?? '' - ]); - } - - foreach ($files['indexes'] as $index) { - $indexes[] = new Document([ - '$id' => ID::custom($index['$id']), - 'type' => $index['type'], - 'attributes' => $index['attributes'], - 'lengths' => $index['lengths'], - 'orders' => $index['orders'], - ]); - } - - $dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); - } - - $connections->reclaim(); - - Console::success('[Setup] - Server database init completed...'); - Console::success('Server started successfully'); }); Http::init()