aboutsummaryrefslogtreecommitdiffhomepage
path: root/Digitigrade/LinkPreview.php
blob: 718a196b42c636516fc5e28a2f6c2ba4e6d43000 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?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;
        }

        // TODO: Respect Content-Type header from response, maybe set an Accept header in the request?
        // I was able to find a URL serving SHIFT-JIS, but they do not specify a charset in the Content-Type header.
        //    https://www2d.biglobe.ne.jp/~msyk/charcode/cp932/Windows-31J-charset.html
        $max_length = 256 * 1024; // 256KiB should be enough for the document head
        $c = @file_get_contents($this->url, false, null, 0, $max_length);
        if (!$c)
            return false;
        $d = \Dom\HTMLDocument::createFromString($c, LIBXML_NOERROR);

        $url = $this->url;
        $siteName = hostname_from_uri($this->url);
        $style = 'text';

        // opengraph
        $title       = $d->querySelector("meta[property='og:title']")?->getAttribute('content');
        $description = $d->querySelector("meta[property='og:description']")?->getAttribute('content');
        $url         = $d->querySelector("meta[property='og:url']")?->getAttribute('content');
        $siteName    = $d->querySelector("meta[property='og:site_name']")?->getAttribute('content');
        $imageUrl    = $d->querySelector("meta[property='og:image']")?->getAttribute('content');
        // fallbacks
        $title = $title ?? $d->querySelector('title')?->textContent;
        $description = $description ?? $d->querySelector('meta[name=description]')?->getAttribute('content');

        if ($d->querySelector("meta[name='twitter:card'][content=summary_large_image]") != null) {
            $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
        ];
    }
}