From 4c7bde1400820f36caf8b2a5374007384c3018f3 Mon Sep 17 00:00:00 2001 From: winter Date: Mon, 9 Dec 2024 21:31:54 +0000 Subject: rename to Digitigrade / PawPub :3 --- Digitigrade/Db.php | 42 ++++ Digitigrade/Db/Migrator.php | 52 +++++ Digitigrade/GlobalConfig.php | 44 +++++ Digitigrade/HttpResponseStatus.php | 7 + Digitigrade/HttpResponseStatus/BadRequest.php | 18 ++ .../HttpResponseStatus/InternalServerError.php | 23 +++ Digitigrade/HttpResponseStatus/NoHandlerFound.php | 18 ++ Digitigrade/HttpResponseStatus/NotFound.php | 18 ++ Digitigrade/Model.php | 219 +++++++++++++++++++++ Digitigrade/Model/Actor.php | 90 +++++++++ Digitigrade/Model/ActorEndpoints.php | 27 +++ Digitigrade/Model/FetchableModel.php | 78 ++++++++ Digitigrade/Model/Note.php | 95 +++++++++ Digitigrade/Model/NoteAttachment.php | 23 +++ Digitigrade/Model/NotePrivacy.php | 39 ++++ Digitigrade/Model/NotePrivacyInteractors.php | 9 + Digitigrade/Model/NotePrivacyScope.php | 9 + Digitigrade/RemoteFetchable.php | 21 ++ Digitigrade/Router.php | 98 +++++++++ Digitigrade/Singleton.php | 18 ++ WpfTest/Db.php | 42 ---- WpfTest/Db/Migrator.php | 52 ----- WpfTest/GlobalConfig.php | 44 ----- WpfTest/HttpResponseStatus.php | 7 - WpfTest/HttpResponseStatus/BadRequest.php | 18 -- WpfTest/HttpResponseStatus/InternalServerError.php | 23 --- WpfTest/HttpResponseStatus/NoHandlerFound.php | 18 -- WpfTest/HttpResponseStatus/NotFound.php | 18 -- WpfTest/Model.php | 219 --------------------- WpfTest/Model/Actor.php | 90 --------- WpfTest/Model/ActorEndpoints.php | 27 --- WpfTest/Model/FetchableModel.php | 78 -------- WpfTest/Model/Note.php | 95 --------- WpfTest/Model/NoteAttachment.php | 23 --- WpfTest/Model/NotePrivacy.php | 39 ---- WpfTest/Model/NotePrivacyInteractors.php | 9 - WpfTest/Model/NotePrivacyScope.php | 9 - WpfTest/RemoteFetchable.php | 21 -- WpfTest/Router.php | 98 --------- WpfTest/Singleton.php | 18 -- bin/migrate | 2 +- composer.json | 2 +- config.ini | 6 +- index.php | 2 +- migrations/20241207_183000_create_actor.php | 2 +- migrations/20241208_111936_create_note.php | 2 +- .../20241208_165411_create_note_mentions.php | 2 +- .../20241208_170639_add_note_attachment_type.php | 2 +- misc/path_to_uri.php | 2 +- routes/actor.php | 8 +- routes/homepage.php | 2 +- routes/nodeinfo.php | 12 +- routes/note.php | 6 +- routes/webfinger.php | 10 +- 54 files changed, 978 insertions(+), 978 deletions(-) create mode 100644 Digitigrade/Db.php create mode 100644 Digitigrade/Db/Migrator.php create mode 100644 Digitigrade/GlobalConfig.php create mode 100644 Digitigrade/HttpResponseStatus.php create mode 100644 Digitigrade/HttpResponseStatus/BadRequest.php create mode 100644 Digitigrade/HttpResponseStatus/InternalServerError.php create mode 100644 Digitigrade/HttpResponseStatus/NoHandlerFound.php create mode 100644 Digitigrade/HttpResponseStatus/NotFound.php create mode 100644 Digitigrade/Model.php create mode 100644 Digitigrade/Model/Actor.php create mode 100644 Digitigrade/Model/ActorEndpoints.php create mode 100644 Digitigrade/Model/FetchableModel.php create mode 100644 Digitigrade/Model/Note.php create mode 100644 Digitigrade/Model/NoteAttachment.php create mode 100644 Digitigrade/Model/NotePrivacy.php create mode 100644 Digitigrade/Model/NotePrivacyInteractors.php create mode 100644 Digitigrade/Model/NotePrivacyScope.php create mode 100644 Digitigrade/RemoteFetchable.php create mode 100644 Digitigrade/Router.php create mode 100644 Digitigrade/Singleton.php delete mode 100644 WpfTest/Db.php delete mode 100644 WpfTest/Db/Migrator.php delete mode 100644 WpfTest/GlobalConfig.php delete mode 100644 WpfTest/HttpResponseStatus.php delete mode 100644 WpfTest/HttpResponseStatus/BadRequest.php delete mode 100644 WpfTest/HttpResponseStatus/InternalServerError.php delete mode 100644 WpfTest/HttpResponseStatus/NoHandlerFound.php delete mode 100644 WpfTest/HttpResponseStatus/NotFound.php delete mode 100644 WpfTest/Model.php delete mode 100644 WpfTest/Model/Actor.php delete mode 100644 WpfTest/Model/ActorEndpoints.php delete mode 100644 WpfTest/Model/FetchableModel.php delete mode 100644 WpfTest/Model/Note.php delete mode 100644 WpfTest/Model/NoteAttachment.php delete mode 100644 WpfTest/Model/NotePrivacy.php delete mode 100644 WpfTest/Model/NotePrivacyInteractors.php delete mode 100644 WpfTest/Model/NotePrivacyScope.php delete mode 100644 WpfTest/RemoteFetchable.php delete mode 100644 WpfTest/Router.php delete mode 100644 WpfTest/Singleton.php diff --git a/Digitigrade/Db.php b/Digitigrade/Db.php new file mode 100644 index 0000000..c9f9df9 --- /dev/null +++ b/Digitigrade/Db.php @@ -0,0 +1,42 @@ +conn = new \PDO($conf->getDbConnection(), $conf->getDbUser(), $conf->getDbPassword(), [ + \PDO::ATTR_PERSISTENT => true, + ]); + } + + public function runInitialSchema() { + $this->conn->exec(file_get_contents(self::INIT_SCHEMA_PATH)); + } + + public function getDbVersion(): int { + $result = $this->conn->query('SELECT db_version FROM db_version'); + return $result->fetch()['db_version']; + } + + /** + * Changes the database schema version + * + * CAREFUL! this shouldn't be used unless you absolutely know why + * @param int $newVersion the new version number + * @return void + */ + public function setDbVersion(int $newVersion) { + $stmt = $this->conn->prepare('UPDATE db_version SET db_version = ?'); + $stmt->execute([$newVersion]); + } + + public function getPdo(): \PDO { + return $this->conn; + } +} \ No newline at end of file diff --git a/Digitigrade/Db/Migrator.php b/Digitigrade/Db/Migrator.php new file mode 100644 index 0000000..a1b4e8b --- /dev/null +++ b/Digitigrade/Db/Migrator.php @@ -0,0 +1,52 @@ +migrations[$newVersion] = $fn; + } + + /** + * Runs all available migrations + * @return void + */ + public function migrate() { + $db = Db::getInstance(); + $currentVersion = null; + try { + $currentVersion = $db->getDbVersion(); + error_log('starting on version ' . $currentVersion); + } catch (\PDOException $e) { + if ($e->getCode() == '42P01') { // undefined table + error_log("initialising database because db version table doesn't exist"); + $db->runInitialSchema(); + $currentVersion = $db->getDbVersion(); + error_log('starting on version ' . $currentVersion); + } else + throw $e; + } + + // find migrations that haven't been run yet + $available = array_filter($this->migrations, function (int $key) use ($currentVersion) { + return $key > $currentVersion; + }, ARRAY_FILTER_USE_KEY); + + try { + // run them in order (at least they should be in order?) + foreach (array_keys($available) as $version) { + $fn = $available[$version]; + $filename = basename((new \ReflectionFunction($fn))->getFileName()); + error_log("running migration $filename"); + $available[$version]($db->getPdo()); + $db->setDbVersion($version); + } + } finally { + error_log('now on version ' . $db->getDbVersion()); + } + } +} \ No newline at end of file diff --git a/Digitigrade/GlobalConfig.php b/Digitigrade/GlobalConfig.php new file mode 100644 index 0000000..f17b758 --- /dev/null +++ b/Digitigrade/GlobalConfig.php @@ -0,0 +1,44 @@ +confData = parse_ini_file(self::CONFIG_INI_PATH, process_sections: true); + } + + public function getCanonicalHost(): string { + $host = $this->confData['instance']['hostname']; + if (isset($this->confData['instance']['port']) && $this->confData['instance']['port'] != ($this->isHttps() ? 443 : 80)) + $host .= ':' . $this->confData['instance']['port']; + return $host; + } + + public function isHttps(): bool { + return $this->confData['debug']['https'] ?? true; + } + + public function getBasePath(): string { + return ($this->isHttps() ? 'https://' : 'http://') . $this->getCanonicalHost(); + } + + public function shouldReportTraces(): bool { + return $this->confData['debug']['send_tracebacks'] ?? false; + } + + public function getDbConnection(): string { + return 'pgsql:host=' . $this->confData['database']['host'] . + ';port=' . $this->confData['database']['port'] . + ';dbname=' . $this->confData['database']['database']; + } + + public function getDbUser(): string { + return $this->confData['database']['username']; + } + + public function getDbPassword(): string { + return $this->confData['database']['password']; + } +} \ No newline at end of file diff --git a/Digitigrade/HttpResponseStatus.php b/Digitigrade/HttpResponseStatus.php new file mode 100644 index 0000000..e169833 --- /dev/null +++ b/Digitigrade/HttpResponseStatus.php @@ -0,0 +1,7 @@ +message = $reason ?? 'Request does not conform to expectations'; + } + + public function httpCode(): int { + return 400; + } + + public function httpReason(): string { + return "Bad Request"; + } +} \ No newline at end of file diff --git a/Digitigrade/HttpResponseStatus/InternalServerError.php b/Digitigrade/HttpResponseStatus/InternalServerError.php new file mode 100644 index 0000000..237c526 --- /dev/null +++ b/Digitigrade/HttpResponseStatus/InternalServerError.php @@ -0,0 +1,23 @@ +message = $reason::class . ': ' . $reason->getMessage(); + if (isset($context)) { + $this->message .= " [$context]"; + } + if (GlobalConfig::getInstance()->shouldReportTraces()) { + $this->message .= "\n" . $reason->getTraceAsString(); + } + } + public function httpCode(): int { + return 500; + } + public function httpReason(): string { + return "Internal Server Error"; + } +} \ No newline at end of file diff --git a/Digitigrade/HttpResponseStatus/NoHandlerFound.php b/Digitigrade/HttpResponseStatus/NoHandlerFound.php new file mode 100644 index 0000000..871b873 --- /dev/null +++ b/Digitigrade/HttpResponseStatus/NoHandlerFound.php @@ -0,0 +1,18 @@ +message = "No handler found for path $path"; + } + + public function httpCode(): int { + return 404; + } + + public function httpReason(): string { + return "Not Found"; + } +} \ No newline at end of file diff --git a/Digitigrade/HttpResponseStatus/NotFound.php b/Digitigrade/HttpResponseStatus/NotFound.php new file mode 100644 index 0000000..643976c --- /dev/null +++ b/Digitigrade/HttpResponseStatus/NotFound.php @@ -0,0 +1,18 @@ +message = $reason ?? 'Request was handled but no appropriate resource found'; + } + + public function httpCode(): int { + return 404; + } + + public function httpReason(): string { + return "Not Found"; + } +} \ No newline at end of file diff --git a/Digitigrade/Model.php b/Digitigrade/Model.php new file mode 100644 index 0000000..8a593eb --- /dev/null +++ b/Digitigrade/Model.php @@ -0,0 +1,219 @@ +id) && static::find($this->{'id'}) != null) { + return 'id = ' . $this->{'id'}; + } + return null; + } + + /** + * Converts a field or type name into the version used in the database + */ + private static function mangleName(string $name): string { + $chars = str_split($name); + $chars[0] = strtolower($chars[0]); + for ($i = 0; $i < count($chars); $i++) { + $c = $chars[$i]; + if (ctype_upper($c)) { + array_splice($chars, $i, 1, ['_', strtolower($c)]); + } + } + return implode($chars); + } + + /** + * Converts a database column name into the corresponding field name + */ + private static function unmangleName(string $name): string { + $chars = str_split($name); + for ($i = 0; $i < count($chars); $i++) { + if ($chars[$i] == '_') { + array_splice($chars, $i, 2, [strtoupper($chars[$i + 1])]); + } + } + return implode($chars); + } + + public function toDbRow(): array { + $refl = new \ReflectionObject($this); + $props = $refl->getProperties(); + $result = []; + foreach ($props as $p) { + $pName = $p->getName(); + if (!isset($this->{$pName})) + continue; + // we need to determine the plain value to insert ... + $value = $this->{$pName}; + $discarded = false; + if ($value instanceof \DateTimeInterface) { + // datetime objects should be turned into iso 8601 timestamps + $value = $value->format('c'); + } elseif ($value instanceof Model) { + // models should generally be turned into their id if there is one or removed if not + if (property_exists($value, 'id') && isset($value->id)) + $value = $value->{'id'}; // doing it like this to shut up the warning (lol) + else + $discarded = true; + } elseif (is_array($value)) { + // arrays ought to be handled separately when saving (different db table) + $discarded = true; + } + if (!$discarded) + $result[self::mangleName($pName)] = $value; + } + return $result; + } + + public static function fromDbRow(array|false $columns): ?static { + if ($columns === false) + return null; + // create the model + $obj = new static(); + $refl = new \ReflectionObject($obj); + // for each field of the db row ... + foreach (array_keys($columns) as $name) { + $value = $columns[$name]; + $name = self::unmangleName($name); // guess the appropriate field in the model + try { + // try to assign it directly (works for simple values) + $obj->{$name} = $value; + } catch (\TypeError $e) { + // if it's not the right type we have to try to wrangle it into the right one + // there's a few strategies for this + $type = $refl->getProperty($name)->getType(); + if ($type == null || !is_a($type, \ReflectionNamedType::class)) + throw $e; + $typeClass = $type->getName(); + if (enum_exists($typeClass)) { + // if it's a backed enum we can find the enum case with the same value + $obj->{$name} = $typeClass::from($value); + } elseif (is_subclass_of($typeClass, Model::class) && is_int($value)) { + // if it's another model we can try to look it up by id automatically + $obj->{$name} = $typeClass::find($value); + } else { + // otherwise try to instantiate the correct class and pass it the simple value + $obj->{$name} = new $typeClass($value); + } + } + } + // set up any referenced fields if needed + $obj->hydrate(); + $obj->saved = true; + return $obj; + } + + /** + * Initialises fields of the model that contain referenced / foreign keyed objects + * which couldn't be initialised automatically + * @return void + */ + protected function hydrate() { + } + + public static function findWhere(string $whereClause, array $parameters): ?static { + $classNameParts = explode('\\', static::class); + $className = $classNameParts[count($classNameParts) - 1]; + $tableName = self::mangleName($className); + + $stmt = Db::getInstance()->getPdo()->prepare("SELECT * FROM $tableName WHERE $whereClause LIMIT 1"); + $stmt->execute($parameters); + return static::fromDbRow($stmt->fetch(\PDO::FETCH_ASSOC)); + } + + public static function findAllWhere(string $whereClause, array $parameters): array { + $classNameParts = explode('\\', static::class); + $className = $classNameParts[count($classNameParts) - 1]; + $tableName = self::mangleName($className); + + $stmt = Db::getInstance()->getPdo()->prepare("SELECT * FROM $tableName WHERE $whereClause"); + $stmt->execute($parameters); + return array_map(function ($row) { + return static::fromDbRow($row); + }, $stmt->fetchAll(\PDO::FETCH_ASSOC)); + } + + public static function find(int $id): ?static { + return static::findWhere('id = ?', [$id]); + } + + /** + * Inserts or updates the model's corresponding database record. + * Also sets the model's id field if it has one. + */ + public function save() { + // figure out all the parameters and such for the query + $paramData = $this->toDbRow(); + $columnNames = array_keys($paramData); + $parameterNames = array_map(function (string $column) { + return ":$column"; + }, $columnNames); + $columns = implode(', ', $columnNames); + $parameters = implode(', ', $parameterNames); + + $classNameParts = explode('\\', static::class); + $className = $classNameParts[count($classNameParts) - 1]; + $tableName = self::mangleName($className); + + $db = Db::getInstance()->getPdo(); + + $whereClause = $this->getUpdateWhereClause($db); + $shouldUpdate = $whereClause != null; + $stmt = $db->prepare( + $shouldUpdate + ? "UPDATE $tableName SET ($columns) = ($parameters) WHERE $whereClause" + : "INSERT INTO $tableName ($columns) VALUES ($parameters)" + ); + + // pdo is really stupid and casts `false`/0 to an empty string, which breaks the query + // so i have to cast `false`s and `0`s manually. hooray + // i also have to convert backed enums to their value because they dont implement Stringable + $paramData = array_map(function ($item) { + if ($item === false || $item === 0) + return '0'; + if (is_a($item, \BackedEnum::class)) + return $item->value; + return $item; + }, $paramData); + // actually save it !!!! yay + $stmt->execute($paramData); + + if (property_exists($this, 'id')) { + $this->id = $shouldUpdate + ? static::findWhere($whereClause, [])->id + : (int) $db->lastInsertId(); + } + + // recursively save inner models + // but not fetchable ones because they should really be saved separately + $reflProps = (new \ReflectionObject($this))->getProperties(); + foreach ($reflProps as $reflProp) { + assert($reflProp instanceof \ReflectionProperty); + $reflType = $reflProp->getType(); + $propName = $reflProp->getName(); + if (!($reflType instanceof \ReflectionNamedType)) + continue; + $type = $reflType->getName(); + if (is_subclass_of($type, Model::class) && !is_subclass_of($type, FetchableModel::class)) { + if (property_exists($this, 'id')) + $this->{$propName}->setOwnerId($this->id); + $this->{$propName}->save(); + } + } + + $this->saved = true; + } +} \ No newline at end of file diff --git a/Digitigrade/Model/Actor.php b/Digitigrade/Model/Actor.php new file mode 100644 index 0000000..946265e --- /dev/null +++ b/Digitigrade/Model/Actor.php @@ -0,0 +1,90 @@ +uri]) != null) + return 'uri = ' . $db->quote($this->uri); + if (self::findWhere('id = ?', [$this->id]) != null) + return "id = $this->id"; + return null; + } + + public static function findLocalByHandle(string $handle): ?self { + return self::findWhere('is_local = true AND handle = ?', [$handle]); + } + + public static function findByWebfinger(string $acct, bool $autoSave = true, bool $forceRefetch = false): ?self { + // normalise uri + if (!str_starts_with($acct, 'acct:')) { + if (str_starts_with($acct, '@')) { + $acct = substr($acct, 1); + } + $acct = "acct:$acct"; + } + // fetch it + $domain = explode('@', $acct, 2)[1]; + $data = @file_get_contents("https://$domain/.well-known/webfinger?resource=$acct", + context: stream_context_create([ + 'http' => ['header' => 'Accept: application/jrd+json, application/json'] + ]) + ); + if ($data === false) + return null; + $data = json_decode($data); + if ($data === null) + return null; + // find the right link + if (!isset($data->links)) + return null; + $matching = array_filter($data->links, function (\stdClass $linkObj) { + return $linkObj->rel == self::REL_URI; + }); + if (count($matching) == 0) + return null; + return self::findByUri($matching[0]->href, $autoSave, $forceRefetch); + } + + public function jsonSerialize(): array { + return [ + 'type' => 'actor', + 'self' => path_to_uri("/user/$this->handle"), + 'created' => $this->created?->format('c'), + 'modified' => $this->modified?->format('c'), + 'homepage' => path_to_uri("/$this->handle"), + 'handle' => $this->handle, + 'displayName' => $this->displayName, + 'bio' => $this->bio, + 'pronouns' => $this->pronouns, + 'automated' => $this->automated, + 'endpoints' => [ + 'basicFeed' => path_to_uri("/user/$this->handle/basicFeed"), + 'fullFeed' => path_to_uri("/user/$this->handle/fullFeed") + ] + ]; + } + + public function hydrate() { + $this->endpoints = ActorEndpoints::findWhere('actor_id = ?', [$this->id]); + } +} \ No newline at end of file diff --git a/Digitigrade/Model/ActorEndpoints.php b/Digitigrade/Model/ActorEndpoints.php new file mode 100644 index 0000000..8821d1c --- /dev/null +++ b/Digitigrade/Model/ActorEndpoints.php @@ -0,0 +1,27 @@ +actorId = $id; + } + + protected function getUpdateWhereClause($db): ?string { + if (self::findWhere('actor_id = ?', [$this->actorId]) != null) { + return "actor_id = $this->actorId"; + } + return null; + } +} \ No newline at end of file diff --git a/Digitigrade/Model/FetchableModel.php b/Digitigrade/Model/FetchableModel.php new file mode 100644 index 0000000..30b68e7 --- /dev/null +++ b/Digitigrade/Model/FetchableModel.php @@ -0,0 +1,78 @@ +bStrictObjectTypeChecking = false; + } + + private static function fetchFromRemote(string $uri, bool $autoSave): ?static { + // fetch the object + $data = @file_get_contents($uri, context: stream_context_create(['http' => [ + 'header' => 'Accept: application/json' + ]])); + if ($data === false) + return null; + $data = json_decode($data); + if ($data === null) + return null; + // default everything nullable to null + $obj = new static(); + $reflProps = (new \ReflectionClass(static::class))->getProperties(); + foreach ($reflProps as $reflProp) { + assert($reflProp instanceof \ReflectionProperty); // for intellisense lmao + if ($reflProp->getType()->allowsNull()) { + $obj->{$reflProp->getName()} = null; + } + } + // rename 'self' to 'uri' because of reasons + if (property_exists($data, 'self')) { + $data->uri = $data->self; + unset($data->self); + } + // map all present fields + self::$mapper->map($data, $obj); + // (hopefully) recursively fetch any other fetchable models contained in this one + foreach ($reflProps as $reflProp) { + assert($reflProp instanceof \ReflectionProperty); + $reflType = $reflProp->getType(); + $propName = $reflProp->getName(); + if (!($reflType instanceof \ReflectionNamedType)) + continue; + $type = $reflType->getName(); + if (is_subclass_of($type, FetchableModel::class) && is_string($data->{$propName})) { + // try to avoid recursion + // TODO: make a better effort at avoiding recursion (e.g. when it's more than one step nested) + if ($data->{$propName} == $uri) { + $obj->{$propName} = $obj; + } else { + $obj->{$propName} = $type::findByUri($data->{$propName}, $autoSave); + } + } + } + + if ($autoSave) { + $obj->save(); + } + + return $obj; + } + + public static function findByUri(string $uri, bool $autoSave = true, bool $forceRefetch = false): ?static { + if (!$forceRefetch) { + $obj = static::findWhere('uri = ?', [$uri]); + if ($obj != null) + return $obj; + } + return static::fetchFromRemote($uri, $autoSave); + } +} + +FetchableModel::__initStatic(); \ No newline at end of file diff --git a/Digitigrade/Model/Note.php b/Digitigrade/Model/Note.php new file mode 100644 index 0000000..3b7c362 --- /dev/null +++ b/Digitigrade/Model/Note.php @@ -0,0 +1,95 @@ + + */ + public array $formattedContent = []; + public string $language; + /** + * @var Actor[] + */ + public array $mentions = []; + public ?Note $inReplyTo; + public ?Note $threadApex; + public NotePrivacy $privacy; + /** + * @var NoteAttachment[] + */ + public array $attachments = []; + public array $extensions = []; + + protected function getUpdateWhereClause(\PDO $db): ?string { + if (self::findWhere('uri = ?', [$this->uri]) != null) + return 'uri = ' . $db->quote($this->uri); + if (self::findWhere('id = ?', [$this->id]) != null) + return "id = $this->id"; + return null; + } + + protected function hydrate() { + $this->privacy = NotePrivacy::findWhere('note_id = ?', [$this->id]); + $this->formattedContent = $this->findFormattedContents(); + $this->mentions = $this->findMentions(); + $this->attachments = NoteAttachment::findAllWhere('note_id = ?', [$this->id]); + } + + private function findFormattedContents(): array { + $pdo = Db::getInstance()->getPdo(); + $stmt = $pdo->prepare('SELECT mimetype, body FROM note_formatted_content WHERE note_id = ?'); + $stmt->execute([$this->id]); + return $stmt->fetchAll(\PDO::FETCH_ASSOC); + } + + private function findMentions(): array { + $pdo = Db::getInstance()->getPdo(); + $stmt = $pdo->prepare('SELECT actor_id FROM note_mention WHERE note_id = ?'); + $stmt->execute([$this->id]); + $actors = array_map(function ($actorId) { + return Actor::find($actorId); + }, $stmt->fetchAll(\PDO::FETCH_COLUMN, 0)); + return $actors; + } + + public static function findAllWithAuthor(Actor $author): array { + return self::findAllWhere('author = ?', [$author->id]); + } + + public function jsonSerialize(): array { + return [ + 'type' => 'note', + 'self' => path_to_uri("/post/$this->id"), + 'created' => $this->created->format('c'), + 'modified' => $this->modified?->format('c'), + 'author' => $this->author->uri, + 'summary' => $this->summary, + 'plainContent' => $this->plainContent, + 'formattedContent' => $this->formattedContent, + 'language' => $this->language, + 'mentions' => array_map(function (Actor $actor) { + return $actor->uri; + }, $this->mentions), + 'inReplyTo' => $this->inReplyTo?->uri, + 'threadApex' => $this->threadApex?->uri, + 'privacy' => $this->privacy, + 'attachments' => array_map(function (NoteAttachment $attachment) { + return [ + 'type' => $attachment->type, + 'href' => $attachment->href, + 'description' => $attachment->description + ]; + }, $this->attachments), + //'extensions' => [] + ]; + } +} \ No newline at end of file diff --git a/Digitigrade/Model/NoteAttachment.php b/Digitigrade/Model/NoteAttachment.php new file mode 100644 index 0000000..c47ec64 --- /dev/null +++ b/Digitigrade/Model/NoteAttachment.php @@ -0,0 +1,23 @@ +noteId = $id; + } + + protected function getUpdateWhereClause($db): ?string { + if (self::findWhere('note_id = ?', [$this->noteId]) != null) { + return "note_id = $this->noteId"; + } + return null; + } +} \ No newline at end of file diff --git a/Digitigrade/Model/NotePrivacy.php b/Digitigrade/Model/NotePrivacy.php new file mode 100644 index 0000000..faaacf6 --- /dev/null +++ b/Digitigrade/Model/NotePrivacy.php @@ -0,0 +1,39 @@ +noteId = $id; + } + + protected function getUpdateWhereClause($db): ?string { + if (self::findWhere('note_id = ?', [$this->noteId]) != null) { + return "note_id = $this->noteId"; + } + return null; + } + + public function jsonSerialize(): array { + return [ + 'scope' => $this->scope->value, + 'alsoVisibleTo' => $this->alsoVisibleTo, + 'indexable' => $this->indexable, + 'canReshare' => $this->canReshare->value, + 'canReply' => $this->canReply->value, + 'canOtherInteract' => $this->canOtherInteract->value + ]; + } +} \ No newline at end of file diff --git a/Digitigrade/Model/NotePrivacyInteractors.php b/Digitigrade/Model/NotePrivacyInteractors.php new file mode 100644 index 0000000..8313e90 --- /dev/null +++ b/Digitigrade/Model/NotePrivacyInteractors.php @@ -0,0 +1,9 @@ +requestPath = $_GET['requestPath'] ?? $_SERVER['PATH_INFO'] ?? explode('?', $_SERVER['REQUEST_URI'], 2)[0] ?? '/'; + $this->routes = []; + } + + /** + * Processes and sends a response to the current request + * @return void + */ + public function processRequest() { + try { + $this->processRequestInner(); + } catch (HttpResponseStatus $e) { + http_response_code($e->httpCode()); + echo '

' . $e->httpReason() . '

' . $e->getMessage() . '
'; + } + } + + private function processRequestInner() { + $context = null; + try { + $route = $this->matchPath($this->requestPath); + if ($route == null) { + throw new NoHandlerFound($this->requestPath); + } + $context = 'handler for /' . implode('/', $route[0]); + $route[1]($route[2]); // call handler with args + } catch (\Exception $e) { + if (is_a($e, HttpResponseStatus::class)) { + throw $e; + } + throw new InternalServerError($e, $context); + } catch (\Error $e) { + throw new InternalServerError($e, $context); + } + } + + public function mount(string $pathSpec, callable $handler) { + $this->routes[] = [$this->explodePathPieces($pathSpec), $handler]; + } + + private function explodePathPieces(string $pathSpec): array { + $pieces = explode('/', $pathSpec); + if ($pieces[0] == '') + array_shift($pieces); + return $pieces; + } + + private function matchPath(string $requestPath): ?array { + // find matching candidates + $pieces = $this->explodePathPieces($requestPath); + $candidates = []; + foreach ($this->routes as $route) { + $routePieces = $route[0]; + if (count($routePieces) != count($pieces)) + continue; + $args = []; + $matches = true; + for ($i = 0; $i < count($pieces); $i++) { + if (str_starts_with($routePieces[$i], ':')) { // this is a parameter + $args[substr($routePieces[$i], 1)] = $pieces[$i]; + } elseif ($routePieces[$i] != $pieces[$i]) { + $matches = false; + break; + } + } + if ($matches) { + $candidates[] = [$route[0], $route[1], $args]; + } + } + + // select the best matching one (has the longest path, for now) + $bestScore = 0; + $bestItem = null; + foreach ($candidates as $c) { + $score = count($c[0]) - count(array_filter($c[0], function ($item) { + return str_starts_with($item, ':'); + })) * 0.1; + if ($score > $bestScore) { + $bestItem = $c; + $bestScore = $score; + } + } + return $bestItem; + } + +} diff --git a/Digitigrade/Singleton.php b/Digitigrade/Singleton.php new file mode 100644 index 0000000..23b2e78 --- /dev/null +++ b/Digitigrade/Singleton.php @@ -0,0 +1,18 @@ +conn = new \PDO($conf->getDbConnection(), $conf->getDbUser(), $conf->getDbPassword(), [ - \PDO::ATTR_PERSISTENT => true, - ]); - } - - public function runInitialSchema() { - $this->conn->exec(file_get_contents(self::INIT_SCHEMA_PATH)); - } - - public function getDbVersion(): int { - $result = $this->conn->query('SELECT db_version FROM db_version'); - return $result->fetch()['db_version']; - } - - /** - * Changes the database schema version - * - * CAREFUL! this shouldn't be used unless you absolutely know why - * @param int $newVersion the new version number - * @return void - */ - public function setDbVersion(int $newVersion) { - $stmt = $this->conn->prepare('UPDATE db_version SET db_version = ?'); - $stmt->execute([$newVersion]); - } - - public function getPdo(): \PDO { - return $this->conn; - } -} \ No newline at end of file diff --git a/WpfTest/Db/Migrator.php b/WpfTest/Db/Migrator.php deleted file mode 100644 index ea7f92a..0000000 --- a/WpfTest/Db/Migrator.php +++ /dev/null @@ -1,52 +0,0 @@ -migrations[$newVersion] = $fn; - } - - /** - * Runs all available migrations - * @return void - */ - public function migrate() { - $db = Db::getInstance(); - $currentVersion = null; - try { - $currentVersion = $db->getDbVersion(); - error_log('starting on version ' . $currentVersion); - } catch (\PDOException $e) { - if ($e->getCode() == '42P01') { // undefined table - error_log("initialising database because db version table doesn't exist"); - $db->runInitialSchema(); - $currentVersion = $db->getDbVersion(); - error_log('starting on version ' . $currentVersion); - } else - throw $e; - } - - // find migrations that haven't been run yet - $available = array_filter($this->migrations, function (int $key) use ($currentVersion) { - return $key > $currentVersion; - }, ARRAY_FILTER_USE_KEY); - - try { - // run them in order (at least they should be in order?) - foreach (array_keys($available) as $version) { - $fn = $available[$version]; - $filename = basename((new \ReflectionFunction($fn))->getFileName()); - error_log("running migration $filename"); - $available[$version]($db->getPdo()); - $db->setDbVersion($version); - } - } finally { - error_log('now on version ' . $db->getDbVersion()); - } - } -} \ No newline at end of file diff --git a/WpfTest/GlobalConfig.php b/WpfTest/GlobalConfig.php deleted file mode 100644 index 4601257..0000000 --- a/WpfTest/GlobalConfig.php +++ /dev/null @@ -1,44 +0,0 @@ -confData = parse_ini_file(self::CONFIG_INI_PATH, process_sections: true); - } - - public function getCanonicalHost(): string { - $host = $this->confData['instance']['hostname']; - if (isset($this->confData['instance']['port']) && $this->confData['instance']['port'] != ($this->isHttps() ? 443 : 80)) - $host .= ':' . $this->confData['instance']['port']; - return $host; - } - - public function isHttps(): bool { - return $this->confData['debug']['https'] ?? true; - } - - public function getBasePath(): string { - return ($this->isHttps() ? 'https://' : 'http://') . $this->getCanonicalHost(); - } - - public function shouldReportTraces(): bool { - return $this->confData['debug']['send_tracebacks'] ?? false; - } - - public function getDbConnection(): string { - return 'pgsql:host=' . $this->confData['database']['host'] . - ';port=' . $this->confData['database']['port'] . - ';dbname=' . $this->confData['database']['database']; - } - - public function getDbUser(): string { - return $this->confData['database']['username']; - } - - public function getDbPassword(): string { - return $this->confData['database']['password']; - } -} \ No newline at end of file diff --git a/WpfTest/HttpResponseStatus.php b/WpfTest/HttpResponseStatus.php deleted file mode 100644 index c81e000..0000000 --- a/WpfTest/HttpResponseStatus.php +++ /dev/null @@ -1,7 +0,0 @@ -message = $reason ?? 'Request does not conform to expectations'; - } - - public function httpCode(): int { - return 400; - } - - public function httpReason(): string { - return "Bad Request"; - } -} \ No newline at end of file diff --git a/WpfTest/HttpResponseStatus/InternalServerError.php b/WpfTest/HttpResponseStatus/InternalServerError.php deleted file mode 100644 index 11b67f3..0000000 --- a/WpfTest/HttpResponseStatus/InternalServerError.php +++ /dev/null @@ -1,23 +0,0 @@ -message = $reason::class . ': ' . $reason->getMessage(); - if (isset($context)) { - $this->message .= " [$context]"; - } - if (GlobalConfig::getInstance()->shouldReportTraces()) { - $this->message .= "\n" . $reason->getTraceAsString(); - } - } - public function httpCode(): int { - return 500; - } - public function httpReason(): string { - return "Internal Server Error"; - } -} \ No newline at end of file diff --git a/WpfTest/HttpResponseStatus/NoHandlerFound.php b/WpfTest/HttpResponseStatus/NoHandlerFound.php deleted file mode 100644 index 9ceeb0b..0000000 --- a/WpfTest/HttpResponseStatus/NoHandlerFound.php +++ /dev/null @@ -1,18 +0,0 @@ -message = "No handler found for path $path"; - } - - public function httpCode(): int { - return 404; - } - - public function httpReason(): string { - return "Not Found"; - } -} \ No newline at end of file diff --git a/WpfTest/HttpResponseStatus/NotFound.php b/WpfTest/HttpResponseStatus/NotFound.php deleted file mode 100644 index b36d0a3..0000000 --- a/WpfTest/HttpResponseStatus/NotFound.php +++ /dev/null @@ -1,18 +0,0 @@ -message = $reason ?? 'Request was handled but no appropriate resource found'; - } - - public function httpCode(): int { - return 404; - } - - public function httpReason(): string { - return "Not Found"; - } -} \ No newline at end of file diff --git a/WpfTest/Model.php b/WpfTest/Model.php deleted file mode 100644 index 53b2d83..0000000 --- a/WpfTest/Model.php +++ /dev/null @@ -1,219 +0,0 @@ -id) && static::find($this->{'id'}) != null) { - return 'id = ' . $this->{'id'}; - } - return null; - } - - /** - * Converts a field or type name into the version used in the database - */ - private static function mangleName(string $name): string { - $chars = str_split($name); - $chars[0] = strtolower($chars[0]); - for ($i = 0; $i < count($chars); $i++) { - $c = $chars[$i]; - if (ctype_upper($c)) { - array_splice($chars, $i, 1, ['_', strtolower($c)]); - } - } - return implode($chars); - } - - /** - * Converts a database column name into the corresponding field name - */ - private static function unmangleName(string $name): string { - $chars = str_split($name); - for ($i = 0; $i < count($chars); $i++) { - if ($chars[$i] == '_') { - array_splice($chars, $i, 2, [strtoupper($chars[$i + 1])]); - } - } - return implode($chars); - } - - public function toDbRow(): array { - $refl = new \ReflectionObject($this); - $props = $refl->getProperties(); - $result = []; - foreach ($props as $p) { - $pName = $p->getName(); - if (!isset($this->{$pName})) - continue; - // we need to determine the plain value to insert ... - $value = $this->{$pName}; - $discarded = false; - if ($value instanceof \DateTimeInterface) { - // datetime objects should be turned into iso 8601 timestamps - $value = $value->format('c'); - } elseif ($value instanceof Model) { - // models should generally be turned into their id if there is one or removed if not - if (property_exists($value, 'id') && isset($value->id)) - $value = $value->{'id'}; // doing it like this to shut up the warning (lol) - else - $discarded = true; - } elseif (is_array($value)) { - // arrays ought to be handled separately when saving (different db table) - $discarded = true; - } - if (!$discarded) - $result[self::mangleName($pName)] = $value; - } - return $result; - } - - public static function fromDbRow(array|false $columns): ?static { - if ($columns === false) - return null; - // create the model - $obj = new static(); - $refl = new \ReflectionObject($obj); - // for each field of the db row ... - foreach (array_keys($columns) as $name) { - $value = $columns[$name]; - $name = self::unmangleName($name); // guess the appropriate field in the model - try { - // try to assign it directly (works for simple values) - $obj->{$name} = $value; - } catch (\TypeError $e) { - // if it's not the right type we have to try to wrangle it into the right one - // there's a few strategies for this - $type = $refl->getProperty($name)->getType(); - if ($type == null || !is_a($type, \ReflectionNamedType::class)) - throw $e; - $typeClass = $type->getName(); - if (enum_exists($typeClass)) { - // if it's a backed enum we can find the enum case with the same value - $obj->{$name} = $typeClass::from($value); - } elseif (is_subclass_of($typeClass, Model::class) && is_int($value)) { - // if it's another model we can try to look it up by id automatically - $obj->{$name} = $typeClass::find($value); - } else { - // otherwise try to instantiate the correct class and pass it the simple value - $obj->{$name} = new $typeClass($value); - } - } - } - // set up any referenced fields if needed - $obj->hydrate(); - $obj->saved = true; - return $obj; - } - - /** - * Initialises fields of the model that contain referenced / foreign keyed objects - * which couldn't be initialised automatically - * @return void - */ - protected function hydrate() { - } - - public static function findWhere(string $whereClause, array $parameters): ?static { - $classNameParts = explode('\\', static::class); - $className = $classNameParts[count($classNameParts) - 1]; - $tableName = self::mangleName($className); - - $stmt = Db::getInstance()->getPdo()->prepare("SELECT * FROM $tableName WHERE $whereClause LIMIT 1"); - $stmt->execute($parameters); - return static::fromDbRow($stmt->fetch(\PDO::FETCH_ASSOC)); - } - - public static function findAllWhere(string $whereClause, array $parameters): array { - $classNameParts = explode('\\', static::class); - $className = $classNameParts[count($classNameParts) - 1]; - $tableName = self::mangleName($className); - - $stmt = Db::getInstance()->getPdo()->prepare("SELECT * FROM $tableName WHERE $whereClause"); - $stmt->execute($parameters); - return array_map(function ($row) { - return static::fromDbRow($row); - }, $stmt->fetchAll(\PDO::FETCH_ASSOC)); - } - - public static function find(int $id): ?static { - return static::findWhere('id = ?', [$id]); - } - - /** - * Inserts or updates the model's corresponding database record. - * Also sets the model's id field if it has one. - */ - public function save() { - // figure out all the parameters and such for the query - $paramData = $this->toDbRow(); - $columnNames = array_keys($paramData); - $parameterNames = array_map(function (string $column) { - return ":$column"; - }, $columnNames); - $columns = implode(', ', $columnNames); - $parameters = implode(', ', $parameterNames); - - $classNameParts = explode('\\', static::class); - $className = $classNameParts[count($classNameParts) - 1]; - $tableName = self::mangleName($className); - - $db = Db::getInstance()->getPdo(); - - $whereClause = $this->getUpdateWhereClause($db); - $shouldUpdate = $whereClause != null; - $stmt = $db->prepare( - $shouldUpdate - ? "UPDATE $tableName SET ($columns) = ($parameters) WHERE $whereClause" - : "INSERT INTO $tableName ($columns) VALUES ($parameters)" - ); - - // pdo is really stupid and casts `false`/0 to an empty string, which breaks the query - // so i have to cast `false`s and `0`s manually. hooray - // i also have to convert backed enums to their value because they dont implement Stringable - $paramData = array_map(function ($item) { - if ($item === false || $item === 0) - return '0'; - if (is_a($item, \BackedEnum::class)) - return $item->value; - return $item; - }, $paramData); - // actually save it !!!! yay - $stmt->execute($paramData); - - if (property_exists($this, 'id')) { - $this->id = $shouldUpdate - ? static::findWhere($whereClause, [])->id - : (int) $db->lastInsertId(); - } - - // recursively save inner models - // but not fetchable ones because they should really be saved separately - $reflProps = (new \ReflectionObject($this))->getProperties(); - foreach ($reflProps as $reflProp) { - assert($reflProp instanceof \ReflectionProperty); - $reflType = $reflProp->getType(); - $propName = $reflProp->getName(); - if (!($reflType instanceof \ReflectionNamedType)) - continue; - $type = $reflType->getName(); - if (is_subclass_of($type, Model::class) && !is_subclass_of($type, FetchableModel::class)) { - if (property_exists($this, 'id')) - $this->{$propName}->setOwnerId($this->id); - $this->{$propName}->save(); - } - } - - $this->saved = true; - } -} \ No newline at end of file diff --git a/WpfTest/Model/Actor.php b/WpfTest/Model/Actor.php deleted file mode 100644 index a745a96..0000000 --- a/WpfTest/Model/Actor.php +++ /dev/null @@ -1,90 +0,0 @@ -uri]) != null) - return 'uri = ' . $db->quote($this->uri); - if (self::findWhere('id = ?', [$this->id]) != null) - return "id = $this->id"; - return null; - } - - public static function findLocalByHandle(string $handle): ?self { - return self::findWhere('is_local = true AND handle = ?', [$handle]); - } - - public static function findByWebfinger(string $acct, bool $autoSave = true, bool $forceRefetch = false): ?self { - // normalise uri - if (!str_starts_with($acct, 'acct:')) { - if (str_starts_with($acct, '@')) { - $acct = substr($acct, 1); - } - $acct = "acct:$acct"; - } - // fetch it - $domain = explode('@', $acct, 2)[1]; - $data = @file_get_contents("https://$domain/.well-known/webfinger?resource=$acct", - context: stream_context_create([ - 'http' => ['header' => 'Accept: application/jrd+json, application/json'] - ]) - ); - if ($data === false) - return null; - $data = json_decode($data); - if ($data === null) - return null; - // find the right link - if (!isset($data->links)) - return null; - $matching = array_filter($data->links, function (\stdClass $linkObj) { - return $linkObj->rel == self::REL_URI; - }); - if (count($matching) == 0) - return null; - return self::findByUri($matching[0]->href, $autoSave, $forceRefetch); - } - - public function jsonSerialize(): array { - return [ - 'type' => 'actor', - 'self' => path_to_uri("/user/$this->handle"), - 'created' => $this->created?->format('c'), - 'modified' => $this->modified?->format('c'), - 'homepage' => path_to_uri("/$this->handle"), - 'handle' => $this->handle, - 'displayName' => $this->displayName, - 'bio' => $this->bio, - 'pronouns' => $this->pronouns, - 'automated' => $this->automated, - 'endpoints' => [ - 'basicFeed' => path_to_uri("/user/$this->handle/basicFeed"), - 'fullFeed' => path_to_uri("/user/$this->handle/fullFeed") - ] - ]; - } - - public function hydrate() { - $this->endpoints = ActorEndpoints::findWhere('actor_id = ?', [$this->id]); - } -} \ No newline at end of file diff --git a/WpfTest/Model/ActorEndpoints.php b/WpfTest/Model/ActorEndpoints.php deleted file mode 100644 index f6120d3..0000000 --- a/WpfTest/Model/ActorEndpoints.php +++ /dev/null @@ -1,27 +0,0 @@ -actorId = $id; - } - - protected function getUpdateWhereClause($db): ?string { - if (self::findWhere('actor_id = ?', [$this->actorId]) != null) { - return "actor_id = $this->actorId"; - } - return null; - } -} \ No newline at end of file diff --git a/WpfTest/Model/FetchableModel.php b/WpfTest/Model/FetchableModel.php deleted file mode 100644 index 5dce010..0000000 --- a/WpfTest/Model/FetchableModel.php +++ /dev/null @@ -1,78 +0,0 @@ -bStrictObjectTypeChecking = false; - } - - private static function fetchFromRemote(string $uri, bool $autoSave): ?static { - // fetch the object - $data = @file_get_contents($uri, context: stream_context_create(['http' => [ - 'header' => 'Accept: application/json' - ]])); - if ($data === false) - return null; - $data = json_decode($data); - if ($data === null) - return null; - // default everything nullable to null - $obj = new static(); - $reflProps = (new \ReflectionClass(static::class))->getProperties(); - foreach ($reflProps as $reflProp) { - assert($reflProp instanceof \ReflectionProperty); // for intellisense lmao - if ($reflProp->getType()->allowsNull()) { - $obj->{$reflProp->getName()} = null; - } - } - // rename 'self' to 'uri' because of reasons - if (property_exists($data, 'self')) { - $data->uri = $data->self; - unset($data->self); - } - // map all present fields - self::$mapper->map($data, $obj); - // (hopefully) recursively fetch any other fetchable models contained in this one - foreach ($reflProps as $reflProp) { - assert($reflProp instanceof \ReflectionProperty); - $reflType = $reflProp->getType(); - $propName = $reflProp->getName(); - if (!($reflType instanceof \ReflectionNamedType)) - continue; - $type = $reflType->getName(); - if (is_subclass_of($type, FetchableModel::class) && is_string($data->{$propName})) { - // try to avoid recursion - // TODO: make a better effort at avoiding recursion (e.g. when it's more than one step nested) - if ($data->{$propName} == $uri) { - $obj->{$propName} = $obj; - } else { - $obj->{$propName} = $type::findByUri($data->{$propName}, $autoSave); - } - } - } - - if ($autoSave) { - $obj->save(); - } - - return $obj; - } - - public static function findByUri(string $uri, bool $autoSave = true, bool $forceRefetch = false): ?static { - if (!$forceRefetch) { - $obj = static::findWhere('uri = ?', [$uri]); - if ($obj != null) - return $obj; - } - return static::fetchFromRemote($uri, $autoSave); - } -} - -FetchableModel::__initStatic(); \ No newline at end of file diff --git a/WpfTest/Model/Note.php b/WpfTest/Model/Note.php deleted file mode 100644 index af2d2aa..0000000 --- a/WpfTest/Model/Note.php +++ /dev/null @@ -1,95 +0,0 @@ - - */ - public array $formattedContent = []; - public string $language; - /** - * @var Actor[] - */ - public array $mentions = []; - public ?Note $inReplyTo; - public ?Note $threadApex; - public NotePrivacy $privacy; - /** - * @var NoteAttachment[] - */ - public array $attachments = []; - public array $extensions = []; - - protected function getUpdateWhereClause(\PDO $db): ?string { - if (self::findWhere('uri = ?', [$this->uri]) != null) - return 'uri = ' . $db->quote($this->uri); - if (self::findWhere('id = ?', [$this->id]) != null) - return "id = $this->id"; - return null; - } - - protected function hydrate() { - $this->privacy = NotePrivacy::findWhere('note_id = ?', [$this->id]); - $this->formattedContent = $this->findFormattedContents(); - $this->mentions = $this->findMentions(); - $this->attachments = NoteAttachment::findAllWhere('note_id = ?', [$this->id]); - } - - private function findFormattedContents(): array { - $pdo = Db::getInstance()->getPdo(); - $stmt = $pdo->prepare('SELECT mimetype, body FROM note_formatted_content WHERE note_id = ?'); - $stmt->execute([$this->id]); - return $stmt->fetchAll(\PDO::FETCH_ASSOC); - } - - private function findMentions(): array { - $pdo = Db::getInstance()->getPdo(); - $stmt = $pdo->prepare('SELECT actor_id FROM note_mention WHERE note_id = ?'); - $stmt->execute([$this->id]); - $actors = array_map(function ($actorId) { - return Actor::find($actorId); - }, $stmt->fetchAll(\PDO::FETCH_COLUMN, 0)); - return $actors; - } - - public static function findAllWithAuthor(Actor $author): array { - return self::findAllWhere('author = ?', [$author->id]); - } - - public function jsonSerialize(): array { - return [ - 'type' => 'note', - 'self' => path_to_uri("/post/$this->id"), - 'created' => $this->created->format('c'), - 'modified' => $this->modified?->format('c'), - 'author' => $this->author->uri, - 'summary' => $this->summary, - 'plainContent' => $this->plainContent, - 'formattedContent' => $this->formattedContent, - 'language' => $this->language, - 'mentions' => array_map(function (Actor $actor) { - return $actor->uri; - }, $this->mentions), - 'inReplyTo' => $this->inReplyTo?->uri, - 'threadApex' => $this->threadApex?->uri, - 'privacy' => $this->privacy, - 'attachments' => array_map(function (NoteAttachment $attachment) { - return [ - 'type' => $attachment->type, - 'href' => $attachment->href, - 'description' => $attachment->description - ]; - }, $this->attachments), - //'extensions' => [] - ]; - } -} \ No newline at end of file diff --git a/WpfTest/Model/NoteAttachment.php b/WpfTest/Model/NoteAttachment.php deleted file mode 100644 index 16947c5..0000000 --- a/WpfTest/Model/NoteAttachment.php +++ /dev/null @@ -1,23 +0,0 @@ -noteId = $id; - } - - protected function getUpdateWhereClause($db): ?string { - if (self::findWhere('note_id = ?', [$this->noteId]) != null) { - return "note_id = $this->noteId"; - } - return null; - } -} \ No newline at end of file diff --git a/WpfTest/Model/NotePrivacy.php b/WpfTest/Model/NotePrivacy.php deleted file mode 100644 index b955447..0000000 --- a/WpfTest/Model/NotePrivacy.php +++ /dev/null @@ -1,39 +0,0 @@ -noteId = $id; - } - - protected function getUpdateWhereClause($db): ?string { - if (self::findWhere('note_id = ?', [$this->noteId]) != null) { - return "note_id = $this->noteId"; - } - return null; - } - - public function jsonSerialize(): array { - return [ - 'scope' => $this->scope->value, - 'alsoVisibleTo' => $this->alsoVisibleTo, - 'indexable' => $this->indexable, - 'canReshare' => $this->canReshare->value, - 'canReply' => $this->canReply->value, - 'canOtherInteract' => $this->canOtherInteract->value - ]; - } -} \ No newline at end of file diff --git a/WpfTest/Model/NotePrivacyInteractors.php b/WpfTest/Model/NotePrivacyInteractors.php deleted file mode 100644 index 917d90a..0000000 --- a/WpfTest/Model/NotePrivacyInteractors.php +++ /dev/null @@ -1,9 +0,0 @@ -requestPath = $_GET['requestPath'] ?? $_SERVER['PATH_INFO'] ?? explode('?', $_SERVER['REQUEST_URI'], 2)[0] ?? '/'; - $this->routes = []; - } - - /** - * Processes and sends a response to the current request - * @return void - */ - public function processRequest() { - try { - $this->processRequestInner(); - } catch (HttpResponseStatus $e) { - http_response_code($e->httpCode()); - echo '

' . $e->httpReason() . '

' . $e->getMessage() . '
'; - } - } - - private function processRequestInner() { - $context = null; - try { - $route = $this->matchPath($this->requestPath); - if ($route == null) { - throw new NoHandlerFound($this->requestPath); - } - $context = 'handler for /' . implode('/', $route[0]); - $route[1]($route[2]); // call handler with args - } catch (\Exception $e) { - if (is_a($e, HttpResponseStatus::class)) { - throw $e; - } - throw new InternalServerError($e, $context); - } catch (\Error $e) { - throw new InternalServerError($e, $context); - } - } - - public function mount(string $pathSpec, callable $handler) { - $this->routes[] = [$this->explodePathPieces($pathSpec), $handler]; - } - - private function explodePathPieces(string $pathSpec): array { - $pieces = explode('/', $pathSpec); - if ($pieces[0] == '') - array_shift($pieces); - return $pieces; - } - - private function matchPath(string $requestPath): ?array { - // find matching candidates - $pieces = $this->explodePathPieces($requestPath); - $candidates = []; - foreach ($this->routes as $route) { - $routePieces = $route[0]; - if (count($routePieces) != count($pieces)) - continue; - $args = []; - $matches = true; - for ($i = 0; $i < count($pieces); $i++) { - if (str_starts_with($routePieces[$i], ':')) { // this is a parameter - $args[substr($routePieces[$i], 1)] = $pieces[$i]; - } elseif ($routePieces[$i] != $pieces[$i]) { - $matches = false; - break; - } - } - if ($matches) { - $candidates[] = [$route[0], $route[1], $args]; - } - } - - // select the best matching one (has the longest path, for now) - $bestScore = 0; - $bestItem = null; - foreach ($candidates as $c) { - $score = count($c[0]) - count(array_filter($c[0], function ($item) { - return str_starts_with($item, ':'); - })) * 0.1; - if ($score > $bestScore) { - $bestItem = $c; - $bestScore = $score; - } - } - return $bestItem; - } - -} diff --git a/WpfTest/Singleton.php b/WpfTest/Singleton.php deleted file mode 100644 index b365e1e..0000000 --- a/WpfTest/Singleton.php +++ /dev/null @@ -1,18 +0,0 @@ -migrate(); \ No newline at end of file +\Digitigrade\Db\Migrator::getInstance()->migrate(); \ No newline at end of file diff --git a/composer.json b/composer.json index d349fcd..02dbc38 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ }, "autoload": { "psr-4": { - "WpfTest\\": "WpfTest/" + "Digitigrade\\": "Digitigrade/" } } } diff --git a/config.ini b/config.ini index 8047cdd..16b9f06 100644 --- a/config.ini +++ b/config.ini @@ -7,9 +7,9 @@ hostname = example.org [database] host = localhost port = 5432 -username = wpftest -password = wpftest -database = wpftest +username = digitigrade +password = digitigrade +database = digitigrade [debug] ; whether to send full tracebacks on internal server errors (optional, diff --git a/index.php b/index.php index 43603aa..617dc0e 100644 --- a/index.php +++ b/index.php @@ -11,4 +11,4 @@ function require_all_glob(string $pattern) { require_all_glob('routes/*.php'); require_all_glob('misc/*.php'); -WpfTest\Router::getInstance()->processRequest(); +Digitigrade\Router::getInstance()->processRequest(); diff --git a/migrations/20241207_183000_create_actor.php b/migrations/20241207_183000_create_actor.php index 08de53d..27f0b1b 100644 --- a/migrations/20241207_183000_create_actor.php +++ b/migrations/20241207_183000_create_actor.php @@ -1,6 +1,6 @@ register(20241207_183000, function (PDO $db) { // create actor, endpoints, extensions diff --git a/migrations/20241208_111936_create_note.php b/migrations/20241208_111936_create_note.php index a8471e7..75f02ff 100644 --- a/migrations/20241208_111936_create_note.php +++ b/migrations/20241208_111936_create_note.php @@ -1,6 +1,6 @@ register(20241208_111936, function (PDO $db) { // create notes and their related tables diff --git a/migrations/20241208_165411_create_note_mentions.php b/migrations/20241208_165411_create_note_mentions.php index f589161..8cf494c 100644 --- a/migrations/20241208_165411_create_note_mentions.php +++ b/migrations/20241208_165411_create_note_mentions.php @@ -1,6 +1,6 @@ register(20241208_165411, function (PDO $db) { // forgot this table in create_note :( diff --git a/migrations/20241208_170639_add_note_attachment_type.php b/migrations/20241208_170639_add_note_attachment_type.php index 66d2808..fec0e0f 100644 --- a/migrations/20241208_170639_add_note_attachment_type.php +++ b/migrations/20241208_170639_add_note_attachment_type.php @@ -1,6 +1,6 @@ register(20241208_170639, function (PDO $db) { // forgot this one also diff --git a/misc/path_to_uri.php b/misc/path_to_uri.php index 756640e..1e2c2fb 100644 --- a/misc/path_to_uri.php +++ b/misc/path_to_uri.php @@ -1,6 +1,6 @@ mount('/user/:handle', function (array $args) { $actor = Actor::findLocalByHandle($args['handle']); diff --git a/routes/homepage.php b/routes/homepage.php index ea5d31b..5fbc102 100644 --- a/routes/homepage.php +++ b/routes/homepage.php @@ -1,6 +1,6 @@ mount('/', function (array $args) { echo '

home

'; diff --git a/routes/nodeinfo.php b/routes/nodeinfo.php index e85bca1..c29ac0a 100644 --- a/routes/nodeinfo.php +++ b/routes/nodeinfo.php @@ -1,6 +1,6 @@ mount('/.well-known/nodeinfo', function (array $args) { json_response([ @@ -17,10 +17,10 @@ Router::getInstance()->mount('/nodeinfo/2.1', function (array $args) { json_response([ 'version' => '2.1', 'software' => [ - 'name' => 'WPF test server', + 'name' => 'Digitigrade', 'version' => '0.1.0' ], - 'protocols' => ['wpf'], + 'protocols' => ['pawpub'], 'services' => [ 'inbound' => [], 'outbound' => [] @@ -32,9 +32,9 @@ Router::getInstance()->mount('/nodeinfo/2.1', function (array $args) { ] ], 'metadata' => [ - 'nodeName' => "winter's pawsome test server", - 'nodeDescription' => 'written in php i dunno', - 'wpf' => [ + 'nodeName' => 'A Digitigrade server', + 'nodeDescription' => 'nodeinfo is hardcoded for now', + 'pawpub' => [ 'extensions' => [ // 'https://whatever.example/ext/meow', ], diff --git a/routes/note.php b/routes/note.php index 65b9313..be7eb25 100644 --- a/routes/note.php +++ b/routes/note.php @@ -1,8 +1,8 @@ mount('/post/:id', function (array $args) { $note = Note::find($args['id']); diff --git a/routes/webfinger.php b/routes/webfinger.php index db29e73..cfccb22 100644 --- a/routes/webfinger.php +++ b/routes/webfinger.php @@ -1,10 +1,10 @@ mount('/.well-known/webfinger', function (array $args) { if (!isset($_GET['resource'])) { -- cgit v1.3