From 74a524ded12c6527745957ac219e1ca34828aa6c Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Fri, 10 Apr 2026 13:37:26 +0200 Subject: switch routing to attributes --- composer.json | 3 +- composer.lock | 235 +++++++++++++++- src/App.php | 1 - src/Controllers/AccountController.php | 29 -- src/Controllers/Client/ClientController.php | 292 ++++++++++++++++++++ src/Controllers/Client/KeyController.php | 47 ++++ src/Controllers/Client/RoomController.php | 295 ++++++++++++++++++++ .../Client/ServerInformationController.php | 57 ++++ src/Controllers/Client/UserController.php | 71 +++++ src/Controllers/KeyController.php | 78 ------ src/Controllers/LoginController.php | 151 ----------- src/Controllers/RoomController.php | 299 --------------------- .../Server/ServerInformationController.php | 30 +++ src/Controllers/ServerDiscoveryController.php | 34 --- src/Controllers/ServerImplementationController.php | 29 -- src/Controllers/SyncController.php | 126 --------- src/Controllers/UserController.php | 57 ---- src/Models/User.php | 7 +- src/Router.php | 109 ++++++++ src/Router/Router.php | 109 -------- src/Router/routes_client_server.php | 116 -------- src/Router/routes_server_server.php | 24 -- 22 files changed, 1142 insertions(+), 1057 deletions(-) delete mode 100755 src/Controllers/AccountController.php create mode 100644 src/Controllers/Client/ClientController.php create mode 100644 src/Controllers/Client/KeyController.php create mode 100755 src/Controllers/Client/RoomController.php create mode 100644 src/Controllers/Client/ServerInformationController.php create mode 100755 src/Controllers/Client/UserController.php delete mode 100644 src/Controllers/KeyController.php delete mode 100644 src/Controllers/LoginController.php delete mode 100755 src/Controllers/RoomController.php create mode 100644 src/Controllers/Server/ServerInformationController.php delete mode 100644 src/Controllers/ServerDiscoveryController.php delete mode 100644 src/Controllers/ServerImplementationController.php delete mode 100755 src/Controllers/SyncController.php delete mode 100755 src/Controllers/UserController.php create mode 100644 src/Router.php delete mode 100644 src/Router/Router.php delete mode 100644 src/Router/routes_client_server.php delete mode 100644 src/Router/routes_server_server.php diff --git a/composer.json b/composer.json index 5a1178e..2b51506 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "psr/log": "^3.0", "symfony/dotenv": "^7.3", "symfony/http-foundation": "^7.3", - "symfony/routing": "^7.3" + "symfony/routing": "^7.3", + "symfony/config": "^8.0" }, "require-dev": { "guzzlehttp/guzzle": "^7.9", diff --git a/composer.lock b/composer.lock index d720eb4..253dba4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4964ee19b991107643bde192528a6119", + "content-hash": "1db0630d632f53cdc961a9f51269f376", "packages": [ { "name": "psr/http-message", @@ -109,6 +109,84 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "symfony/config", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "c7369cc1da250fcbfe0c5a9d109e419661549c39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/c7369cc1da250fcbfe0c5a9d109e419661549c39", + "reference": "c7369cc1da250fcbfe0c5a9d109e419661549c39", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, { "name": "symfony/deprecation-contracts", "version": "v3.6.0", @@ -254,6 +332,76 @@ ], "time": "2025-07-10T08:29:33+00:00" }, + { + "name": "symfony/filesystem", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/66b769ae743ce2d13e435528fbef4af03d623e5a", + "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, { "name": "symfony/http-foundation", "version": "v7.3.3", @@ -337,6 +485,89 @@ ], "time": "2025-08-20T08:04:18+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.32.0", @@ -2691,5 +2922,5 @@ "prefer-lowest": false, "platform": {}, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/src/App.php b/src/App.php index 9b1edf9..33f71ef 100644 --- a/src/App.php +++ b/src/App.php @@ -2,7 +2,6 @@ namespace App; -use App\Router\Router; use Symfony\Component\Dotenv\Dotenv; class App diff --git a/src/Controllers/AccountController.php b/src/Controllers/AccountController.php deleted file mode 100755 index 8e20880..0000000 --- a/src/Controllers/AccountController.php +++ /dev/null @@ -1,29 +0,0 @@ -getId()); - - return new JsonResponse(new ClientAccountWhoamiGetResponse( - userId: $user->getId(), - deviceId: $device->getId(), - )); - } -} diff --git a/src/Controllers/Client/ClientController.php b/src/Controllers/Client/ClientController.php new file mode 100644 index 0000000..053d288 --- /dev/null +++ b/src/Controllers/Client/ClientController.php @@ -0,0 +1,292 @@ +getContent(), true); + RequestValidator::validateJson(); + + // validate login type + $loginType = null; + try { + $loginType = LoginType::from($body["type"]); + } catch (\ValueError $error) { + throw new UnknownError("Bad login type.", Response::HTTP_BAD_REQUEST); + } + + // get user id + $userId = Parser::parseUser($body["identifier"]["user"]); + if (empty($userId["server"])) { + $userId = "@$userId[username]:$_ENV[DOMAIN]"; + #$userId = "@$userId[username]:localhost"; + } else { + $userId = "@$userId[username]:$userId[server]"; + } + + if ($loginType !== LoginType::PASSWORD) { + throw new AppException(ErrorCode::UNRECOGNIZED, "only password login supported for now", Response::HTTP_SERVICE_UNAVAILABLE); + } + + $user = User::fetchWithPassword($userId, $body["password"]); + + if (! $user) { + throw new AppException(ErrorCode::FORBIDDEN, "Invalid credentials", Response::HTTP_FORBIDDEN); + } + + $deviceId = $body["device_id"] ?? ""; + + $device = null; + $tokens = null; + + // create new device with tokens + if (empty($deviceId)) { + $device = Device::new( + $user->getId(), + initialDisplayName: $body["initial_device_display_name"] ?? "", + ); + $device->insert(); + + $tokens = Tokens::new($userId, $device->getId()); + $tokens->insert(); + } else { // fetch existing device and tokens + $device = $user->fetchDevice($deviceId); + $tokens = Tokens::fetch($userId, $device->getId()); + + if (empty($tokens)) { + throw new AppException( + ErrorCode::UNKNOWN_TOKEN, + "Soft logged out", + Response::HTTP_UNAUTHORIZED, + ["soft_logout" => true], + ); + } + } + + return new JsonResponse(new ClientLoginPostResponse( + accessToken: $tokens->getAccessToken(), + deviceId:$device->getId(), + userId: $user->getId(), + expiresInMilliseconds: $tokens->getExpiresIn(), + refreshToken: $tokens->getRefreshToken(), + )); + } + + #[Route(path: "_matrix/client/v3/register", methods: ["POST"])] + public function register(Request $request): Response + { + $body = json_decode($request->getContent(), true); + RequestValidator::validateJson(); + + // validate kind + $kind = null; + try { + $kind = UserRegistrationKind::from($request->query->get("kind") ?? "user"); + } catch (\ValueError $error) { + throw new UnknownError("Bad registration kind.", Response::HTTP_BAD_REQUEST); + } + + $username = $body["username"]; + $userId = "@$username:$_ENV[DOMAIN]"; + + Database::getInstance()->query("insert into users (id, password) values (:id, :password)", [ + "id" => $userId, + "password" => $body["password"], + ]); + + $device_id = $body["device_id"] ?? ""; + $initialDeviceDisplayName = $body["initial_device_display_name"] ?? ""; + + $device = Device::new($userId, $device_id, $initialDeviceDisplayName); + $device->insert(); + + $tokens = Tokens::new($userId, $device->getId()); + $tokens->insert(); + + return new JsonResponse(new ClientRegisterPostResponse( + accessToken: $tokens->getAccessToken(), + deviceId: $device->getId(), + expiresInMilliseconds: $tokens->getExpiresIn(), + refreshToken: $tokens->getRefreshToken(), + userId: $userId, + )); + } + + /** + * @see https://spec.matrix.org/v1.15/client-server-api/#get_matrixclientv3sync + * @see https://spec.matrix.org/v1.15/client-server-api/#extensions-to-sync + */ + #[Route(path: "_matrix/client/r0/sync", methods: ["GET"])] + #[Route(path: "_matrix/client/v3/sync", methods: ["GET"])] + public function sync(Request $request): Response + { + $user = User::authenticateWithRequest($request); + + $filter = $request->query->get("filter", ""); + $syncFullState = $request->query->get("full_state", false); + $setPresence = PresenceState::tryFrom($request->query->get("set_presence") ?? "") ?? PresenceState::ONLINE; + $since = $request->query->get("since", ""); + $timeout = $request->query->get("timeout", 0); + $useStateAfter = $request->query->get("use_state_after", false); + + if (! empty($filter)) { + if (str_starts_with($filter, "{")) { + $filter = json_decode($filter, true); + } else { + $filter = Database::getInstance()->query("select * from filters where id=:id", ["id" => $filter])->fetch(); + } + } + + $rooms = Database::getInstance()->query(<< $user->getId(), + ])->fetchAll(); + + $invitedRooms = []; + $joinedRooms = []; + $knockedRooms = []; + $leftRooms = []; + + foreach ($rooms as $room) { + $events = Database::getInstance()->query(<< $room["room_id"], + #"limit" => ($filter["room"]["timeline"]["limit"] ?? false) ? "limit " . $filter["room"]["timeline"]["limit"] : "", + ])->fetchAll(); + + if ($since === "" && MembershipState::tryFrom($room["state"]) === MembershipState::JOIN) { + $joinedRooms[$room["room_id"]] = new JoinedRoom( + accountData: new AccountData([]), + ephemeral: new Ephemeral([]), + state: new State([]), + summary: new RoomSummary( + heroes: [], + invitedMemberCount: 0, + joinedMemberCount: 1, + ), + timeline: new Timeline( + events: array_map([RoomEvent::class, "transformEvent"], $events), + limited: false,# $filter["room"]["timeline"]["limit"] ?? false, + previousBatch: null, + ), + unreadNotifications: new UnreadNotificationCounts(0, 0), + unreadThreadNotifications: [], + ); + } + } + + return new JsonResponse(new ClientSyncGetResponse( + nextBatch: "1", + + accountData: new AccountData([]), + + deviceLists: new DeviceLists([], []), + + deviceOneTimeKeysCount: [ + "signed_curve25519" => 10, + ], + + presence: new Presence([ + new PresenceEvent( + sender: $user->getId(), + presence: $setPresence, + ), + ]), + + rooms: new Rooms( + $invitedRooms, + $joinedRooms, + $knockedRooms, + $leftRooms, + ), + + toDevice: new ToDevice([]), + )); + } + + #[Route(path: "/_matrix/client/v3/refresh", methods: ["POST"])] + public function refresh(Request $request): Response + { + $body = json_decode($request->getContent(), true); + RequestValidator::validateJson(); + + $tokens = Tokens::fetchWithRefreshToken($body["refresh_token"]); + + if (empty($tokens)) { + throw new AppException( + ErrorCode::UNKNOWN_TOKEN, + "Soft logged out", + Response::HTTP_UNAUTHORIZED, + ["soft_logout" => true], + ); + } + + $newTokens = Tokens::new($tokens->getUserId(), $tokens->getDeviceId()); + $newTokens->insert(); + + return new JsonResponse(new ClientRefreshPostResponse( + accessToken: $newTokens->getAccessToken(), + expiresInMilliseconds: $newTokens->getExpiresIn(), + refreshToken: $newTokens->getRefreshToken(), + )); + } +} diff --git a/src/Controllers/Client/KeyController.php b/src/Controllers/Client/KeyController.php new file mode 100644 index 0000000..b9ae61f --- /dev/null +++ b/src/Controllers/Client/KeyController.php @@ -0,0 +1,47 @@ +getContent(), true); + RequestValidator::validateJson(); + + $deviceKeys = $body["device_keys"]; + $timeout = $body["timeout"] ?? 10000; + + foreach ($deviceKeys as $keysUserId => $deviceIds) {} + + return new JsonResponse([ + "device_keys" => [], + ]); + } + + #[Route(path: "/_matrix/client/r0/keys/upload", methods: ["POST"])] + #[Route(path: "/_matrix/client/v3/keys/upload", methods: ["POST"])] + public function upload(Request $request): Response + { + $user = User::authenticateWithRequest($request); + $body = json_decode($request->getContent(), true); + RequestValidator::validateJson(); + + foreach ($body["one_time_keys"] as $identifier => $object) {} + + return new JsonResponse(new ClientKeysUploadPostResponse([ + #"curve25519" => 0, + "signed_curve25519" => count($body["one_time_keys"]), + ])); + } +} diff --git a/src/Controllers/Client/RoomController.php b/src/Controllers/Client/RoomController.php new file mode 100755 index 0000000..ec04a2f --- /dev/null +++ b/src/Controllers/Client/RoomController.php @@ -0,0 +1,295 @@ +getContent(), true); + RequestValidator::validateJson(); + + $creationContent = $body["creation_content"] ?? []; + $initialState = $body["initial_state"] ?? []; + $invite = $body["invite"] ?? []; + $body["invite_3pid"] ?? []; + $body["is_direct"] ?? false; + $name = $body["name"] ?? ""; + $body["power_level_content_override"] ?? []; + $preset = $body["preset"] ?? ""; + $roomAliasName = $body["room_alias_name"] ?? ""; + $roomVersion = $body["room_version"] ?? "DEFAULT_ROOM_VERSION"; + $topic = $body["topic"] ?? ""; + $visibility = RoomVisibility::tryFrom($body["visibility"] ?? "private"); + + if (! $preset) { + $preset = $visibility->value . "_chat"; + } + + # TODO: get events for preset + # TODO: override preset events with initial state + # TODO: override events with name and topic if applicable + $state = []; + + if ($name) { + $state[EventType::ROOM_NAME->value] = $name; + } + + if ($topic) { + $state[EventType::ROOM_TOPIC->value] = $topic; + } + + // create room + $roomId = Id::generateRoomId(); + Database::getInstance()->query(<< $roomId, + "name" => $roomAliasName, # "#$roomAliasName:$_ENV[DOMAIN]", + ]); + + $roomCreateEvent = new RoomEvent(new CreateEvent( + eventId: Id::generateEventId(), + originServerTimestamp: time(), + roomId: $roomId, + sender: $user->getId(), + roomVersion: "12", + )); + $roomCreateEvent->insert(); + + $roomMemberEvent = new RoomEvent(new MemberEvent( + eventId: Id::generateEventId(), + originServerTimestamp: time(), + roomId: $roomId, + sender: $user->getId(), + stateKey: $user->getId(), + isDirect: false, + membership: MembershipState::JOIN, + displayName: $user->getName(), + )); + $roomMemberEvent->insert(); + + $roomPowerLevelsEvent = new RoomEvent(new PowerLevelsEvent( + eventId: Id::generateEventId(), + originServerTimestamp: time(), + roomId: $roomId, + sender: $user->getId(), + )); + $roomPowerLevelsEvent->insert(); + + $roomJoinRulesEvent = new RoomEvent(new JoinRulesEvent( + eventId: Id::generateEventId(), + originServerTimestamp: time(), + roomId: $roomId, + sender: $user->getId(), + joinRule: RoomJoinRule::INVITE, + )); + $roomJoinRulesEvent->insert(); + + $roomHistoryVisibilityEvent = new RoomEvent(new HistoryVisibilityEvent( + eventId: Id::generateEventId(), + originServerTimestamp: time(), + roomId: $roomId, + sender: $user->getId(), + historyVisibility: RoomHistoryVisibility::SHARED, + )); + $roomHistoryVisibilityEvent->insert(); + + $roomGuestAccessEvent = new RoomEvent(new GuestAccessEvent( + eventId: Id::generateEventId(), + originServerTimestamp: time(), + roomId: $roomId, + sender: $user->getId(), + guestAccess: RoomGuestAccess::CAN_JOIN, + )); + $roomGuestAccessEvent->insert(); + + $roomNameEvent = new RoomEvent(new NameEvent( + eventId: Id::generateEventId(), + originServerTimestamp: time(), + roomId: $roomId, + sender: $user->getId(), + name: $roomAliasName, + )); + $roomNameEvent->insert(); + + Database::getInstance()->query(<< $roomId, + "user_id" => $user->getId(), + "state" => MembershipState::JOIN->value, + ]); + + return new JsonResponse([ + "room_id" => $roomId, + ]); + } + + /** + * @see https://spec.matrix.org/v1.15/client-server-api/#get_matrixclientv3directoryroomroomalias + */ + #[Route(path: "/_matrix/client/v3/directory/room/{roomAlias}", methods: ["GET"])] + public function resolveAlias(Request $request): Response + { + $alias = $request->attributes->get("roomAlias"); + + $roomAlias = Parser::parseRoomAlias($alias); # TODO: on parse error => 400 + $roomId = null; + + if ($roomAlias["server"] != $_ENV["DOMAIN"]) { + # TODO: federation API resolve + $roomId = -1; + } + else { + $room = Database::getInstance()->query(<< $roomAlias["name"], + ])->fetch(); + + $roomId = $room["id"] ?? null; + } + + if (empty($roomId)) { + throw new AppException( + ErrorCode::NOT_FOUND, + "Room alias $alias not found.", + Response::HTTP_NOT_FOUND + ); + } + + return new JsonResponse([ + "room_id" => $roomId, + "servers" => [], + ]); + } + + #[Route(path: "/_matrix/client/v3/{roomId}/messages", methods: ["GET"])] + public function getMessages(Request $request): Response + { + $user = User::authenticateWithRequest($request); + + $roomId = $request->attributes->get("roomId"); + + $membership = Database::getInstance() + ->query("select state from room_memberships where user_id=:user_id and room_id=:room_id", [ + "user_id" => $user->getId(), + "room_id" => $roomId, + ]) + ->fetchColumn(); + + if (MembershipState::from($membership) !== MembershipState::JOIN) { + throw new Exception(ErrorCode::FORBIDDEN, "You aren't a member of the room.", Response::HTTP_FORBIDDEN); + } + + $direction = $request->query->get("dir"); + $filter = $request->query->get("filter"); + $from = $request->query->get("from"); + $limit = $request->query->get("limit", 10); + $to = $request->query->get("to"); + + $events = Database::getInstance()->query(<< $roomId, + #"limit" => ($filter["room"]["timeline"]["limit"] ?? false) ? "limit " . $filter["room"]["timeline"]["limit"] : "", + ])->fetchAll(); + + return new JsonResponse([ + "chunk" => array_map([RoomEvent::class, "transformEvent"], $events), + "end" => "", + "start" => "", + "state" => [], + ]); + } + + #[Route(path: "/_matrix/client/v3/rooms/{roomId}/read_markers", methods: ["POST"])] + public function readMarkers(Request $request): Response + { + $user = User::authenticateWithRequest($request); + $body = json_decode($request->getContent(), true); + RequestValidator::validateJson(); + + $roomId = $request->attributes->get("roomId"); + + return new JsonResponse(); + } + + /** + * @see https://spec.matrix.org/v1.15/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid + */ + #[Route(path: "/_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}", methods: ["PUT"])] + public function send(Request $request): Response + { + $user = User::authenticateWithRequest($request); + + if (empty($user)) { + throw new UnauthorizedError(); + } + + $roomId = $request->attributes->get("roomId"); + $eventType = EventType::from($request->attributes->get("eventType")); + $transactionId = $request->attributes->get("txnId"); + + $body = json_decode($request->getContent(), true); + RequestValidator::validateJson(); + + // validate msgtype + MessageType::from($body["msgtype"]); + + $eventId = Id::generateEventId(); + $event = new RoomEvent(new ClientEvent( + content: $body, + eventId: $eventId, + originServerTimestamp: time(), + roomId: $roomId, + sender: $user->getId(), + type: $eventType, + unsigned: new UnsignedData( + age: 1234, # TODO + membership: MembershipState::JOIN, + ), + )); + $event->insert(); + + return new JsonResponse([ + "event_id" => $eventId, + ]); + } +} diff --git a/src/Controllers/Client/ServerInformationController.php b/src/Controllers/Client/ServerInformationController.php new file mode 100644 index 0000000..51e68e6 --- /dev/null +++ b/src/Controllers/Client/ServerInformationController.php @@ -0,0 +1,57 @@ + [ + "base_url" => "https://$_ENV[DOMAIN]", + ], + ]); + } + + #[Route(path: "/.well-known/matrix/support", methods: ["GET"])] + public function support(Request $request): Response + { + return new JsonResponse([ + "contacts" => [], + "support_page" => "", + ]); + } + + #[Route(path: "/_matrix/client/versions", methods: ["GET"])] + public function versions(Request $request): Response + { + return new JsonResponse(new ClientVersionsGetResponse([ + "r0.0.1", + "v1.1", + ])); + } + + public function server(Request $request): Response + { + return new JsonResponse([ + "m.server" => "$_ENV[DOMAIN]:443", + ]); + } + + public function version(Request $request): Response + { + return new JsonResponse([ + "server" => [ + "name" => "Matrix PHP", + "version" => "0.1.0", + ], + ]); + } +} diff --git a/src/Controllers/Client/UserController.php b/src/Controllers/Client/UserController.php new file mode 100755 index 0000000..038caba --- /dev/null +++ b/src/Controllers/Client/UserController.php @@ -0,0 +1,71 @@ +getId()); + + return new JsonResponse(new ClientAccountWhoamiGetResponse( + userId: $user->getId(), + deviceId: $device->getId(), + )); + } + + /** + * @see https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3useruseridfilter + */ + #[Route(path: "/_matrix/client/r0/user/{userId}/filter", methods: ["POST"])] + #[Route(path: "/_matrix/client/v3/user/{userId}/filter", methods: ["POST"])] + public function uploadFilter(Request $request): Response + { + $accessToken = str_replace("Bearer ", "", $request->headers->get("authorization") ?: ""); + $user = User::fetchWithAccessToken($accessToken); + + if (empty($user)) { + throw new UnauthorizedError(); + } + + $userId = $request->get("userId"); + if ($user->getId() !== $userId) { + throw new UnauthorizedError(); + } + + $body = json_decode($request->getContent(), true); + RequestValidator::validateJson(); + + $filterId = md5($userId . random_bytes(512)); + + Database::getInstance()->query(<< $filterId, + "account_data" => isset($body["account_data"]) ? json_encode($body["account_data"]) : null, + "event_fields" => isset($body["event_fields"]) ? json_encode($body["event_fields"]) : null, + "event_format" => isset($body["event_format"]) ? json_encode($body["event_format"]) : null, + "presence" => isset($body["presence"]) ? json_encode($body["presence"]) : null, + "room" => isset($body["room"]) ? json_encode($body["room"]) : null, + "user_id" => $userId, + ]); + + return new JsonResponse([ + "filter_id" => $filterId, + ]); + } +} diff --git a/src/Controllers/KeyController.php b/src/Controllers/KeyController.php deleted file mode 100644 index ebf580a..0000000 --- a/src/Controllers/KeyController.php +++ /dev/null @@ -1,78 +0,0 @@ - [ - "name" => "Matrix PHP", - "version" => "0.1.0", - ], - ]); - } - - /** - * POST /_matrix/client/v3/keys/upload - */ - public function upload(Request $request): Response - { - $user = User::authenticateWithRequest($request); - $body = json_decode($request->getContent(), true); - RequestValidator::validateJson(); - - foreach ($body["one_time_keys"] as $identifier => $data) {} - - return new JsonResponse(new ClientKeysUploadPostResponse([ - #"curve25519" => 0, - "signed_curve25519" => count($body["one_time_keys"]), - ])); - } - - public function query(Request $request): Response - { - $serverName = $request->attributes->get("serverName"); - } - - /** - * POST /_matrix/client/v3/refresh - */ - public function refresh(Request $request): Response - { - $body = json_decode($request->getContent(), true); - RequestValidator::validateJson(); - - $tokens = Tokens::fetchWithRefreshToken($body["refresh_token"]); - - if (empty($tokens)) { - throw new AppException( - ErrorCode::UNKNOWN_TOKEN, - "Soft logged out", - Response::HTTP_UNAUTHORIZED, - ["soft_logout" => true], - ); - } - - $newTokens = Tokens::new($tokens->getUserId(), $tokens->getDeviceId()); - $newTokens->insert(); - - return new JsonResponse(new ClientRefreshPostResponse( - accessToken: $newTokens->getAccessToken(), - expiresInMilliseconds: $newTokens->getExpiresIn(), - refreshToken: $newTokens->getRefreshToken(), - )); - } -} diff --git a/src/Controllers/LoginController.php b/src/Controllers/LoginController.php deleted file mode 100644 index f9576fb..0000000 --- a/src/Controllers/LoginController.php +++ /dev/null @@ -1,151 +0,0 @@ -getContent(), true); - RequestValidator::validateJson(); - - // validate login type - $loginType = null; - try { - $loginType = LoginType::from($body["type"]); - } catch (\ValueError $error) { - throw new UnknownError("Bad login type.", Response::HTTP_BAD_REQUEST); - } - - // get user id - $userId = Parser::parseUser($body["identifier"]["user"]); - if (empty($userId["server"])) { - #$userId = "@$userId[username]:$_ENV[DOMAIN]"; - $userId = "@$userId[username]:localhost"; - } else { - $userId = "@$userId[username]:$userId[server]"; - } - - #if ($loginType == LoginType::PASSWORD) {} - - $user = User::fetchWithPassword($userId, $body["password"]); - - if (! $user) { - throw new AppException(ErrorCode::FORBIDDEN, "Invalid credentials", Response::HTTP_FORBIDDEN); - } - - $deviceId = $body["device_id"] ?? ""; - - $device = null; - $tokens = null; - - // create new device with tokens - if (empty($deviceId)) { - $device = Device::new( - $user->getId(), - initialDisplayName: $body["initial_device_display_name"] ?? "", - ); - $device->insert(); - - $tokens = Tokens::new($userId, $device->getId()); - $tokens->insert(); - } else { // fetch existing device and tokens - $device = $user->fetchDevice($deviceId); - $tokens = Tokens::fetch($userId, $device->getId()); - - if (empty($tokens)) { - throw new AppException( - ErrorCode::UNKNOWN_TOKEN, - "Soft logged out", - Response::HTTP_UNAUTHORIZED, - ["soft_logout" => true], - ); - } - } - - return new JsonResponse(new ClientLoginPostResponse( - accessToken: $tokens->getAccessToken(), - deviceId:$device->getId(), - userId: $user->getId(), - expiresInMilliseconds: $tokens->getExpiresIn(), - refreshToken: $tokens->getRefreshToken(), - )); - } - - /** - * POST /_matrix/client/v3/register - */ - public function register(Request $request): Response - { - $body = json_decode($request->getContent(), true); - RequestValidator::validateJson(); - - // validate kind - $kind = null; - try { - $kind = UserRegistrationKind::from($request->query->get("kind") ?? "user"); - } catch (\ValueError $error) { - throw new UnknownError("Bad registration kind.", Response::HTTP_BAD_REQUEST); - } - - $username = $body["username"]; - $userId = "@$username:$_ENV[DOMAIN]"; - - Database::getInstance()->query("insert into users (id, password) values (:id, :password)", [ - "id" => $userId, - "password" => $body["password"], - ]); - - $device_id = $body["device_id"] ?? ""; - $initialDeviceDisplayName = $body["initial_device_display_name"] ?? ""; - - $device = Device::new($userId, $device_id, $initialDeviceDisplayName); - $device->insert(); - - $tokens = Tokens::new($userId, $device->getId()); - $tokens->insert(); - - return new JsonResponse(new ClientRegisterPostResponse( - accessToken: $tokens->getAccessToken(), - deviceId: $device->getId(), - expiresInMilliseconds: $tokens->getExpiresIn(), - refreshToken: $tokens->getRefreshToken(), - userId: $userId, - )); - } -} diff --git a/src/Controllers/RoomController.php b/src/Controllers/RoomController.php deleted file mode 100755 index 060a030..0000000 --- a/src/Controllers/RoomController.php +++ /dev/null @@ -1,299 +0,0 @@ -getContent(), true); - RequestValidator::validateJson(); - - $creationContent = $body["creation_content"] ?? []; - $initialState = $body["initial_state"] ?? []; - $invite = $body["invite"] ?? []; - $body["invite_3pid"] ?? []; - $body["is_direct"] ?? false; - $name = $body["name"] ?? ""; - $body["power_level_content_override"] ?? []; - $preset = $body["preset"] ?? ""; - $roomAliasName = $body["room_alias_name"] ?? ""; - $roomVersion = $body["room_version"] ?? "DEFAULT_ROOM_VERSION"; - $topic = $body["topic"] ?? ""; - $visibility = RoomVisibility::tryFrom($body["visibility"] ?? "private"); - - if (! $preset) { - $preset = $visibility->value . "_chat"; - } - - # TODO: get events for preset - # TODO: override preset events with initial state - # TODO: override events with name and topic if applicable - $state = []; - - if ($name) { - $state[EventType::ROOM_NAME->value] = $name; - } - - if ($topic) { - $state[EventType::ROOM_TOPIC->value] = $topic; - } - - // create room - $roomId = Id::generateRoomId(); - Database::getInstance()->query(<< $roomId, - "name" => $roomAliasName, # "#$roomAliasName:$_ENV[DOMAIN]", - ]); - - $roomCreateEvent = new RoomEvent(new CreateEvent( - eventId: Id::generateEventId(), - originServerTimestamp: time(), - roomId: $roomId, - sender: $user->getId(), - roomVersion: "12", - )); - $roomCreateEvent->insert(); - - $roomMemberEvent = new RoomEvent(new MemberEvent( - eventId: Id::generateEventId(), - originServerTimestamp: time(), - roomId: $roomId, - sender: $user->getId(), - stateKey: $user->getId(), - isDirect: false, - membership: MembershipState::JOIN, - displayName: $user->getName(), - )); - $roomMemberEvent->insert(); - - $roomPowerLevelsEvent = new RoomEvent(new PowerLevelsEvent( - eventId: Id::generateEventId(), - originServerTimestamp: time(), - roomId: $roomId, - sender: $user->getId(), - )); - $roomPowerLevelsEvent->insert(); - - $roomJoinRulesEvent = new RoomEvent(new JoinRulesEvent( - eventId: Id::generateEventId(), - originServerTimestamp: time(), - roomId: $roomId, - sender: $user->getId(), - joinRule: RoomJoinRule::INVITE, - )); - $roomJoinRulesEvent->insert(); - - $roomHistoryVisibilityEvent = new RoomEvent(new HistoryVisibilityEvent( - eventId: Id::generateEventId(), - originServerTimestamp: time(), - roomId: $roomId, - sender: $user->getId(), - historyVisibility: RoomHistoryVisibility::SHARED, - )); - $roomHistoryVisibilityEvent->insert(); - - $roomGuestAccessEvent = new RoomEvent(new GuestAccessEvent( - eventId: Id::generateEventId(), - originServerTimestamp: time(), - roomId: $roomId, - sender: $user->getId(), - guestAccess: RoomGuestAccess::CAN_JOIN, - )); - $roomGuestAccessEvent->insert(); - - $roomNameEvent = new RoomEvent(new NameEvent( - eventId: Id::generateEventId(), - originServerTimestamp: time(), - roomId: $roomId, - sender: $user->getId(), - name: $roomAliasName, - )); - $roomNameEvent->insert(); - - Database::getInstance()->query(<< $roomId, - "user_id" => $user->getId(), - "state" => MembershipState::JOIN->value, - ]); - - return new JsonResponse([ - "room_id" => $roomId, - ]); - } - - /** - * GET /_matrix/client/v3/directory/room/{roomAlias} - * - * @see https://spec.matrix.org/v1.15/client-server-api/#get_matrixclientv3directoryroomroomalias - */ - public function resolveAlias(Request $request): Response - { - $alias = $request->attributes->get("roomAlias"); - - $roomAlias = Parser::parseRoomAlias($alias); # TODO: on parse error => 400 - $roomId = null; - - if ($roomAlias["server"] != $_ENV["DOMAIN"]) { - # TODO: federation API resolve - $roomId = -1; - } - else { - $room = Database::getInstance()->query(<< $roomAlias["name"], - ])->fetch(); - - $roomId = $room["id"] ?? null; - } - - if (empty($roomId)) { - throw new AppException( - ErrorCode::NOT_FOUND, - "Room alias $alias not found.", - Response::HTTP_NOT_FOUND - ); - } - - return new JsonResponse([ - "room_id" => $roomId, - "servers" => [], - ]); - } - - /** - * GET /_matrix/client/v3/rooms/{roomId}/messages - */ - public function getMessages(Request $request): Response - { - $user = User::authenticateWithRequest($request); - - $roomId = $request->attributes->get("roomId"); - - $membership = Database::getInstance() - ->query("select state from room_memberships where user_id=:user_id and room_id=:room_id", [ - "user_id" => $user->getId(), - "room_id" => $roomId, - ]) - ->fetchColumn(); - - if (MembershipState::from($membership) !== MembershipState::JOIN) { - throw new Exception(ErrorCode::FORBIDDEN, "You aren't a member of the room.", Response::HTTP_FORBIDDEN); - } - - $direction = $request->query->get("dir"); - $filter = $request->query->get("filter"); - $from = $request->query->get("from"); - $limit = $request->query->get("limit", 10); - $to = $request->query->get("to"); - - $events = Database::getInstance()->query(<< $roomId, - #"limit" => ($filter["room"]["timeline"]["limit"] ?? false) ? "limit " . $filter["room"]["timeline"]["limit"] : "", - ])->fetchAll(); - - return new JsonResponse([ - "chunk" => array_map([RoomEvent::class, "transformEvent"], $events), - "end" => "", - "start" => "", - "state" => [], - ]); - } - - /** - * POST /_matrix/client/v3/rooms/{roomId}/read_markers - */ - public function readMarkers(Request $request): Response - { - $user = User::authenticateWithRequest($request); - $body = json_decode($request->getContent(), true); - RequestValidator::validateJson(); - - $roomId = $request->attributes->get("roomId"); - - return new JsonResponse(); - } - - /** - * PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId} - * - * @see https://spec.matrix.org/v1.15/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid - */ - public function send(Request $request): Response - { - $user = User::authenticateWithRequest($request); - - if (empty($user)) { - throw new UnauthorizedError(); - } - - $roomId = $request->attributes->get("roomId"); - $eventType = EventType::from($request->attributes->get("eventType")); - $transactionId = $request->attributes->get("txnId"); - - $body = json_decode($request->getContent(), true); - RequestValidator::validateJson(); - - // validate msgtype - MessageType::from($body["msgtype"]); - - $eventId = Id::generateEventId(); - $event = new RoomEvent(new ClientEvent( - content: $body, - eventId: $eventId, - originServerTimestamp: time(), - roomId: $roomId, - sender: $user->getId(), - type: $eventType, - unsigned: new UnsignedData( - age: 1234, # TODO - membership: MembershipState::JOIN, - ), - )); - $event->insert(); - - return new JsonResponse([ - "event_id" => $eventId, - ]); - } -} diff --git a/src/Controllers/Server/ServerInformationController.php b/src/Controllers/Server/ServerInformationController.php new file mode 100644 index 0000000..a77bcca --- /dev/null +++ b/src/Controllers/Server/ServerInformationController.php @@ -0,0 +1,30 @@ + "$_ENV[DOMAIN]:443", + ]); + } + + #[Route(path: "/_matrix/federation/v1/version", methods: ["GET"])] + public function version(Request $request): Response + { + return new JsonResponse([ + "server" => [ + "name" => "Matrix PHP", + "version" => "0.1.0", + ], + ]); + } +} diff --git a/src/Controllers/ServerDiscoveryController.php b/src/Controllers/ServerDiscoveryController.php deleted file mode 100644 index 917df14..0000000 --- a/src/Controllers/ServerDiscoveryController.php +++ /dev/null @@ -1,34 +0,0 @@ - "$_ENV[DOMAIN]:443", - ]); - } - - public function client(Request $request): Response - { - return new JsonResponse([ - "m.homeserver" => [ - "base_url" => "http://$_ENV[DOMAIN]", - ], - ]); - } - - public function support(Request $request): Response - { - return new JsonResponse([ - "contacts" => [], - "support_page" => "", - ]); - } -} diff --git a/src/Controllers/ServerImplementationController.php b/src/Controllers/ServerImplementationController.php deleted file mode 100644 index 93b9a3f..0000000 --- a/src/Controllers/ServerImplementationController.php +++ /dev/null @@ -1,29 +0,0 @@ - [ - "name" => "Matrix PHP", - "version" => "0.1.0", - ], - ]); - } - - public function versions(Request $request): Response - { - return new JsonResponse([ - "versions" => [ - "v1.1", - ], - ]); - } -} diff --git a/src/Controllers/SyncController.php b/src/Controllers/SyncController.php deleted file mode 100755 index acebb11..0000000 --- a/src/Controllers/SyncController.php +++ /dev/null @@ -1,126 +0,0 @@ -query->get("filter", ""); - $syncFullState = $request->query->get("full_state", false); - $setPresence = PresenceState::tryFrom($request->query->get("set_presence") ?? "") ?? PresenceState::ONLINE; - $since = $request->query->get("since", ""); - $timeout = $request->query->get("timeout", 0); - $useStateAfter = $request->query->get("use_state_after", false); - - if (! empty($filter)) { - if (str_starts_with($filter, "{")) { - $filter = json_decode($filter, true); - } else { - $filter = Database::getInstance()->query("select * from filters where id=:id", ["id" => $filter])->fetch(); - } - } - - $rooms = Database::getInstance()->query(<< $user->getId(), - ])->fetchAll(); - - $invitedRooms = []; - $joinedRooms = []; - $knockedRooms = []; - $leftRooms = []; - - foreach ($rooms as $room) { - $events = Database::getInstance()->query(<< $room["room_id"], - #"limit" => ($filter["room"]["timeline"]["limit"] ?? false) ? "limit " . $filter["room"]["timeline"]["limit"] : "", - ])->fetchAll(); - - if ($since === "" && MembershipState::tryFrom($room["state"]) === MembershipState::JOIN) { - $joinedRooms[$room["room_id"]] = new JoinedRoom( - accountData: new AccountData([]), - ephemeral: new Ephemeral([]), - state: new State([]), - summary: new RoomSummary( - heroes: [], - invitedMemberCount: 0, - joinedMemberCount: 1, - ), - timeline: new Timeline( - events: array_map([RoomEvent::class, "transformEvent"], $events), - limited: false,# $filter["room"]["timeline"]["limit"] ?? false, - previousBatch: null, - ), - unreadNotifications: new UnreadNotificationCounts(0, 0), - unreadThreadNotifications: [], - ); - } - } - - return new JsonResponse(new ClientSyncGetResponse( - nextBatch: "1", - - accountData: new AccountData([]), - - deviceLists: new DeviceLists([], []), - - deviceOneTimeKeysCount: [ - "signed_curve25519" => 10, - ], - - presence: new Presence([ - new PresenceEvent( - sender: $user->getId(), - presence: $setPresence, - ), - ]), - - rooms: new Rooms( - $invitedRooms, - $joinedRooms, - $knockedRooms, - $leftRooms, - ), - - toDevice: new ToDevice([]), - )); - } -} diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php deleted file mode 100755 index d102160..0000000 --- a/src/Controllers/UserController.php +++ /dev/null @@ -1,57 +0,0 @@ -headers->get("authorization") ?: ""); - $user = User::fetchWithAccessToken($accessToken); - - if (empty($user)) { - throw new UnauthorizedError(); - } - - $userId = $request->get("userId"); - if ($user->getId() !== $userId) { - throw new UnauthorizedError(); - } - - $body = json_decode($request->getContent(), true); - RequestValidator::validateJson(); - - $filterId = md5($userId . random_bytes(512)); - - Database::getInstance()->query(<< $filterId, - "account_data" => isset($body["account_data"]) ? json_encode($body["account_data"]) : null, - "event_fields" => isset($body["event_fields"]) ? json_encode($body["event_fields"]) : null, - "event_format" => isset($body["event_format"]) ? json_encode($body["event_format"]) : null, - "presence" => isset($body["presence"]) ? json_encode($body["presence"]) : null, - "room" => isset($body["room"]) ? json_encode($body["room"]) : null, - "user_id" => $userId, - ]); - - return new JsonResponse([ - "filter_id" => $filterId, - ]); - } -} diff --git a/src/Models/User.php b/src/Models/User.php index b8aad62..b24afaf 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -22,7 +22,7 @@ class User implements ConnectsToDatabase { return new self( $row["id"], - $row["name"], + $row["name"] ?? "", ); } @@ -82,6 +82,11 @@ class User implements ConnectsToDatabase public static function authenticateWithRequest(Request $request): self { $accessToken = str_replace("Bearer ", "", $request->headers->get("authorization") ?: ""); + + if (empty($accessToken)) { + throw new AppException(ErrorCode::UNAUTHORIZED, "Missing access token", Response::HTTP_UNAUTHORIZED); + } + $user = self::fetchWithAccessToken($accessToken); if (empty($user)) { diff --git a/src/Router.php b/src/Router.php new file mode 100644 index 0000000..cda3006 --- /dev/null +++ b/src/Router.php @@ -0,0 +1,109 @@ +setDefault("_controller", [$class->getName(), $method->getName()]); + } + }); + + $this->routes = $loader->load(__DIR__ . "/Controllers"); + } + + /** + * match the current url against the routes. + * also add CORS headers. + */ + public function run(): Response + { + $request = Request::createFromGlobals(); + + $response = new Response(); + $corsHeaders = [ + "Access-Control-Allow-Origin" => "*", + "Access-Control-Allow-Methods" => "GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD", + "Access-Control-Allow-Headers" => "X-Requested-With, Content-Type, Authorization", + ]; + + // handle OPTIONS + if ($request->isMethod("OPTIONS")) { + $response->headers->add($corsHeaders); + + return $response; + } + + $context = new RequestContext(); + $context->fromRequest($request); + + try { + $matcher = new UrlMatcher($this->routes, $context); + $match = $matcher->matchRequest($request); + + $class = $match["_controller"][0]; + $method = $match["_controller"][1]; + + $request->attributes->add(array_diff_key( + $match, + array_flip(["_controller", "_route"]) + )); + + Logger::logRequestToFile($request); + + $response = (new $class)->$method($request); + } catch (Exception $exception) { + $response = ErrorResponse::fromException($exception); + } catch (ResourceNotFoundException $exception) { + $response = new ErrorResponse(ErrorCode::NOT_FOUND, "404", Response::HTTP_NOT_FOUND); + } catch (MethodNotAllowedException $exception) { + $response = new ErrorResponse(ErrorCode::FORBIDDEN, "403", Response::HTTP_FORBIDDEN); + } catch (\LogicException $exception) { // display logic exceptions normally + throw $exception; + } catch (\Exception $exception) { + $response = new ErrorResponse( + ErrorCode::UNKNOWN, + $exception->getMessage() ?: "Unknown error occured", + Response::HTTP_INTERNAL_SERVER_ERROR + ); + } catch (\Error $error) { + error_log($error->getMessage() ?: "Unknown error occured"); + + $response = new ErrorResponse( + ErrorCode::UNKNOWN, + $error->getMessage() ?: "Unknown error occured", + Response::HTTP_INTERNAL_SERVER_ERROR + ); + } + + // add cors headers to all responses + $response->headers->add($corsHeaders); + + return $response; + } +} diff --git a/src/Router/Router.php b/src/Router/Router.php deleted file mode 100644 index 6859771..0000000 --- a/src/Router/Router.php +++ /dev/null @@ -1,109 +0,0 @@ -routes = new RouteCollection(); - $this->configurator = new RouteConfigurator($this->routes, $this->routes); - - $this->addRoutes(); - } - - /** - * match the current url against the routes. - * also add preflight CORS headers on OPTIONS requests. - */ - public function run(): Response - { - $request = Request::createFromGlobals(); - - $response = new Response(); - $response->headers->add([ - "Access-Control-Allow-Origin" => "*", - "Access-Control-Allow-Methods" => "GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD", - "Access-Control-Allow-Headers" => "X-Requested-With, Content-Type, Authorization", - ]); - - if ($request->isMethod("OPTIONS")) { - return $response; - } - - $context = new RequestContext(); - $context->fromRequest($request); - - try { - $matcher = new UrlMatcher($this->routes, $context); - $match = $matcher->matchRequest($request); - - $class = $match["_controller"][0]; - $method = $match["_controller"][1]; - - $request->attributes->add(array_diff_key( - $match, - array_flip(["_controller", "_route"]) - )); - - Logger::logRequestToFile($request); - - $response = (new $class)->$method($request); - } catch (Exception $exception) { - $response = ErrorResponse::fromException($exception); - } catch (ResourceNotFoundException $exception) { - $response = new ErrorResponse(ErrorCode::NOT_FOUND, "404", Response::HTTP_NOT_FOUND); - } catch (MethodNotAllowedException $exception) { - $response = new ErrorResponse(ErrorCode::FORBIDDEN, "403", Response::HTTP_FORBIDDEN); - } catch (\LogicException $exception) { // display logic exceptions normally - throw $exception; - } catch (\Exception $exception) { - $response = new ErrorResponse( - ErrorCode::UNKNOWN, - $exception->getMessage() ?: "Unknown error occured", - Response::HTTP_INTERNAL_SERVER_ERROR - ); - } catch (\Error $error) { - error_log($error->getMessage() ?: "Unknown error occured"); - - $response = new ErrorResponse( - ErrorCode::UNKNOWN, - $error->getMessage() ?: "Unknown error occured", - Response::HTTP_INTERNAL_SERVER_ERROR - ); - } - - return $response; - } - - /** - * add routes from the routes file - */ - private function addRoutes(): void - { - $routesClientServer = include_once(__DIR__ . "/routes_client_server.php"); - $routesClientServer($this->configurator); - - $routesServerServer = include_once(__DIR__ . "/routes_server_server.php"); - $routesServerServer($this->configurator); - } -} diff --git a/src/Router/routes_client_server.php b/src/Router/routes_client_server.php deleted file mode 100644 index e888782..0000000 --- a/src/Router/routes_client_server.php +++ /dev/null @@ -1,116 +0,0 @@ -add("well_known_matrix_client", "/.well-known/matrix/client") - ->controller([ServerDiscoveryController::class, "client"]) - ->methods(["GET"]); - - $routes - ->add("well_known_matrix_support", "/.well-known/matrix/support") - ->controller([ServerDiscoveryController::class, "support"]) - ->methods(["GET"]); - - $routes - ->add("matrix_client_versions", "/_matrix/client/versions") - ->controller([ServerImplementationController::class, "versions"]) - ->methods(["GET"]); - - $supportedLoginTypes = [LoginController::class, "supportedLoginTypes"]; - $routes - ->add("matrix_client_r0_login_types", "/_matrix/client/r0/login") - ->controller($supportedLoginTypes) - ->methods(["GET"]); - $routes - ->add("matrix_client_v3_login_types", "/_matrix/client/v3/login") - ->controller($supportedLoginTypes) - ->methods(["GET"]); - - $routes - ->add("matrix_client_r0_login", "/_matrix/client/r0/login") - ->controller([LoginController::class, "login"]) - ->methods(["POST"]); - - $routes - ->add("matrix_client_v3_login", "/_matrix/client/v3/login") - ->controller([LoginController::class, "login"]) - ->methods(["POST"]); - - $routes - ->add("matrix_client_v3_keys_upload", "/_matrix/client/v3/keys/upload") - ->controller([KeyController::class, "upload"]) - ->methods(["POST"]); - - $routes - ->add("matrix_client_r0_keys_upload", "/_matrix/client/r0/keys/upload") - ->controller([KeyController::class, "upload"]) - ->methods(["POST"]); - - $routes - ->add("matrix_client_v3_sync", "/_matrix/client/v3/sync") - ->controller([SyncController::class, "sync"]) - ->methods(["GET"]); - - $routes - ->add("matrix_client_r0_sync", "/_matrix/client/r0/sync") - ->controller([SyncController::class, "sync"]) - ->methods(["GET"]); - - $routes - ->add("matrix_client_v3_refresh", "/_matrix/client/v3/refresh") - ->controller([KeyController::class, "refresh"]) - ->methods(["POST"]); - - $routes - ->add("matrix_client_v3_directory_room_alias_get", "/_matrix/client/v3/directory/room/{roomAlias}") - ->controller([RoomController::class, "resolveAlias"]) - ->methods(["GET"]); - - $routes - ->add("matrix_client_v3_account_whoami", "/_matrix/client/v3/account/whoami") - ->controller([AccountController::class, "whoami"]) - ->methods(["GET"]); - - $routes - ->add("matrix_client_v3_rooms_id_send_event_transaction", "/_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}") - ->controller([RoomController::class, "send"]) - ->methods(["PUT"]); - - $routes - ->add("matrix_client_r0_user_id_filter", "/_matrix/client/r0/user/{userId}/filter") - ->controller([UserController::class, "uploadFilter"]) - ->methods(["POST"]); - - $routes - ->add("matrix_client_v3_user_id_filter", "/_matrix/client/v3/user/{userId}/filter") - ->controller([UserController::class, "uploadFilter"]) - ->methods(["POST"]); - - $routes - ->add("matrix_client_v3_room_create", "/_matrix/client/v3/createRoom") - ->controller([RoomController::class, "createRoom"]) - ->methods(["POST"]); - - $routes - ->add("matrix_client_v3_rooms_id_read_markers", "/_matrix/client/v3/rooms/{roomId}/read_markers") - ->controller([RoomController::class, "readMarkers"]) - ->methods(["POST"]); - - $routes - ->add("matrix_client_v3_rooms_id_messages", "/_matrix/client/v3/rooms/{roomId}/messages") - ->controller([RoomController::class, "getMessages"]) - ->methods(["GET"]); -}; diff --git a/src/Router/routes_server_server.php b/src/Router/routes_server_server.php deleted file mode 100644 index 2e85a17..0000000 --- a/src/Router/routes_server_server.php +++ /dev/null @@ -1,24 +0,0 @@ -add("well_known_matrix_server", "/.well-known/matrix/server") - ->controller([ServerDiscoveryController::class, "server"]) - ->methods(["GET"]); - - $routes - ->add("matrix_federation_version", "/_matrix/federation/v1/version") - ->controller([ServerImplementationController::class, "version"]) - ->methods(["GET"]); - - # /_matrix/key/v2/server - # /_matrix/key/v2/query - # /_matrix/key/v2/query/{serverName} -}; -- cgit v1.2.3