PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.14.1
Independent Analytics – WordPress Analytics Plugin v2.14.1
2.15.5 2.15.4 2.15.3 2.15.2 2.15.1 2.15.0 2.14.10 trunk 1.1 1.10 1.10.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.17.1 1.17.2 1.17.3 1.17.4 1.18 1.18.1 1.19.0 1.19.1 1.2 1.20.0 1.21.0 1.22.0 1.22.1 1.23.0 1.23.1 1.24.0 1.24.1 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.28.1 1.28.2 1.28.3 1.29.0 1.3 1.30.0 1.30.1 1.4 1.5 1.6 1.7 1.8 1.9 2.0.0 2.0.1 2.1.4 2.1.5 2.1.6 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.10 2.11.2 2.11.3 2.11.4 2.11.5 2.11.6 2.11.7 2.11.8 2.11.9 2.12.0 2.12.1 2.12.2 2.13.1 2.13.2 2.13.5 2.13.6 2.14.0 2.14.1 2.14.2 2.14.4 2.14.6 2.14.7 2.14.8 2.14.9 2.2.0 2.2.1 2.3.1 2.3.2 2.4.2 2.4.3 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.7.1 2.7.2 2.7.3 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7
independent-analytics / IAWP / Favicon / FaviconDownloader.php
independent-analytics / IAWP / Favicon Last commit date
Favicon.php 7 months ago FaviconDownloader.php 7 months ago
FaviconDownloader.php
219 lines
1 <?php
2
3 namespace IAWP\Favicon;
4
5 /** @internal */
6 class FaviconDownloader
7 {
8 private \IAWP\Favicon\Favicon $favicon;
9 public function __construct(\IAWP\Favicon\Favicon $favicon)
10 {
11 $this->favicon = $favicon;
12 }
13 public function download() : void
14 {
15 try {
16 $this->attempt_download();
17 } catch (\Throwable $e) {
18 //
19 }
20 }
21 private function attempt_download()
22 {
23 if (!$this->has_required_php_extensions()) {
24 return;
25 }
26 if ($this->favicon->exists()) {
27 return;
28 }
29 $url = \sanitize_url($this->favicon->domain);
30 $content = $this->fetch($url);
31 if ($content === null) {
32 return;
33 }
34 try {
35 $favicon_url = $this->extractFaviconUrl($content, $url);
36 } catch (\Throwable $e) {
37 return;
38 }
39 if ($favicon_url === null) {
40 return;
41 }
42 $save_path = \IAWPSCOPED\iawp_upload_path_to('iawp-favicons/' . $this->favicon->file_name());
43 $this->convertFaviconToPng($favicon_url, $save_path);
44 }
45 private function fetch(string $url) : ?string
46 {
47 if (!\extension_loaded('curl')) {
48 return null;
49 }
50 $handle = \curl_init();
51 \curl_setopt($handle, \CURLOPT_URL, $url);
52 \curl_setopt($handle, \CURLOPT_RETURNTRANSFER, 1);
53 \curl_setopt($handle, \CURLOPT_FOLLOWLOCATION, \true);
54 \curl_setopt($handle, \CURLOPT_MAXREDIRS, 10);
55 \curl_setopt($handle, \CURLOPT_TIMEOUT, 10);
56 \curl_setopt($handle, \CURLOPT_ENCODING, '');
57 // Handle compression automatically
58 // Mimic a real browser to bypass Cloudflare and other bot protections
59 \curl_setopt($handle, \CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
60 $headers = ['Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', 'Accept-Language: en-US,en;q=0.9', 'Upgrade-Insecure-Requests: 1', 'Sec-Fetch-Dest: document', 'Sec-Fetch-Mode: navigate', 'Sec-Fetch-Site: none', 'Sec-Fetch-User: ?1'];
61 \curl_setopt($handle, \CURLOPT_HTTPHEADER, $headers);
62 $content = \curl_exec($handle);
63 $httpCode = \curl_getinfo($handle, \CURLINFO_HTTP_CODE);
64 \curl_close($handle);
65 if ($httpCode >= 400) {
66 return null;
67 }
68 return $content !== \false ? $content : null;
69 }
70 private function convertFaviconToPng(string $faviconUrl, string $savePath) : bool
71 {
72 // ... Imagick extension check ...
73 try {
74 $imagick = new \Imagick();
75 $blob = $this->fetch($faviconUrl);
76 if ($blob === null) {
77 return \false;
78 }
79 // $imagick->setResolution(96, 96);
80 try {
81 $imagick->readImageBlob($blob);
82 } catch (\Throwable $e) {
83 // Retry with ICO format if initial read fails (common for some ICO files)
84 $imagick = new \Imagick();
85 $imagick->setFormat('ico');
86 $imagick->readImageBlob($blob);
87 }
88 // Handle multi-frame files (like ICO) by selecting the best frame
89 if ($imagick->getNumberImages() > 1) {
90 $bestIndex = 0;
91 $maxArea = 0;
92 $numImages = $imagick->getNumberImages();
93 for ($i = 0; $i < $numImages; $i++) {
94 $imagick->setIteratorIndex($i);
95 $area = $imagick->getImageWidth() * $imagick->getImageHeight();
96 if ($area > $maxArea) {
97 $maxArea = $area;
98 $bestIndex = $i;
99 }
100 }
101 $imagick->setIteratorIndex($bestIndex);
102 $image = $imagick->getImage();
103 $imagick->clear();
104 $imagick = $image;
105 }
106 // 3. Set the output format to PNG. The rasterized SVG is saved as PNG.
107 $imagick->setImageFormat('png');
108 $max_size = 48;
109 if ($imagick->getImageWidth() > $max_size) {
110 $imagick->thumbnailImage($max_size, 0);
111 }
112 if ($imagick->getImageHeight() > $max_size) {
113 $imagick->thumbnailImage(0, $max_size);
114 }
115 $thumbnail_size = \min($imagick->getImageWidth(), $imagick->getImageHeight());
116 $imagick->cropThumbnailImage($thumbnail_size, $thumbnail_size);
117 $imagick->writeImage($savePath);
118 return \true;
119 } catch (\Throwable $e) {
120 return \false;
121 }
122 }
123 private function extractFaviconUrl(string $html, string $baseUrl) : ?string
124 {
125 $dom = new \DOMDocument();
126 @$dom->loadHTML($html);
127 // Suppress HTML parsing errors
128 $xpath = new \DOMXPath($dom);
129 // XPath Query: Find link tags in <head> with 'icon' in the 'rel' attribute
130 $query = "//head/link[contains(translate(@rel, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'icon')]";
131 $linkNodes = $xpath->query($query);
132 if ($linkNodes->length > 0) {
133 $bestIcon = null;
134 foreach ($linkNodes as $node) {
135 $rel = \strtolower($node->getAttribute('rel'));
136 $href = $node->getAttribute('href');
137 $sizes = $node->getAttribute('sizes');
138 $score = 0;
139 // Simple rel categorization
140 if (\strpos($rel, 'shortcut') !== \false) {
141 $rel = 'shortcut icon';
142 } elseif (\strpos($rel, 'apple') !== \false) {
143 $rel = 'apple-touch-icon';
144 if (!$sizes) {
145 $score = 180 * 180;
146 }
147 } elseif (\strpos($rel, 'icon') !== \false) {
148 $rel = 'icon';
149 } else {
150 continue;
151 }
152 if ($sizes) {
153 if (\strtolower($sizes) === 'any') {
154 $score = \PHP_INT_MAX;
155 } else {
156 $dimensions = \explode(' ', $sizes);
157 foreach ($dimensions as $dim) {
158 $parts = \explode('x', \strtolower($dim));
159 if (\count($parts) === 2) {
160 $w = (int) $parts[0];
161 $h = (int) $parts[1];
162 $area = $w * $h;
163 if ($area > $score) {
164 $score = $area;
165 }
166 }
167 }
168 }
169 }
170 $candidate = ['href' => $href, 'rel' => $rel, 'score' => $score];
171 if ($bestIcon === null) {
172 $bestIcon = $candidate;
173 continue;
174 }
175 // Prioritize by score (size)
176 if ($candidate['score'] > $bestIcon['score']) {
177 $bestIcon = $candidate;
178 } elseif ($candidate['score'] === $bestIcon['score']) {
179 // Tie-breaker: Priority based on rel type
180 $priorities = ['apple-touch-icon' => 3, 'icon' => 2, 'shortcut icon' => 1];
181 $currentP = $priorities[$candidate['rel']] ?? 0;
182 $bestP = $priorities[$bestIcon['rel']] ?? 0;
183 if ($currentP > $bestP) {
184 $bestIcon = $candidate;
185 }
186 }
187 }
188 if ($bestIcon) {
189 $relativeUrl = $bestIcon['href'];
190 // Construct the absolute URL
191 if (\strpos($relativeUrl, '//') === 0) {
192 return "https:{$relativeUrl}";
193 } elseif (\strpos($relativeUrl, 'http') === 0) {
194 return $relativeUrl;
195 } else {
196 return \rtrim($baseUrl, '/') . '/' . \ltrim($relativeUrl, '/');
197 }
198 }
199 }
200 // 4. Final Fallback: Check root /favicon.ico since DOM parsing failed
201 // We use the fetch method to gain the benefits of browser-mimicking headers
202 $standardIco = \rtrim($baseUrl, '/') . "/favicon.ico";
203 $icoContent = $this->fetch($standardIco);
204 // Ensure we got content and it's not HTML (which would indicate a challenge page or 404 page)
205 if ($icoContent !== null && \strpos($icoContent, '<html') === \false && \strpos($icoContent, '<!DOCTYPE') === \false) {
206 return $standardIco;
207 }
208 return null;
209 }
210 private function has_required_php_extensions() : bool
211 {
212 return \extension_loaded('curl') && \extension_loaded('imagick');
213 }
214 public static function for(\IAWP\Favicon\Favicon $favicon) : self
215 {
216 return new self($favicon);
217 }
218 }
219