PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.0
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
fluent-community / app / Services / RemoteUrlParser.php

RemoteUrlParser.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.7.0, at app/Services/RemoteUrlParser.php

390 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Services;
4
5 use FluentCommunity\Framework\Support\Arr;
6
7 class RemoteUrlParser
8 {
9
10 private static $instance;
11
12 /*
13 * Create a method to call the getInfoFromRemoteUrl static method as magic method
14 */
15 public static function parse($url)
16 {
17 if (!self::$instance) {
18 self::$instance = new self();
19 }
20
21 $oEmbed = self::$instance->getOembed($url);
22
23 if ($oEmbed) {
24 return $oEmbed;
25 }
26
27 return self::$instance->getInfoFromRemoteUrl($url);
28 }
29
30 public static function extractIframeThumbnail(&$html)
31 {
32 if (!is_string($html)) {
33 return '';
34 }
35 $html = self::sanitizeOembedHtml($html);
36 if (!preg_match('/<iframe\s[^>]*\bsrc\s*=\s*([\'"])(.*?)\1/i', $html, $matches)) {
37 return '';
38 }
39
40 $src = sanitize_url(html_entity_decode($matches[2], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
41 if (!$src) {
42 return '';
43 }
44
45 $providers = [
46 '#^https?://(?:[\w-]+\.)?youtube\.com/embed/([^?/]+).*$#i' => 'https://img.youtube.com/vi/$1/hqdefault.jpg',
47 '#^https?://player\.vimeo\.com/video/([^?/]+).*$#i' => 'https://vumbnail.com/$1.jpg',
48 '#^https?://fast\.wistia\.net/embed/iframe/([^?/]+).*$#i' => 'https://fast.wistia.net/embed/medias/$1/swatch',
49 '#^https?://(?:www\.)?dailymotion\.com/(?:embed/video|player\.html\?video=)/?([^?/&]+).*$#i' => 'https://www.dailymotion.com/thumbnail/video/$1',
50 ];
51
52 foreach ($providers as $pattern => $template) {
53 $thumb = preg_replace($pattern, $template, $src, 1, $count);
54 if ($count) {
55 return $thumb;
56 }
57 }
58
59 $parsed = self::parse($src);
60 return (!is_wp_error($parsed) && !empty($parsed['image'])) ? $parsed['image'] : '';
61 }
62
63 public function getOembed($url)
64 {
65 $data = (new \WP_oEmbed())->get_data($url, [
66 'discover' => false
67 ]);
68
69 if (empty($data) || is_wp_error($data) || empty($data->provider_name)) {
70 return null;
71 }
72
73 $data = (array)$data;
74
75 return array_filter([
76 'title' => Arr::get($data, 'title'),
77 'author_name' => Arr::get($data, 'author_name'),
78 'type' => 'oembed',
79 'provider' => strtolower(Arr::get($data, 'provider_name')),
80 'content_type' => Arr::get($data, 'type'),
81 'url' => $url,
82 'html' => self::sanitizeOembedHtml(Arr::get($data, 'html')),
83 'image' => Arr::get($data, 'thumbnail_url'),
84 ]);
85 }
86
87 public static function sanitizeOembedHtml($html)
88 {
89 if (empty($html)) {
90 return $html;
91 }
92
93 static $allowed = null;
94 if ($allowed === null) {
95 $allowed = wp_kses_allowed_html('post');
96 $allowed['iframe'] = [
97 'src' => true,
98 'width' => true,
99 'height' => true,
100 'frameborder' => true,
101 'allowfullscreen' => true,
102 'title' => true,
103 'loading' => true,
104 'referrerpolicy' => true,
105 'sandbox' => true,
106 ];
107 }
108
109 return wp_kses($html, $allowed, ['https']);
110 }
111
112 public function getInfoFromRemoteUrl($url)
113 {
114 $url = untrailingslashit($url);
115
116 if (empty($url)) {
117 return new \WP_Error('rest_invalid_url', __('Invalid URL', 'fluent-community'), array('status' => 404));
118 }
119
120 $cacheKey = 'fcom_url_details_meta_' . md5($url);
121 $cachedReponse = wp_cache_get($cacheKey, 'fluent-community');
122
123 if ($cachedReponse) {
124 return $cachedReponse;
125 }
126
127 $preempted = apply_filters('fluent_community/preview_metadata_pre_fetch', null, $url);
128 if ($preempted && is_array($preempted)) {
129 wp_cache_set($cacheKey, $preempted, 'fluent-community', apply_filters('rest_url_details_cache_expiration', HOUR_IN_SECONDS)); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
130 return $preempted;
131 }
132
133 $remote_url_response = $this->getRemoteBody($url);
134 if (is_wp_error($remote_url_response) || empty($remote_url_response)) {
135 return $remote_url_response;
136 }
137
138 $html_head = $this->getDocumentHead($remote_url_response);
139
140 $title = $this->getTitle($html_head);
141 if (!$title) {
142 return new \WP_Error('rest_invalid_url', __('Invalid URL', 'fluent-community'), array('status' => 404));
143 }
144
145 $meta_elements = $this->getMetaWithContentElements($html_head);
146
147 $data = array_filter([
148 'title' => $title,
149 'image' => $this->getImage($meta_elements, $url),
150 'description' => $this->getDescription($meta_elements),
151 'icon' => $this->getIcon($html_head, $url),
152 'type' => 'meta_data',
153 'url' => $url
154 ]);
155
156 wp_cache_set($cacheKey, $data, 'fluent-community', apply_filters('rest_url_details_cache_expiration', HOUR_IN_SECONDS)); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
157
158 return $data;
159 }
160
161 private function getRemoteBody($url)
162 {
163 $modified_user_agent = 'WP-URLDetails/' . get_bloginfo('version') . ' (+' . get_bloginfo('url') . ')';
164
165 $args = array(
166 'limit_response_size' => 300 * KB_IN_BYTES,
167 'user-agent' => $modified_user_agent,
168 );
169
170 /**
171 * Filters the HTTP request args for URL data retrieval.
172 *
173 * Can be used to adjust response size limit and other WP_Http::request() args.
174 *
175 * @param array $args Arguments used for the HTTP request.
176 * @param string $url The attempted URL.
177 * @since 5.9.0
178 *
179 */
180 $args = apply_filters('rest_url_details_http_request_args', $args, $url); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
181
182 $response = wp_safe_remote_get($url, $args);
183
184 if (\WP_Http::OK !== wp_remote_retrieve_response_code($response)) {
185 // Not saving the error response to cache since the error might be temporary.
186 return new \WP_Error(
187 'no_response',
188 __('URL not found. Response returned a non-200 status code for this URL.', 'fluent-community'),
189 array('status' => \WP_Http::NOT_FOUND)
190 );
191 }
192
193 $remote_body = wp_remote_retrieve_body($response);
194
195 if (empty($remote_body)) {
196 return new \WP_Error(
197 'no_content',
198 __('Unable to retrieve body from response at this URL.', 'fluent-community'),
199 array('status' => \WP_Http::NOT_FOUND)
200 );
201 }
202
203 return $remote_body;
204 }
205
206 private function getDocumentHead($html)
207 {
208 $head_html = $html;
209
210 // Find the opening `<head>` tag.
211 $head_start = strpos($html, '<head');
212 if (false === $head_start) {
213 // Didn't find it. Return the original HTML.
214 return $html;
215 }
216
217 // Find the closing `</head>` tag.
218 $head_end = strpos($head_html, '</head>');
219 if (false === $head_end) {
220 // Didn't find it. Find the opening `<body>` tag.
221 $head_end = strpos($head_html, '<body');
222
223 // Didn't find it. Return the original HTML.
224 if (false === $head_end) {
225 return $html;
226 }
227 }
228
229 // Extract the HTML from opening tag to the closing tag. Then add the closing tag.
230 $head_html = substr($head_html, $head_start, $head_end);
231 $head_html .= '</head>';
232
233 return $head_html;
234 }
235
236 private function getDescription($meta_elements)
237 {
238 // Bail out if there are no meta elements.
239 if (empty($meta_elements[0])) {
240 return '';
241 }
242
243 $description = $this->getMetadataFromMetaElement(
244 $meta_elements,
245 'name',
246 '(?:description|og:description)'
247 );
248
249 // Bail out if description not found.
250 if ('' === $description) {
251 return '';
252 }
253
254 return $this->prepare_metadata_for_output($description);
255 }
256
257 private function getImage($meta_elements, $url)
258 {
259 $image = $this->getMetadataFromMetaElement(
260 $meta_elements,
261 'property',
262 '(?:og:image|og:image:url)'
263 );
264
265 // Bail out if image not found.
266 if ('' === $image) {
267 return '';
268 }
269
270 // Attempt to convert relative URLs to absolute.
271 $parsed_url = wp_parse_url($url);
272 if (isset($parsed_url['scheme']) && isset($parsed_url['host'])) {
273 $root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
274 $image = \WP_Http::make_absolute_url($image, $root_url);
275 }
276
277 if (!$image) {
278 return $image;
279 }
280
281 return sanitize_url(html_entity_decode($image, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
282 }
283
284 private function getTitle($html)
285 {
286 if (!$html) {
287 return '';
288 }
289
290 $pattern = '#<title[^>]*>(.*?)<\s*/\s*title>#is';
291 preg_match($pattern, $html, $match_title);
292
293 if (empty($match_title[1]) || !is_string($match_title[1])) {
294 return '';
295 }
296
297 $title = trim($match_title[1]);
298
299 return $this->prepare_metadata_for_output($title);
300 }
301
302 private function getMetaWithContentElements($html)
303 {
304 $pattern = '#<meta\s' .
305 '[^>]*' .
306 'content=(["\']??)(.*)\1' .
307 '[^>]*' .
308 '\/?>#' .
309 'isU';
310
311 preg_match_all($pattern, $html, $elements);
312
313 return $elements;
314 }
315
316 private function prepare_metadata_for_output($metadata)
317 {
318 $metadata = html_entity_decode($metadata, ENT_QUOTES, get_bloginfo('charset'));
319 $metadata = wp_strip_all_tags($metadata);
320 return $metadata;
321 }
322
323 private function getMetadataFromMetaElement($meta_elements, $attr, $attr_value)
324 {
325 // Bail out if there are no meta elements.
326 if (empty($meta_elements[0])) {
327 return '';
328 }
329
330 $metadata = '';
331 $pattern = '#' .
332 $attr . '=([\"\']??)\s*' . $attr_value . '\s*\1' .
333 '#isU';
334
335 foreach ($meta_elements[0] as $index => $element) {
336 preg_match($pattern, $element, $match);
337
338 if (empty($match)) {
339 continue;
340 }
341
342 if (isset($meta_elements[2][$index]) && is_string($meta_elements[2][$index])) {
343 $metadata = trim($meta_elements[2][$index]);
344 }
345
346 break;
347 }
348
349 return $metadata;
350 }
351
352 private function getIcon($html, $url)
353 {
354 // Grab the icon's link element.
355 $pattern = '#<link\s[^>]*rel=(?:[\"\']??)\s*(?:icon|shortcut icon|icon shortcut)\s*(?:[\"\']??)[^>]*\/?>#isU';
356 preg_match($pattern, $html, $element);
357 if (empty($element[0]) || !is_string($element[0])) {
358 return '';
359 }
360 $element = trim($element[0]);
361
362 // Get the icon's href value.
363 $pattern = '#href=([\"\']??)([^\" >]*?)\\1[^>]*#isU';
364 preg_match($pattern, $element, $icon);
365 if (empty($icon[2]) || !is_string($icon[2])) {
366 return '';
367 }
368 $icon = trim($icon[2]);
369
370 // If the icon is a data URL, return it.
371 $parsed_icon = wp_parse_url($icon);
372 if (isset($parsed_icon['scheme']) && 'data' === $parsed_icon['scheme']) {
373 return $icon;
374 }
375
376 // Attempt to convert relative URLs to absolute.
377 if (!is_string($url) || '' === $url) {
378 return $icon;
379 }
380
381 $parsed_url = wp_parse_url($url);
382 if (isset($parsed_url['scheme']) && isset($parsed_url['host'])) {
383 $root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
384 $icon = \WP_Http::make_absolute_url($icon, $root_url);
385 }
386
387 return $icon;
388 }
389 }
390