PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.14.2
Independent Analytics – WordPress Analytics Plugin v2.14.2
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
Converter 6 months ago Favicon.php 6 months ago FaviconDownloader.php 6 months ago
FaviconDownloader.php
176 lines
1 <?php
2
3 namespace IAWP\Favicon;
4
5 use IAWP\Favicon\Converter\AbstractConverter;
6 /** @internal */
7 class FaviconDownloader
8 {
9 private \IAWP\Favicon\Favicon $favicon;
10 public function __construct(\IAWP\Favicon\Favicon $favicon)
11 {
12 $this->favicon = $favicon;
13 }
14 public function download() : void
15 {
16 try {
17 $this->attempt_download();
18 } catch (\Throwable $e) {
19 //
20 }
21 }
22 private function attempt_download()
23 {
24 if (!$this->has_required_php_extensions()) {
25 return;
26 }
27 if ($this->favicon->exists()) {
28 return;
29 }
30 $url = \sanitize_url($this->favicon->domain);
31 $content = $this->fetch($url);
32 if ($content === null) {
33 return;
34 }
35 try {
36 $favicon_url = $this->extractFaviconUrl($content, $url);
37 } catch (\Throwable $e) {
38 return;
39 }
40 if ($favicon_url === null) {
41 return;
42 }
43 $path = \IAWPSCOPED\iawp_upload_path_to('iawp-favicons/' . $this->favicon->file_name());
44 $blob = $this->fetch($favicon_url);
45 if (!$blob) {
46 return;
47 }
48 $converter = AbstractConverter::make($blob, $path);
49 if (!$converter) {
50 return;
51 }
52 $converter->save();
53 }
54 private function fetch(string $url) : ?string
55 {
56 try {
57 $handle = \curl_init();
58 \curl_setopt($handle, \CURLOPT_URL, $url);
59 \curl_setopt($handle, \CURLOPT_RETURNTRANSFER, 1);
60 \curl_setopt($handle, \CURLOPT_FOLLOWLOCATION, \true);
61 \curl_setopt($handle, \CURLOPT_MAXREDIRS, 10);
62 \curl_setopt($handle, \CURLOPT_TIMEOUT, 10);
63 \curl_setopt($handle, \CURLOPT_ENCODING, '');
64 // Handle compression automatically
65 // Mimic a real browser to bypass Cloudflare and other bot protections
66 \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');
67 $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'];
68 \curl_setopt($handle, \CURLOPT_HTTPHEADER, $headers);
69 $content = \curl_exec($handle);
70 $httpCode = \curl_getinfo($handle, \CURLINFO_HTTP_CODE);
71 \curl_close($handle);
72 if ($httpCode >= 400) {
73 return null;
74 }
75 return $content !== \false ? $content : null;
76 } catch (\Throwable $e) {
77 return null;
78 }
79 }
80 private function extractFaviconUrl(string $html, string $baseUrl) : ?string
81 {
82 $dom = new \DOMDocument();
83 @$dom->loadHTML($html);
84 // Suppress HTML parsing errors
85 $xpath = new \DOMXPath($dom);
86 // XPath Query: Find link tags in <head> with 'icon' in the 'rel' attribute
87 $query = "//head/link[contains(translate(@rel, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'icon')]";
88 $linkNodes = $xpath->query($query);
89 if ($linkNodes->length > 0) {
90 $bestIcon = null;
91 foreach ($linkNodes as $node) {
92 $rel = \strtolower($node->getAttribute('rel'));
93 $href = $node->getAttribute('href');
94 $sizes = $node->getAttribute('sizes');
95 $score = 0;
96 // Simple rel categorization
97 if (\strpos($rel, 'shortcut') !== \false) {
98 $rel = 'shortcut icon';
99 } elseif (\strpos($rel, 'apple') !== \false) {
100 $rel = 'apple-touch-icon';
101 if (!$sizes) {
102 $score = 180 * 180;
103 }
104 } elseif (\strpos($rel, 'icon') !== \false) {
105 $rel = 'icon';
106 } else {
107 continue;
108 }
109 if ($sizes) {
110 if (\strtolower($sizes) === 'any') {
111 $score = \PHP_INT_MAX;
112 } else {
113 $dimensions = \explode(' ', $sizes);
114 foreach ($dimensions as $dim) {
115 $parts = \explode('x', \strtolower($dim));
116 if (\count($parts) === 2) {
117 $w = (int) $parts[0];
118 $h = (int) $parts[1];
119 $area = $w * $h;
120 if ($area > $score) {
121 $score = $area;
122 }
123 }
124 }
125 }
126 }
127 $candidate = ['href' => $href, 'rel' => $rel, 'score' => $score];
128 if ($bestIcon === null) {
129 $bestIcon = $candidate;
130 continue;
131 }
132 // Prioritize by score (size)
133 if ($candidate['score'] > $bestIcon['score']) {
134 $bestIcon = $candidate;
135 } elseif ($candidate['score'] === $bestIcon['score']) {
136 // Tie-breaker: Priority based on rel type
137 $priorities = ['apple-touch-icon' => 3, 'icon' => 2, 'shortcut icon' => 1];
138 $currentP = $priorities[$candidate['rel']] ?? 0;
139 $bestP = $priorities[$bestIcon['rel']] ?? 0;
140 if ($currentP > $bestP) {
141 $bestIcon = $candidate;
142 }
143 }
144 }
145 if ($bestIcon) {
146 $relativeUrl = $bestIcon['href'];
147 // Construct the absolute URL
148 if (\strpos($relativeUrl, '//') === 0) {
149 return "https:{$relativeUrl}";
150 } elseif (\strpos($relativeUrl, 'http') === 0) {
151 return $relativeUrl;
152 } else {
153 return \rtrim($baseUrl, '/') . '/' . \ltrim($relativeUrl, '/');
154 }
155 }
156 }
157 // 4. Final Fallback: Check root /favicon.ico since DOM parsing failed
158 // We use the fetch method to gain the benefits of browser-mimicking headers
159 $standardIco = \rtrim($baseUrl, '/') . "/favicon.ico";
160 $icoContent = $this->fetch($standardIco);
161 // Ensure we got content and it's not HTML (which would indicate a challenge page or 404 page)
162 if ($icoContent !== null && \strpos($icoContent, '<html') === \false && \strpos($icoContent, '<!DOCTYPE') === \false) {
163 return $standardIco;
164 }
165 return null;
166 }
167 private function has_required_php_extensions() : bool
168 {
169 return \extension_loaded('curl') && (\extension_loaded('gd') || \extension_loaded('imagick'));
170 }
171 public static function for(\IAWP\Favicon\Favicon $favicon) : self
172 {
173 return new self($favicon);
174 }
175 }
176