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 +60 -3 2.7.02.10.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;
@@ -104,8 +157,12 @@
104 157 'referrerpolicy' => true,
105 158 'sandbox' => true,
106 159 ];
107 160 }
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);
108 165
109 166 return wp_kses($html, $allowed, ['https']);
110 167 }
111 168