PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.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 +66 -3 2.6.02.11.0 View file →
@@ -41,10 +41,13 @@
41 41 if (!$src) {
42 42 return '';
43 43 }
44 44
45 + if (preg_match('#^https?://(?:[\w-]+\.)?youtube\.com/embed/([^?/]+)#i', $src, $ytMatch)) {
46 + return self::bestYoutubeThumbnail($ytMatch[1]);
47 + }
48 +
45 49 $providers = [
46 - '#^https?://(?:[\w-]+\.)?youtube\.com/embed/([^?/]+).*$#i' => 'https://img.youtube.com/vi/$1/hqdefault.jpg',
47 50 '#^https?://player\.vimeo\.com/video/([^?/]+).*$#i' => 'https://vumbnail.com/$1.jpg',
48 51 '#^https?://fast\.wistia\.net/embed/iframe/([^?/]+).*$#i' => 'https://fast.wistia.net/embed/medias/$1/swatch',
49 52 '#^https?://(?:www\.)?dailymotion\.com/(?:embed/video|player\.html\?video=)/?([^?/&]+).*$#i' => 'https://www.dailymotion.com/thumbnail/video/$1',
50 53 ];
@@ -59,8 +62,42 @@
59 62 $parsed = self::parse($src);
60 63 return (!is_wp_error($parsed) && !empty($parsed['image'])) ? $parsed['image'] : '';
61 64 }
62 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 +
63 100 public function getOembed($url)
64 101 {
65 102 $data = (new \WP_oEmbed())->get_data($url, [
66 103 'discover' => false
@@ -71,20 +108,36 @@
71 108 }
72 109
73 110 $data = (array)$data;
74 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 +
75 119 return array_filter([
76 120 'title' => Arr::get($data, 'title'),
77 121 'author_name' => Arr::get($data, 'author_name'),
78 122 'type' => 'oembed',
79 - 'provider' => strtolower(Arr::get($data, 'provider_name')),
123 + 'provider' => $provider,
80 124 'content_type' => Arr::get($data, 'type'),
81 125 'url' => $url,
82 126 'html' => self::sanitizeOembedHtml(Arr::get($data, 'html')),
83 - 'image' => Arr::get($data, 'thumbnail_url'),
127 + 'image' => $image,
84 128 ]);
85 129 }
86 130
131 + protected static function getYoutubeVideoId($url)
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 +
87 140 public static function sanitizeOembedHtml($html)
88 141 {
89 142 if (empty($html)) {
90 143 return $html;
@@ -105,8 +158,12 @@
105 158 'sandbox' => true,
106 159 ];
107 160 }
108 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 +
109 166 return wp_kses($html, $allowed, ['https']);
110 167 }
111 168
112 169 public function getInfoFromRemoteUrl($url)
@@ -121,8 +178,14 @@
121 178 $cachedReponse = wp_cache_get($cacheKey, 'fluent-community');
122 179
123 180 if ($cachedReponse) {
124 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;
125 188 }
126 189
127 190 $remote_url_response = $this->getRemoteBody($url);
128 191 if (is_wp_error($remote_url_response) || empty($remote_url_response)) {