aboutsummaryrefslogtreecommitdiffhomepage
path: root/Digitigrade
diff options
context:
space:
mode:
authorwinter2025-02-16 19:36:51 +0000
committerwinter2025-02-16 19:36:51 +0000
commit5a44137ddea7b3a6d7f68cbe71594f7633d6e9a6 (patch)
tree1583032971f5fbde6e333a4c377c2b6672dc296c /Digitigrade
parent65dfe7aff06dfd3b044a5deb47b03d643c651ec1 (diff)
link previews
Diffstat (limited to 'Digitigrade')
-rw-r--r--Digitigrade/LinkPreview.php91
-rw-r--r--Digitigrade/Model/Note.php45
2 files changed, 135 insertions, 1 deletions
diff --git a/Digitigrade/LinkPreview.php b/Digitigrade/LinkPreview.php
new file mode 100644
index 0000000..4716cc8
--- /dev/null
+++ b/Digitigrade/LinkPreview.php
@@ -0,0 +1,91 @@
+<?php
+namespace Digitigrade;
+
+class LinkPreview implements \JsonSerializable {
+ public string $url;
+ public string $siteName;
+ public string $title;
+ public string $description;
+ public string $style;
+ public ?string $imageUrl = null;
+
+ /**
+ * Generates a link preview for the given URL.
+ * @param string $url
+ * @return LinkPreview|null the link preview, or null if none is available
+ */
+ public static function generate(string $url): ?self {
+ $p = new self();
+ $p->url = $url;
+ return $p->update() ? $p : null;
+ }
+
+ /**
+ * Updates the information for this link preview.
+ * @return bool true if successful
+ */
+ public function update(): bool {
+ if (!(str_starts_with($this->url, "https://") || str_starts_with($this->url, "http://"))) {
+ return false;
+ }
+
+ $d = new \DOMDocument();
+ $c = @file_get_contents($this->url);
+ if (!$c)
+ return false;
+ @$d->loadHTML($c);
+
+ $url = $this->url;
+ $siteName = hostname_from_uri($this->url);
+ $style = 'text';
+ $title = $d->getElementsByTagName('title')->item(0)?->textContent;
+ $metas = $d->getElementsByTagName('meta');
+ foreach ($metas as $tag) {
+ if ($tag->attributes->getNamedItem('name')?->textContent == 'description') {
+ // <meta name="description" content="...">
+ $description = $tag->attributes->getNamedItem('content')?->textContent;
+ } elseif ($prop = $tag->attributes->getNamedItem('property')) {
+ // <meta property="og:..." content="...">
+ $content = $tag->attributes->getNamedItem('content')?->textContent;
+ match ($prop->textContent) {
+ 'og:title' => $title = $content,
+ 'og:description' => $description = $content,
+ 'og:url' => $url = $content,
+ 'og:site_name' => $siteName = $content,
+ 'og:image' => $imageUrl = $content,
+ default => null
+ };
+
+ } elseif ($tag->attributes->getNamedItem('name')?->textContent == 'twitter:card'
+ && $tag->attributes->getNamedItem("content")?->textContent == 'summary_large_image') {
+ // <meta name="twitter:card" content="summary_large_image">
+ $style = 'largeImage';
+ }
+ }
+ // turn text into smallImage if an image is present
+ $style = ($style == 'text' && isset($imageUrl)) ? 'smallImage' : $style;
+
+ if (!isset($title, $description))
+ return false;
+
+ $this->url = $url;
+ $this->siteName = $siteName;
+ $this->title = $title;
+ $this->description = $description;
+ $this->style = $style;
+ $this->imageUrl = $imageUrl ?? null;
+
+ return true;
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'url' => $this->url,
+ 'siteName' => $this->siteName,
+ 'title' => $this->title,
+ 'description' => $this->description,
+ 'style' => $this->style,
+ 'imageUrl' => $this->imageUrl
+ ];
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/Model/Note.php b/Digitigrade/Model/Note.php
index d5d4e69..ac9cc3c 100644
--- a/Digitigrade/Model/Note.php
+++ b/Digitigrade/Model/Note.php
@@ -4,12 +4,16 @@ namespace Digitigrade\Model;
use Digitigrade\Db;
use Digitigrade\FormattingProvider\CommonMark;
use Digitigrade\FormattingProvider\SanitisedHtml;
+use Digitigrade\LinkPreview;
use Digitigrade\Model\HomeTimelineItem;
use Digitigrade\Notification\Notifyable;
use Digitigrade\Timeline\TimelineIncludeable;
use Digitigrade\Timeline\TimelineItem;
+use JsonMapper;
class Note extends PushableModel implements TimelineIncludeable, Notifyable {
+ public const EXTENSION_LINK_PREVIEWS = 'https://pawpub.entities.org.uk/extension/link-previews';
+
public ?int $id;
public string $uri;
public \DateTimeImmutable $created;
@@ -265,6 +269,45 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable {
return htmlspecialchars($this->plainContent);
}
+ /**
+ * Gets a list of LinkPreviews applicable to this note
+ * @return LinkPreview[]
+ */
+ public function getLinkPreviews(): array {
+ if (($this->extensions[self::EXTENSION_LINK_PREVIEWS] ?? []) == []) {
+ return [];
+ }
+ $mapper = new JsonMapper();
+ $results = [];
+ foreach ($this->extensions[self::EXTENSION_LINK_PREVIEWS] as $preview) {
+ $results[] = $mapper->map($preview, LinkPreview::class);
+ }
+ return $results;
+ }
+
+ /**
+ * Generates new LinkPreviews for this note (but does not save them automatically)
+ * @return LinkPreview[] the new link previews
+ */
+ public function generateLinkPreviews(): array {
+ $matches = [];
+ preg_match_all(
+ '`[a-z+]+://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(([-a-zA-Z0-9@:(%_\+.~#?&/=]|\)[^\s])*)`',
+ $this->getFormattedContent('text/markdown') ?? $this->plainContent,
+ $matches
+ );
+ $urls = $matches[0] ?? [];
+ $previews = array_filter(
+ array_map(fn($url) => LinkPreview::generate($url), $urls),
+ fn($p) => $p != null
+ );
+ $this->extensions[self::EXTENSION_LINK_PREVIEWS] = array_map(
+ fn($p) => json_decode(json_encode($p)),
+ $previews
+ );
+ return $previews;
+ }
+
public function renderAsHtml() {
render_template('note', ['note' => $this]);
}
@@ -345,7 +388,7 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable {
'description' => $attachment->description
];
}, $this->attachments),
- //'extensions' => []
+ 'extensions' => $this->extensions
];
}
} \ No newline at end of file