aboutsummaryrefslogtreecommitdiffhomepage
path: root/Digitigrade
diff options
context:
space:
mode:
authorwinter2025-02-23 18:32:09 +0000
committerwinter2025-02-23 18:32:09 +0000
commit261ab1364aa40384b0315be60e78cd89ea7fe661 (patch)
treebf114af430c761abfaf33e814e3cf9ba0ed096d2 /Digitigrade
parent1ba56a5b687e6c906c2b24e5b37df119ef5752e3 (diff)
implement policies
Diffstat (limited to 'Digitigrade')
-rw-r--r--Digitigrade/HttpResponseStatus/PolicyRejected.php22
-rw-r--r--Digitigrade/Model/FetchableModel.php3
-rw-r--r--Digitigrade/Model/Note.php2
-rw-r--r--Digitigrade/Model/PushableModel.php3
-rw-r--r--Digitigrade/Policy.php37
-rw-r--r--Digitigrade/Policy/BasicCensor.php47
-rw-r--r--Digitigrade/Policy/BlockInstance.php37
-rw-r--r--Digitigrade/PolicyManager.php46
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