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