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
109
|
<?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
$url = $url ?? $this->url;
$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
];
}
}
|