$parts */ function routeProjects(array $parts, string $method): never { $store = new Store(dirname(__DIR__) . '/data/projects'); if (count($parts) === 1) { if ($method === 'GET') { Response::json($store->list()); } if ($method === 'POST') { $body = Response::body(); $name = trim((string) ($body['name'] ?? '')); if ($name === '' || mb_strlen($name) > 120) { Response::error('Project name must be 1–120 characters', 422); } Response::json($store->create($name), 201); } Response::error('Method not allowed', 405); } $id = $parts[1]; if (!Store::isValidId($id)) { Response::error('Invalid project id', 400); } if ($method === 'GET') { $doc = $store->load($id); if ($doc === null) { Response::error('Project not found', 404); } Response::json($doc); } if ($method === 'PUT') { $body = Response::body(); if (!isset($body['revision']) || !is_int($body['revision']) || !is_array($body['project'] ?? null)) { Response::error('Body must contain integer "revision" and object "project"', 422); } try { $result = $store->save($id, $body['revision'], $body['project']); } catch (StaleRevisionException $e) { Response::error('Project was modified elsewhere', 409, ['revision' => $e->currentRevision]); } if ($result === null) { Response::error('Project not found', 404); } Response::json(['revision' => $result['revision'], 'updatedAt' => $result['updatedAt']]); } if ($method === 'DELETE') { if (!$store->delete($id)) { Response::error('Project not found', 404); } Response::noContent(); } Response::error('Method not allowed', 405); } /** @param list $parts */ function routeTextures(array $parts, string $method): never { $store = new TextureStore(dirname(__DIR__) . '/data/textures'); if (count($parts) === 1) { if ($method === 'GET') { Response::json($store->list()); } if ($method === 'POST') { $bytes = Response::rawBody(); if (strlen($bytes) > MAX_TEXTURE_BYTES) { Response::error('Image must be at most ' . (MAX_TEXTURE_BYTES / 1024 / 1024) . ' MB', 413); } try { Response::json($store->store($bytes, (string) ($_GET['name'] ?? '')), 201); } catch (InvalidImageException $e) { Response::error($e->getMessage(), 422); } } Response::error('Method not allowed', 405); } if ($method === 'DELETE') { if (!$store->delete($parts[1])) { Response::error('Texture not found', 404); } Response::noContent(); } Response::error('Method not allowed', 405); } /** @param list $parts */ function routeFonts(array $parts, string $method): never { $store = new FontStore(dirname(__DIR__) . '/data/fonts'); if (count($parts) === 1) { if ($method === 'GET') { Response::json($store->list()); } if ($method === 'POST') { $bytes = Response::rawBody(); if (strlen($bytes) > MAX_FONT_BYTES) { Response::error('Font must be at most ' . (MAX_FONT_BYTES / 1024 / 1024) . ' MB', 413); } try { Response::json($store->store($bytes, (string) ($_GET['name'] ?? '')), 201); } catch (InvalidFontException $e) { Response::error($e->getMessage(), 422); } } Response::error('Method not allowed', 405); } if ($method === 'DELETE') { if (!$store->delete($parts[1])) { Response::error('Font not found', 404); } Response::noContent(); } Response::error('Method not allowed', 405); } /** @param list $parts */ function routeSounds(array $parts, string $method): never { $store = new SoundStore(dirname(__DIR__) . '/data/sounds'); if (count($parts) === 1) { if ($method === 'GET') { Response::json($store->list()); } if ($method === 'POST') { $bytes = Response::rawBody(); if (strlen($bytes) > MAX_SOUND_BYTES) { Response::error('Sound must be at most ' . (MAX_SOUND_BYTES / 1024 / 1024) . ' MB', 413); } try { Response::json($store->store($bytes, (string) ($_GET['name'] ?? '')), 201); } catch (InvalidSoundException $e) { Response::error($e->getMessage(), 422); } } Response::error('Method not allowed', 405); } if ($method === 'DELETE') { if (!$store->delete($parts[1])) { Response::error('Sound not found', 404); } Response::noContent(); } Response::error('Method not allowed', 405); } /** @param list $parts */ function routePlugins(array $parts, string $method): never { $store = new PluginStore(dirname(__DIR__) . '/data/plugins'); if (count($parts) === 1) { if ($method !== 'GET') { Response::error('Method not allowed', 405); } Response::json($store->list()); } if ($method === 'PUT') { $active = Response::body()['active'] ?? null; if (!is_bool($active)) { Response::error('active must be true or false', 422); } $entry = $store->setActive($parts[1], $active); if ($entry === null) { Response::error('Plugin not found', 404); } Response::json($entry); } if ($method !== 'GET') { Response::error('Method not allowed', 405); } $source = $store->read($parts[1]); if ($source === null) { Response::error('Plugin not found', 404); } // Switched off means not run: the editor never asks for such a module, and nobody else may. if (!$store->isActive($parts[1])) { Response::error('Plugin not active', 403); } header('Content-Type: text/javascript; charset=utf-8'); header('X-Content-Type-Options: nosniff'); // A replaced file has to be the one the next editor start runs. header('Cache-Control: no-cache'); echo $source; exit; } try { if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > MAX_BODY_BYTES) { Response::error('Request body too large', 413); } $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $route = trim((string) ($_GET['r'] ?? ''), '/'); $parts = $route === '' ? [] : explode('/', $route); if (count($parts) < 1 || count($parts) > 2) { Response::error('Not found', 404); } match ($parts[0]) { 'projects' => routeProjects($parts, $method), 'textures' => routeTextures($parts, $method), 'fonts' => routeFonts($parts, $method), 'sounds' => routeSounds($parts, $method), 'plugins' => routePlugins($parts, $method), default => Response::error('Not found', 404), }; } catch (Throwable $e) { error_log('[game-maker api] ' . $e->getMessage()); Response::error('Internal server error', 500); }