aboutsummaryrefslogtreecommitdiffhomepage
path: root/Digitigrade/LinkPreview.php
blob: f23b1c0100b26d951d86920da4d51f013cde8af1 (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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?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;
        }

        // 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
        $ch = curl_init($this->url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Accept: text/html,application/xhtml+xml,application/xml',
        ]);
        curl_setopt($ch, CURLOPT_RANGE, "0-256000"); // request first 256KB of the page
        curl_setopt($ch, CURLOPT_MAXFILESIZE, 8 * 1024 * 1024); // don't download more than 8MiB of HTML
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $response_charset = null;
        curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$response_charset) {
            $len = strlen($header);
            $header = explode(':', $header, 2);
            if (count($header) != 2)
                return $len;

            // content-type: text/html; charset=utf-8
            if (strtolower(trim($header[0])) == 'content-type') {
                $vals = explode(';', $header[1]);
                foreach($vals as $val) {
                    $val = strtolower(trim($val));
                    if(str_starts_with($val,"charset=")) {
                        $response_charset = substr($val, 8);
                    }
                }
            }
            return $len;
        });
        $c = curl_exec($ch);
        if (!$c)
            return false;
        $d = \Dom\HTMLDocument::createFromString($c, LIBXML_NOERROR, $response_charset);

        $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
        ];
    }
}