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

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