PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.0
2.11.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 All 78 releases
← All changes | app/Services/RemoteUrlParser.php +99 -3 2.5.02.10.0 View file →
@@ -26,8 +26,78 @@
26 26
27 27 return self::$instance->getInfoFromRemoteUrl($url);
28 28 }
29 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 + if (preg_match('#^https?://(?:[\w-]+\.)?youtube\.com/embed/([^?/]+)#i', $src, $ytMatch)) {
46 + return self::bestYoutubeThumbnail($ytMatch[1]);
47 + }
48 +
49 + $providers = [
50 + '#^https?://player\.vimeo\.com/video/([^?/]+).*$#i' => 'https://vumbnail.com/$1.jpg',
51 + '#^https?://fast\.wistia\.net/embed/iframe/([^?/]+).*$#i' => 'https://fast.wistia.net/embed/medias/$1/swatch',
52 + '#^https?://(?:www\.)?dailymotion\.com/(?:embed/video|player\.html\?video=)/?([^?/&]+).*$#i' => 'https://www.dailymotion.com/thumbnail/video/$1',
53 + ];
54 +
55 + foreach ($providers as $pattern => $template) {
56 + $thumb = preg_replace($pattern, $template, $src, 1, $count);
57 + if ($count) {
58 + return $thumb;
59 + }
60 + }
61 +
62 + $parsed = self::parse($src);
63 + return (!is_wp_error($parsed) && !empty($parsed['image'])) ? $parsed['image'] : '';
64 + }
65 +
66 + /**
67 + * Pick the best YouTube thumbnail for a feed preview.
68 + *
69 + * Runs on the feed-save path, so it stays cheap: a single HEAD probe for
70 + * the HD WebP frame (~30-56% smaller than JPG, sharp 16:9), falling back
71 + * to hqdefault.jpg — the one universally present rung (maxres/sd and even
72 + * hqdefault.webp 404 for non-HD or legacy uploads). The result is stored
73 + * on the feed and cached, so the probe is paid once per video.
74 + */
75 + protected static function bestYoutubeThumbnail($videoId)
76 + {
77 + $videoId = sanitize_text_field($videoId);
78 + if (!$videoId) {
79 + return '';
80 + }
81 +
82 + $fallback = 'https://img.youtube.com/vi/' . $videoId . '/hqdefault.jpg';
83 +
84 + $cacheKey = 'fcom_yt_thumb_' . md5($videoId);
85 + $cached = get_transient($cacheKey);
86 + if ($cached !== false) {
87 + return $cached;
88 + }
89 +
90 + $maxRes = 'https://i.ytimg.com/vi_webp/' . $videoId . '/maxresdefault.webp';
91 + $response = wp_remote_head($maxRes, ['timeout' => 1.5, 'redirection' => 0]);
92 + if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
93 + set_transient($cacheKey, $maxRes, WEEK_IN_SECONDS);
94 + return $maxRes;
95 + }
96 +
97 + return $fallback;
98 + }
99 +
30 100 public function getOembed($url)
31 101 {
32 102 $data = (new \WP_oEmbed())->get_data($url, [
33 103 'discover' => false
@@ -38,22 +108,38 @@
38 108 }
39 109
40 110 $data = (array)$data;
41 111
112 + $provider = strtolower(Arr::get($data, 'provider_name'));
113 +
114 + $image = Arr::get($data, 'thumbnail_url');
115 + if ($provider === 'youtube') {
116 + $image = self::bestYoutubeThumbnail(self::getYoutubeVideoId($url)) ?: $image;
117 + }
118 +
42 119 return array_filter([
43 120 'title' => Arr::get($data, 'title'),
44 121 'author_name' => Arr::get($data, 'author_name'),
45 122 'type' => 'oembed',
46 - 'provider' => strtolower(Arr::get($data, 'provider_name')),
123 + 'provider' => $provider,
47 124 'content_type' => Arr::get($data, 'type'),
48 125 'url' => $url,
49 126 'html' => self::sanitizeOembedHtml(Arr::get($data, 'html')),
50 - 'image' => Arr::get($data, 'thumbnail_url'),
127 + 'image' => $image,
51 128 ]);
52 129 }
53 130
54 - private static function sanitizeOembedHtml($html)
131 + protected static function getYoutubeVideoId($url)
55 132 {
133 + if (preg_match('#(?:youtu\.be/|youtube\.com/(?:embed/|v/|live/|shorts/|watch\?v=))([a-zA-Z0-9_-]+)#i', (string)$url, $match)) {
134 + return $match[1];
135 + }
136 +
137 + return '';
138 + }
139 +
140 + public static function sanitizeOembedHtml($html)
141 + {
56 142 if (empty($html)) {
57 143 return $html;
58 144 }
59 145
@@ -72,8 +158,12 @@
72 158 'sandbox' => true,
73 159 ];
74 160 }
75 161
162 + // Strip <script>/<style> incl. contents; wp_kses keeps their inner text otherwise.
163 + $html = preg_replace('#<(script|style)\b[^>]*>.*?</\1>#is', '', $html);
164 + $html = preg_replace('#<(script|style)\b[^>]*/?>#i', '', $html);
165 +
76 166 return wp_kses($html, $allowed, ['https']);
77 167 }
78 168
79 169 public function getInfoFromRemoteUrl($url)
@@ -88,8 +178,14 @@
88 178 $cachedReponse = wp_cache_get($cacheKey, 'fluent-community');
89 179
90 180 if ($cachedReponse) {
91 181 return $cachedReponse;
182 + }
183 +
184 + $preempted = apply_filters('fluent_community/preview_metadata_pre_fetch', null, $url);
185 + if ($preempted && is_array($preempted)) {
186 + wp_cache_set($cacheKey, $preempted, 'fluent-community', apply_filters('rest_url_details_cache_expiration', HOUR_IN_SECONDS)); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
187 + return $preempted;
92 188 }
93 189
94 190 $remote_url_response = $this->getRemoteBody($url);
95 191 if (is_wp_error($remote_url_response) || empty($remote_url_response)) {