diff options
Diffstat (limited to 'Digitigrade')
| -rw-r--r-- | Digitigrade/HttpResponseStatus/PolicyRejected.php | 22 | ||||
| -rw-r--r-- | Digitigrade/Model/FetchableModel.php | 3 | ||||
| -rw-r--r-- | Digitigrade/Model/Note.php | 2 | ||||
| -rw-r--r-- | Digitigrade/Model/PushableModel.php | 3 | ||||
| -rw-r--r-- | Digitigrade/Policy.php | 37 | ||||
| -rw-r--r-- | Digitigrade/Policy/BasicCensor.php | 47 | ||||
| -rw-r--r-- | Digitigrade/Policy/BlockInstance.php | 37 | ||||
| -rw-r--r-- | Digitigrade/PolicyManager.php | 46 |
8 files changed, 194 insertions, 3 deletions
diff --git a/Digitigrade/HttpResponseStatus/PolicyRejected.php b/Digitigrade/HttpResponseStatus/PolicyRejected.php new file mode 100644 index 0000000..59a4811 --- /dev/null +++ b/Digitigrade/HttpResponseStatus/PolicyRejected.php @@ -0,0 +1,22 @@ +<?php +namespace Digitigrade\HttpResponseStatus; + +use Digitigrade\HttpResponseStatus; +use Digitigrade\Model\FetchableModel; + +class PolicyRejected extends \Exception implements HttpResponseStatus { + public function __construct() { + $this->message = "Object was rejected by one or more of the instance's policies"; + } + + public function httpCode(): int { + return 403; + } + + public function httpReason(): string { + return "Forbidden"; + } + + public function httpHeaders() { + } +}
\ No newline at end of file diff --git a/Digitigrade/Model/FetchableModel.php b/Digitigrade/Model/FetchableModel.php index 6d21f40..3efa763 100644 --- a/Digitigrade/Model/FetchableModel.php +++ b/Digitigrade/Model/FetchableModel.php @@ -2,6 +2,7 @@ namespace Digitigrade\Model; use Digitigrade\Model; +use Digitigrade\PolicyManager; use Digitigrade\RemoteFetchable; abstract class FetchableModel extends Model implements RemoteFetchable { @@ -75,7 +76,7 @@ abstract class FetchableModel extends Model implements RemoteFetchable { $obj = static::createFromJson($data, $autoSave, $uri); $obj->finaliseAfterFetch($uri); - if (!$obj->validate()) { + if (!$obj->validate() || !PolicyManager::getInstance()->check($obj)) { return null; // and don't save } if ($autoSave) { diff --git a/Digitigrade/Model/Note.php b/Digitigrade/Model/Note.php index f3c0901..880e3e8 100644 --- a/Digitigrade/Model/Note.php +++ b/Digitigrade/Model/Note.php @@ -20,7 +20,7 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable { public Actor $author; public ?string $summary; public string $plainContent; - // associative array mapping strings to strings - mimetypes to content + /** @var array associative array mapping strings to strings - mimetypes to content */ public array $formattedContent = []; public string $language; /** diff --git a/Digitigrade/Model/PushableModel.php b/Digitigrade/Model/PushableModel.php index 271b313..e773779 100644 --- a/Digitigrade/Model/PushableModel.php +++ b/Digitigrade/Model/PushableModel.php @@ -2,6 +2,7 @@ namespace Digitigrade\Model; use Digitigrade\Job\PushObject; +use Digitigrade\PolicyManager; abstract class PushableModel extends FetchableModel implements \JsonSerializable { public ?int $id; @@ -20,7 +21,7 @@ abstract class PushableModel extends FetchableModel implements \JsonSerializable } $obj = static::createFromJson($data, $autoSave); - if (!$obj->validate()) { + if (!$obj->validate() || !PolicyManager::getInstance()->check($obj)) { return null; // and don't save } if ($autoSave) { diff --git a/Digitigrade/Policy.php b/Digitigrade/Policy.php new file mode 100644 index 0000000..56fbdb0 --- /dev/null +++ b/Digitigrade/Policy.php @@ -0,0 +1,37 @@ +<?php +namespace Digitigrade; + +use Digitigrade\Model\Actor; +use Digitigrade\Model\Interaction; +use Digitigrade\Model\Note; + +abstract class Policy { + /** + * Checks if an Actor is allowed and possibly alters it. + * @param Actor $actor the actor to check (may be modified in-place) + * @return bool true if allowed, false if the actor should be dropped + */ + public function checkActor(Actor $actor): bool { + return true; + } + + /** + * Checks if an Interaction is allowed and possibly alters it. + * @param Interaction $interaction the interaction to check (may be modified in-place) + * @return bool true if allowed, false if the interaction should be dropped + */ + public function checkInteraction(Interaction $interaction): bool { + return true; + } + + /** + * Checks if a Note is allowed and possibly alters it. + * @param Note $note the note to check (may be modified in-place) + * @return bool true if allowed, false if the note should be dropped + */ + public function checkNote(Note $note): bool { + return true; + } + + // TODO: public function checkExtension() +}
\ No newline at end of file diff --git a/Digitigrade/Policy/BasicCensor.php b/Digitigrade/Policy/BasicCensor.php new file mode 100644 index 0000000..5866488 --- /dev/null +++ b/Digitigrade/Policy/BasicCensor.php @@ -0,0 +1,47 @@ +<?php +namespace Digitigrade\Policy; + +use Digitigrade\Model\Actor; +use Digitigrade\Model\Note; +use Digitigrade\Policy; + +/** + * Simple word censorship policy that replaces all instances of some text with another string. + * + * Suffers from the Scunthorpe problem. + * + * Can also be (ab)used to replace some word with another word for humorous effect :3 + */ +class BasicCensor extends Policy { + /** @var string[] */ + private array $words; + private string $replacement; + + public function __construct(array $words, string $replacement) { + $this->words = $words; + $this->replacement = $replacement; + } + + public function checkNote(Note $note): bool { + foreach ($this->words as $w) { + if (isset($note->summary)) + $note->summary = str_replace($w, $this->replacement, $note->summary); + $note->plainContent = str_replace($w, $this->replacement, $note->plainContent); + foreach ($note->formattedContent as $type => $content) { + $note->formattedContent[$type] = str_replace($w, $this->replacement, $content); + } + } + return true; + } + + public function checkActor(Actor $actor): bool { + foreach ($this->words as $w) { + $actor->displayName = str_replace($w, $this->replacement, $actor->displayName); + if (isset($actor->pronouns)) + $actor->pronouns = str_replace($w, $this->replacement, $actor->pronouns); + if (isset($actor->bio)) + $actor->bio = str_replace($w, $this->replacement, $actor->bio); + } + return true; + } +}
\ No newline at end of file diff --git a/Digitigrade/Policy/BlockInstance.php b/Digitigrade/Policy/BlockInstance.php new file mode 100644 index 0000000..5f902f2 --- /dev/null +++ b/Digitigrade/Policy/BlockInstance.php @@ -0,0 +1,37 @@ +<?php +namespace Digitigrade\Policy; + +use Digitigrade\Model\Actor; +use Digitigrade\Model\Interaction; +use Digitigrade\Model\Note; +use Digitigrade\Model\PushableModel; +use Digitigrade\Policy; + +class BlockInstance extends Policy { + /** @var string[] */ + private array $blockedHosts; + + public function __construct(string ...$hosts) { + $this->blockedHosts = $hosts; + } + + private function isBlocked(PushableModel $obj): bool { + $objHost = hostname_from_uri($obj->uri); + foreach ($this->blockedHosts as $host) { + if (str_ends_with($objHost, $host)) { + return true; + } + } + return false; + } + + public function checkActor(Actor $actor): bool { + return !$this->isBlocked($actor); + } + public function checkInteraction(Interaction $interaction): bool { + return !$this->isBlocked($interaction); + } + public function checkNote(Note $note): bool { + return !$this->isBlocked($note); + } +}
\ No newline at end of file diff --git a/Digitigrade/PolicyManager.php b/Digitigrade/PolicyManager.php new file mode 100644 index 0000000..24c2c7f --- /dev/null +++ b/Digitigrade/PolicyManager.php @@ -0,0 +1,46 @@ +<?php +namespace Digitigrade; + +use Digitigrade\Model\Actor; +use Digitigrade\Model\FetchableModel; +use Digitigrade\Model\Interaction; +use Digitigrade\Model\Note; + +class PolicyManager extends Singleton { + /** @var Policy[] */ + private array $policies = []; + + public function register(Policy $policy) { + $this->policies[] = $policy; + } + + public function registerAll(Policy ...$policies) { + foreach ($policies as $p) { + $this->register($p); + } + } + + /** + * Checks if an object is allowed by all policies and possibly alters it. + * @param FetchableModel $object the object to check (may be modified in-place) + * @return bool true if allowed, false if it should be dropped + */ + public function check(FetchableModel $object): bool { + return match ($object::class) { + Actor::class => self::arrayAll($this->policies, fn(Policy $p) => $p->checkActor($object)), + Interaction::class => self::arrayAll($this->policies, fn(Policy $p) => $p->checkInteraction($object)), + Note::class => self::arrayAll($this->policies, fn(Policy $p) => $p->checkNote($object)), + default => true + }; + } + + private static function arrayAll(array $arr, callable $test): bool { + // i would use array_all but that's php 8.4+ only which i don't have lol + foreach ($arr as $item) { + if (!$test($item)) { + return false; + } + } + return true; + } +}
\ No newline at end of file |
