PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.6.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.6.01
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.6.01, at app/Services/RemoteUrlParser.php

384 lines 11.8 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 $remote_url_response = $this->getRemoteBody($url);
128 if (is_wp_error($remote_url_response) || empty($remote_url_response)) {
129 return $remote_url_response;
130 }
131
132 $html_head = $this->getDocumentHead($remote_url_response);
133
134 $title = $this->getTitle($html_head);
135 if (!$title) {
136 return new \WP_Error('rest_invalid_url', __('Invalid URL', 'fluent-community'), array('status' => 404));
137 }
138
139 $meta_elements = $this->getMetaWithContentElements($html_head);
140
141 $data = array_filter([
142 'title' => $title,
143 'image' => $this->getImage($meta_elements, $url),
144 'description' => $this->getDescription($meta_elements),
145 'icon' => $this->getIcon($html_head, $url),
146 'type' => 'meta_data',
147 'url' => $url
148 ]);
149
150 wp_cache_set($cacheKey, $data, 'fluent-community', apply_filters('rest_url_details_cache_expiration', HOUR_IN_SECONDS)); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
151
152 return $data;
153 }
154
155 private function getRemoteBody($url)
156 {
157 $modified_user_agent = 'WP-URLDetails/' . get_bloginfo('version') . ' (+' . get_bloginfo('url') . ')';
158
159 $args = array(
160 'limit_response_size' => 300 * KB_IN_BYTES,
161 'user-agent' => $modified_user_agent,
162 );
163
164 /**
165 * Filters the HTTP request args for URL data retrieval.
166 *
167 * Can be used to adjust response size limit and other WP_Http::request() args.
168 *
169 * @param array $args Arguments used for the HTTP request.
170 * @param string $url The attempted URL.
171 * @since 5.9.0
172 *
173 */
174 $args = apply_filters('rest_url_details_http_request_args', $args, $url); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
175
176 $response = wp_safe_remote_get($url, $args);
177
178 if (\WP_Http::OK !== wp_remote_retrieve_response_code($response)) {
179 // Not saving the error response to cache since the error might be temporary.
180 return new \WP_Error(
181 'no_response',
182 __('URL not found. Response returned a non-200 status code for this URL.', 'fluent-community'),
183 array('status' => \WP_Http::NOT_FOUND)
184 );
185 }
186
187 $remote_body = wp_remote_retrieve_body($response);
188
189 if (empty($remote_body)) {
190 return new \WP_Error(
191 'no_content',
192 __('Unable to retrieve body from response at this URL.', 'fluent-community'),
193 array('status' => \WP_Http::NOT_FOUND)
194 );
195 }
196
197 return $remote_body;
198 }
199
200 private function getDocumentHead($html)
201 {
202 $head_html = $html;
203
204 // Find the opening `<head>` tag.
205 $head_start = strpos($html, '<head');
206 if (false === $head_start) {
207 // Didn't find it. Return the original HTML.
208 return $html;
209 }
210
211 // Find the closing `</head>` tag.
212 $head_end = strpos($head_html, '</head>');
213 if (false === $head_end) {
214 // Didn't find it. Find the opening `<body>` tag.
215 $head_end = strpos($head_html, '<body');
216
217 // Didn't find it. Return the original HTML.
218 if (false === $head_end) {
219 return $html;
220 }
221 }
222
223 // Extract the HTML from opening tag to the closing tag. Then add the closing tag.
224 $head_html = substr($head_html, $head_start, $head_end);
225 $head_html .= '</head>';
226
227 return $head_html;
228 }
229
230 private function getDescription($meta_elements)
231 {
232 // Bail out if there are no meta elements.
233 if (empty($meta_elements[0])) {
234 return '';
235 }
236
237 $description = $this->getMetadataFromMetaElement(
238 $meta_elements,
239 'name',
240 '(?:description|og:description)'
241 );
242
243 // Bail out if description not found.
244 if ('' === $description) {
245 return '';
246 }
247
248 return $this->prepare_metadata_for_output($description);
249 }
250
251 private function getImage($meta_elements, $url)
252 {
253 $image = $this->getMetadataFromMetaElement(
254 $meta_elements,
255 'property',
256 '(?:og:image|og:image:url)'
257 );
258
259 // Bail out if image not found.
260 if ('' === $image) {
261 return '';
262 }
263
264 // Attempt to convert relative URLs to absolute.
265 $parsed_url = wp_parse_url($url);
266 if (isset($parsed_url['scheme']) && isset($parsed_url['host'])) {
267 $root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
268 $image = \WP_Http::make_absolute_url($image, $root_url);
269 }
270
271 if (!$image) {
272 return $image;
273 }
274
275 return sanitize_url(html_entity_decode($image, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
276 }
277
278 private function getTitle($html)
279 {
280 if (!$html) {
281 return '';
282 }
283
284 $pattern = '#<title[^>]*>(.*?)<\s*/\s*title>#is';
285 preg_match($pattern, $html, $match_title);
286
287 if (empty($match_title[1]) || !is_string($match_title[1])) {
288 return '';
289 }
290
291 $title = trim($match_title[1]);
292
293 return $this->prepare_metadata_for_output($title);
294 }
295
296 private function getMetaWithContentElements($html)
297 {
298 $pattern = '#<meta\s' .
299 '[^>]*' .
300 'content=(["\']??)(.*)\1' .
301 '[^>]*' .
302 '\/?>#' .
303 'isU';
304
305 preg_match_all($pattern, $html, $elements);
306
307 return $elements;
308 }
309
310 private function prepare_metadata_for_output($metadata)
311 {
312 $metadata = html_entity_decode($metadata, ENT_QUOTES, get_bloginfo('charset'));
313 $metadata = wp_strip_all_tags($metadata);
314 return $metadata;
315 }
316
317 private function getMetadataFromMetaElement($meta_elements, $attr, $attr_value)
318 {
319 // Bail out if there are no meta elements.
320 if (empty($meta_elements[0])) {
321 return '';
322 }
323
324 $metadata = '';
325 $pattern = '#' .
326 $attr . '=([\"\']??)\s*' . $attr_value . '\s*\1' .
327 '#isU';
328
329 foreach ($meta_elements[0] as $index => $element) {
330 preg_match($pattern, $element, $match);
331
332 if (empty($match)) {
333 continue;
334 }
335
336 if (isset($meta_elements[2][$index]) && is_string($meta_elements[2][$index])) {
337 $metadata = trim($meta_elements[2][$index]);
338 }
339
340 break;
341 }
342
343 return $metadata;
344 }
345
346 private function getIcon($html, $url)
347 {
348 // Grab the icon's link element.
349 $pattern = '#<link\s[^>]*rel=(?:[\"\']??)\s*(?:icon|shortcut icon|icon shortcut)\s*(?:[\"\']??)[^>]*\/?>#isU';
350 preg_match($pattern, $html, $element);
351 if (empty($element[0]) || !is_string($element[0])) {
352 return '';
353 }
354 $element = trim($element[0]);
355
356 // Get the icon's href value.
357 $pattern = '#href=([\"\']??)([^\" >]*?)\\1[^>]*#isU';
358 preg_match($pattern, $element, $icon);
359 if (empty($icon[2]) || !is_string($icon[2])) {
360 return '';
361 }
362 $icon = trim($icon[2]);
363
364 // If the icon is a data URL, return it.
365 $parsed_icon = wp_parse_url($icon);
366 if (isset($parsed_icon['scheme']) && 'data' === $parsed_icon['scheme']) {
367 return $icon;
368 }
369
370 // Attempt to convert relative URLs to absolute.
371 if (!is_string($url) || '' === $url) {
372 return $icon;
373 }
374
375 $parsed_url = wp_parse_url($url);
376 if (isset($parsed_url['scheme']) && isset($parsed_url['host'])) {
377 $root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
378 $icon = \WP_Http::make_absolute_url($icon, $root_url);
379 }
380
381 return $icon;
382 }
383 }
384