PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-videopress / src / class-initializer.php
jetpack / jetpack_vendor / automattic / jetpack-videopress / src Last commit date
js 2 months ago tus 1 month ago videopress-divi 2 months ago videopress-divi-5 2 months ago class-access-control.php 1 week ago class-admin-ui.php 2 weeks ago class-ajax.php 1 month ago class-attachment-handler.php 1 month ago class-block-editor-content.php 1 month ago class-block-editor-extensions.php 2 weeks ago class-block-replacement.php 9 months ago class-caption-tracks.php 1 month ago class-data.php 1 month ago class-divi.php 2 months ago class-initial-state.php 2 weeks ago class-initializer.php 1 week ago class-jwt-token-bridge.php 2 years ago class-module-control.php 3 years ago class-options.php 2 years ago class-package-version.php 1 week ago class-plan.php 2 years ago class-rest-controller.php 1 month ago class-site.php 3 years ago class-stats.php 1 year ago class-status.php 9 months ago class-upload-exception.php 3 years ago class-uploader-rest-endpoints.php 2 weeks ago class-uploader.php 1 month ago class-utils.php 4 months ago class-video-block-email-renderer.php 2 months ago class-videopress-rest-api-v1-features.php 6 months ago class-videopress-rest-api-v1-settings.php 1 month ago class-videopress-rest-api-v1-site.php 2 weeks ago class-videopress-rest-api-v1-stats.php 3 years ago class-videopresstoken.php 1 month ago class-wpcom-rest-api-v2-attachment-field-videopress.php 9 months ago class-wpcom-rest-api-v2-attachment-videopress-data.php 1 month ago class-wpcom-rest-api-v2-endpoint-videopress-caption-tracks.php 1 month ago class-wpcom-rest-api-v2-endpoint-videopress.php 2 weeks ago class-xmlrpc.php 1 month ago utility-functions.php 1 month ago
class-initializer.php
990 lines
1 <?php
2 /**
3 * The initializer class for the videopress package
4 *
5 * @package automattic/jetpack-videopress
6 */
7
8 namespace Automattic\Jetpack\VideoPress;
9
10 /**
11 * Initialized the VideoPress package
12 */
13 class Initializer {
14
15 const JETPACK_VIDEOPRESS_IFRAME_API_HANDLER = 'jetpack-videopress-iframe-api';
16
17 /**
18 * Initialization optinos
19 *
20 * @var array
21 */
22 protected static $init_options = array();
23
24 /**
25 * Initializes the VideoPress package
26 *
27 * This method is called by Config::ensure.
28 *
29 * @return void
30 */
31 public static function init() {
32 if ( ! did_action( 'videopress_init' ) ) {
33
34 self::unconditional_initialization();
35
36 if ( Status::is_active() ) {
37 self::active_initialization();
38 } elseif ( self::should_initialize_admin_ui() ) {
39 // Keep "Jetpack > VideoPress" in the menu when the module is not
40 // active, linking to the My Jetpack interstitial to activate it.
41 Admin_UI::init_inactive_menu();
42 }
43 }
44
45 /**
46 * Fires after the VideoPress package is initialized
47 *
48 * @since 0.1.1
49 */
50 do_action( 'videopress_init' );
51 }
52
53 /**
54 * Update the initialization options
55 *
56 * This method is called by the Config class
57 *
58 * @param array $options The initialization options.
59 * @return void
60 */
61 public static function update_init_options( array $options ) {
62 if ( empty( $options['admin_ui'] ) || self::should_initialize_admin_ui() ) { // do not overwrite if already set to true.
63 return;
64 }
65
66 self::$init_options['admin_ui'] = $options['admin_ui'];
67 }
68
69 /**
70 * Checks the initialization options and returns whether the admin_ui should be initialized or not
71 *
72 * @return boolean
73 */
74 public static function should_initialize_admin_ui() {
75 return isset( self::$init_options['admin_ui'] ) && true === self::$init_options['admin_ui'];
76 }
77
78 /**
79 * Initialize VideoPress features that should be initialized whenever VideoPress is present, even if the module is not active
80 *
81 * @return void
82 */
83 private static function unconditional_initialization() {
84 if ( self::should_include_utilities() ) {
85 require_once __DIR__ . '/utility-functions.php';
86 }
87
88 // Set up package version hook.
89 add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
90
91 /*
92 * Keep the videopress_guid attachment meta out of reach of the
93 * user-facing meta write APIs (Custom Fields, XML-RPC set_custom_fields,
94 * the WordPress.com JSON API metadata op, REST). The post <-> guid
95 * mapping is what the VideoPress meta and poster endpoints authorize
96 * against, so a writable guid would let a caller point an object they
97 * can edit at somebody else's video and still pass the edit_post check.
98 *
99 * These auth_* filters are consulted by map_meta_cap() for the
100 * add/edit/delete_post_meta capabilities only, so unlike marking the key
101 * protected they leave is_protected_meta() false and meta_key queries
102 * against the WordPress.com JSON API keep working. VideoPress writes the
103 * mapping itself with update_post_meta(), which does not consult
104 * capabilities, so uploads and transcoding are unaffected.
105 *
106 * The subtype-specific filter covers attachments; the generic one covers
107 * calls made before a subtype can be resolved.
108 */
109 add_filter( 'auth_post_meta_videopress_guid_for_attachment', '__return_false' );
110 add_filter( 'auth_post_meta_videopress_guid', '__return_false' );
111
112 Module_Control::init();
113
114 /*
115 * The WPCOM REST API v2 endpoints only register routes/fields on REST
116 * init, so defer constructing them (and autoloading their classes) until
117 * a REST request is actually served. Registered on both REST init hooks
118 * so the routes remain available in every context they were before, and
119 * guarded so the endpoints are instantiated only once per request.
120 */
121 $register_rest_api_v2_endpoints = static function () {
122 static $registered = false;
123 if ( $registered ) {
124 return;
125 }
126 $registered = true;
127 new WPCOM_REST_API_V2_Endpoint_VideoPress();
128 new WPCOM_REST_API_V2_Endpoint_VideoPress_Caption_Tracks();
129 new WPCOM_REST_API_V2_Attachment_VideoPress_Field();
130 new WPCOM_REST_API_V2_Attachment_VideoPress_Data();
131 };
132 add_action( 'rest_api_init', $register_rest_api_v2_endpoints, 0 );
133 add_action( 'restapi_theme_init', $register_rest_api_v2_endpoints, 0 );
134
135 if ( is_admin() ) {
136 AJAX::init();
137 } else {
138 require_once __DIR__ . '/class-block-replacement.php';
139 Block_Replacement::init();
140 }
141 }
142
143 /**
144 * This avoids conflicts when running VideoPress plugin with older versions of the Jetpack plugin
145 *
146 * On version 11.3-a.7 utility functions include were removed from the plugin and it is safe to include it from the package
147 *
148 * @return boolean
149 */
150 private static function should_include_utilities() {
151 if ( ! class_exists( 'Jetpack' ) || ! defined( 'JETPACK__VERSION' ) ) {
152 return true;
153 }
154
155 return version_compare( JETPACK__VERSION, '11.3-a.7', '>=' );
156 }
157
158 /**
159 * Prepare a poster URL for a quoted CSS url() inside an HTML attribute.
160 *
161 * @param mixed $poster Poster URL.
162 * @return string Sanitized and HTML-encoded poster URL, or an empty string.
163 */
164 private static function prepare_poster_url_for_inline_style( $poster ) {
165 if ( ! is_string( $poster ) || '' === $poster ) {
166 return '';
167 }
168
169 /*
170 * Decode one layer so ordinarily encoded URLs retain their semantics. Any
171 * remaining entities are encoded again below and stay inert after the HTML
172 * parser performs its single decoding pass.
173 */
174 $poster_url = esc_url_raw( html_entity_decode( $poster, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
175 if ( '' === $poster_url ) {
176 return '';
177 }
178
179 /*
180 * Force existing character references to be encoded. esc_attr() preserves
181 * them, but this value crosses from an HTML attribute into a CSS string.
182 */
183 return htmlspecialchars( $poster_url, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true );
184 }
185
186 /**
187 * Initialize VideoPress features that should be initialized only when the module is active
188 *
189 * @return void
190 */
191 private static function active_initialization() {
192 Attachment_Handler::init();
193 Jwt_Token_Bridge::init();
194 Caption_Tracks::init();
195 Initial_State::init();
196 XMLRPC::init();
197 Block_Editor_Content::init();
198
199 /*
200 * These endpoints only add their routes on REST init, so defer calling
201 * init() (and autoloading the endpoint classes) until a REST request is
202 * served. Priority 0 ensures the routes still register before the
203 * default-priority rest_api_init handlers run. Class-name strings are
204 * used so the classes are not autoloaded on non-REST requests.
205 */
206 foreach (
207 array(
208 Uploader_Rest_Endpoints::class,
209 Rest_Controller::class,
210 VideoPress_Rest_Api_V1_Stats::class,
211 VideoPress_Rest_Api_V1_Site::class,
212 VideoPress_Rest_Api_V1_Settings::class,
213 VideoPress_Rest_Api_V1_Features::class,
214 ) as $rest_endpoint
215 ) {
216 add_action( 'rest_api_init', array( $rest_endpoint, 'init' ), 0 );
217 }
218 self::register_oembed_providers();
219
220 // Enqueuethe VideoPress Iframe API script in the front-end.
221 add_filter( 'embed_oembed_html', array( __CLASS__, 'enqueue_videopress_iframe_api_script' ), 10, 4 );
222
223 if ( self::should_initialize_admin_ui() ) {
224 Admin_UI::init();
225 }
226
227 Divi::init();
228 }
229
230 /**
231 * Explicitly register VideoPress oembed provider for patterns not supported by core
232 *
233 * @return void
234 */
235 public static function register_oembed_providers() {
236 $host = rawurlencode( home_url() );
237 // videopress.com/v is already registered in core.
238 // By explicitly declaring the provider here, we can speed things up by not relying on oEmbed discovery.
239 wp_oembed_add_provider( '#^https?://video.wordpress.com/v/.*#', 'https://public-api.wordpress.com/oembed/?for=' . $host, true );
240 // This is needed as it's not supported in oEmbed discovery.
241 wp_oembed_add_provider( '|^https?://v\.wordpress\.com/([a-zA-Z\d]{8})(.+)?$|i', 'https://public-api.wordpress.com/oembed/?for=' . $host, true ); // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
242
243 add_filter( 'embed_oembed_html', array( __CLASS__, 'video_enqueue_bridge_when_oembed_present' ), 10, 4 );
244 }
245
246 /**
247 * Enqueues VideoPress token bridge when a VideoPress oembed is present on the current page.
248 *
249 * @param string|false $cache The cached HTML result, stored in post meta.
250 * @param string $url The attempted embed URL.
251 * @param array $attr An array of shortcode attributes.
252 * @param int $post_ID Post ID.
253 *
254 * @return string|false
255 */
256 public static function video_enqueue_bridge_when_oembed_present( $cache, $url, $attr, $post_ID = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
257 if ( Utils::is_videopress_url( $url ) ) {
258 Jwt_Token_Bridge::enqueue_jwt_token_bridge();
259 }
260
261 return $cache;
262 }
263
264 /**
265 * Register all VideoPress blocks
266 *
267 * @return void
268 */
269 public static function register_videopress_blocks() {
270 // Register VideoPress Video block.
271 self::register_videopress_video_block();
272
273 // Register Video Playlist block.
274 self::register_videopress_playlist_block();
275 }
276
277 /**
278 * VideoPress video block render method
279 *
280 * @global \WP_Embed $wp_embed WordPress embed handler.
281 *
282 * @param array $block_attributes Block attributes.
283 * @param string $content Current block markup.
284 * @param \WP_Block $block Current block.
285 *
286 * @return string Block markup.
287 */
288 public static function render_videopress_video_block( $block_attributes, $content, $block ) {
289 global $wp_embed;
290
291 // Pre-build and cache the GUID list for this post to optimize authorization checks.
292 // This is called once per page render and caches GUIDs from all VideoPress blocks
293 // (including those in synced patterns), allowing fast O(1) lookups during token requests.
294 $post_id = $block->context['postId'] ?? get_the_ID();
295 if ( ! empty( $post_id ) ) {
296 Access_Control::build_and_cache_post_guids( absint( $post_id ) );
297 }
298
299 // CSS classes.
300 $align = $block_attributes['align'] ?? null;
301 $align_class = $align ? ' align' . $align : '';
302 $custom_class = isset( $block_attributes['className'] ) ? ' ' . $block_attributes['className'] : '';
303 $classes = 'wp-block-jetpack-videopress jetpack-videopress-player' . $custom_class . $align_class;
304
305 // Inline style.
306 $style = '';
307 $max_width = isset( $block_attributes['maxWidth'] ) && is_string( $block_attributes['maxWidth'] )
308 ? trim( $block_attributes['maxWidth'] )
309 : '';
310
311 // maxWidth is rendered into an inline style. Accept only a plain CSS length
312 // or percentage so the value stays a single, well-formed declaration;
313 // anything else is dropped and the block renders at full width (as with the
314 // "100%" default).
315 if ( '' !== $max_width && '100%' !== $max_width
316 && preg_match( '/^\d+(\.\d+)?(px|%|em|rem|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc|q)$/i', $max_width )
317 ) {
318 $style = sprintf( 'max-width: %s;', $max_width );
319 $classes .= ' wp-block-jetpack-videopress--has-max-width';
320 }
321
322 /*
323 * <figcaption /> element
324 * Caption is stored into the block attributes,
325 * but also it was stored into the <figcaption /> element,
326 * meaning that it could be stored in two different places.
327 */
328 $figcaption = '';
329
330 // Caption from block attributes.
331 $caption = $block_attributes['caption'] ?? null;
332
333 /*
334 * If the caption is not stored into the block attributes,
335 * try to get it from the <figcaption /> element.
336 */
337 if ( null === $caption ) {
338 preg_match( '/<figcaption>(.*?)<\/figcaption>/', $content, $matches );
339 $caption = $matches[1] ?? null;
340 }
341
342 // If we have a caption, create the <figcaption /> element.
343 if ( null !== $caption ) {
344 $figcaption = sprintf( '<figcaption>%s</figcaption>', wp_kses_post( $caption ) );
345 }
346
347 // Custom anchor from block content.
348 $id_attribute = '';
349
350 // Try to get the custom anchor from the block attributes.
351 if ( isset( $block_attributes['anchor'] ) && $block_attributes['anchor'] ) {
352 $id_attribute = sprintf( 'id="%s"', esc_attr( $block_attributes['anchor'] ) );
353 } elseif ( preg_match( '/<figure[^>]*id="([^"]+)"/', $content, $matches ) ) {
354 // Otherwise, try to get the custom anchor from the <figure /> element.
355 $id_attribute = sprintf( 'id="%s"', esc_attr( $matches[1] ) );
356 }
357
358 // Preview On Hover data.
359 $is_poh_enabled =
360 isset( $block_attributes['posterData']['previewOnHover'] ) &&
361 $block_attributes['posterData']['previewOnHover'];
362
363 $autoplay = $block_attributes['autoplay'] ?? false;
364 $controls = $block_attributes['controls'] ?? false;
365 $poster = $block_attributes['posterData']['url'] ?? null;
366
367 $preview_on_hover = '';
368
369 if ( $is_poh_enabled ) {
370 $preview_on_hover = array(
371 'previewAtTime' => $block_attributes['posterData']['previewAtTime'],
372 'previewLoopDuration' => $block_attributes['posterData']['previewLoopDuration'],
373 'autoplay' => $autoplay,
374 'showControls' => $controls,
375 );
376
377 // Create inline style in case video has a custom poster.
378 $inline_style = '';
379 $poster_url = self::prepare_poster_url_for_inline_style( $poster );
380 if ( $poster_url ) {
381 // Emit the poster URL as a double-quoted CSS string so it stays
382 // contained within url() and cannot affect the surrounding style.
383 $inline_style = sprintf(
384 'style="background-image: url(&quot;%s&quot;); background-size: cover; background-position: center center;"',
385 $poster_url
386 );
387 }
388
389 // Expose the preview on hover data to the client.
390 $preview_on_hover = sprintf(
391 '<div class="jetpack-videopress-player__overlay" %s></div><script type="application/json">%s</script>',
392 $inline_style,
393 wp_json_encode( $preview_on_hover, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP )
394 );
395
396 // Set `autoplay` and `muted` attributes to the video element.
397 $block_attributes['autoplay'] = true;
398 $block_attributes['muted'] = true;
399 }
400
401 $figure_template = '
402 <figure class="%1$s" style="%2$s" %3$s>
403 %4$s
404 %5$s
405 %6$s
406 </figure>
407 ';
408
409 // VideoPress URL.
410 $guid = $block_attributes['guid'] ?? null;
411 $videopress_url = Utils::get_video_press_url( $guid, $block_attributes );
412
413 $video_wrapper = '';
414 $video_wrapper_classes = 'jetpack-videopress-player__wrapper';
415
416 if ( $videopress_url ) {
417 $videopress_url = wp_kses_post( $videopress_url );
418
419 /*
420 * Provide a fallback iframe for when the oEmbed endpoint fails, e.g.
421 * when the VideoPress backend isn't ready for a freshly uploaded video.
422 * This prevents the published page from showing a bare link.
423 */
424 $fallback = function ( $output, $url ) use ( $videopress_url ) {
425 $decoded_url = html_entity_decode( $videopress_url, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 );
426 if ( $decoded_url !== $url ) {
427 return $output;
428 }
429
430 return sprintf(
431 '<iframe title="%1$s" aria-label="%1$s" src="%2$s" width="640" height="360" allowfullscreen data-resize-to-parent="true" allow="clipboard-write; presentation"></iframe>',
432 esc_attr__( 'VideoPress Video Player', 'jetpack-videopress-pkg' ),
433 esc_url( preg_replace( '#/v/#', '/embed/', $url, 1 ) )
434 );
435 };
436
437 add_filter( 'embed_maybe_make_link', $fallback, 10, 2 );
438 $oembed_html = apply_filters( 'video_embed_html', $wp_embed->shortcode( array(), $videopress_url ) );
439 remove_filter( 'embed_maybe_make_link', $fallback );
440
441 $video_wrapper = sprintf(
442 '<div class="%s">%s %s</div>',
443 $video_wrapper_classes,
444 $preview_on_hover,
445 $oembed_html
446 );
447
448 /*
449 * Self-heal failed oEmbed cache for VideoPress URLs.
450 *
451 * When the VideoPress backend isn't ready for a freshly uploaded video,
452 * WordPress caches '{{unknown}}' in post meta with a TTL that is too long
453 * for this use case. Clear recent failures so the next page render retries
454 * oEmbed discovery, keeping the fallback iframe above temporary.
455 */
456 $post_id = $block->context['postId'] ?? get_the_ID();
457
458 if ( $post_id ) {
459 $key_suffix = md5( $videopress_url . serialize( wp_embed_defaults( $videopress_url ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Matching WP_Embed cache key format.
460 $oembed_value = get_post_meta( $post_id, '_oembed_' . $key_suffix, true );
461 $oembed_time = (int) get_post_meta( $post_id, '_oembed_time_' . $key_suffix, true );
462
463 /*
464 * Only clear the '{{unknown}}' cache entry when it is recent, to avoid
465 * disabling WordPress's oEmbed backoff for persistent provider failures.
466 */
467 if (
468 '{{unknown}}' === $oembed_value
469 && ( ! $oembed_time || ( time() - $oembed_time ) < MINUTE_IN_SECONDS )
470 ) {
471 delete_post_meta( $post_id, '_oembed_' . $key_suffix );
472 delete_post_meta( $post_id, '_oembed_time_' . $key_suffix );
473 }
474 }
475 }
476
477 // Get premium content from block context.
478 $premium_block_plan_id = isset( $block->context['premium-content/planId'] ) ? intval( $block->context['premium-content/planId'] ) : 0;
479 $is_premium_content_child = isset( $block->context['isPremiumContentChild'] ) ? (bool) $block->context['isPremiumContentChild'] : false;
480 $maybe_premium_script = '';
481 if ( $is_premium_content_child ) {
482 Access_Control::instance()->set_guid_subscription( $guid, $premium_block_plan_id );
483 $escaped_guid = wp_json_encode( $guid, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP );
484 $script_content = "if ( ! window.__guidsToPlanIds ) { window.__guidsToPlanIds = {}; }; window.__guidsToPlanIds[$escaped_guid] = $premium_block_plan_id;";
485 $maybe_premium_script = '<script>' . $script_content . '</script>';
486 }
487
488 // $id_attribute, $video_wrapper, $figcaption properly escaped earlier in the code.
489 return sprintf(
490 $figure_template,
491 esc_attr( $classes ),
492 esc_attr( $style ),
493 $id_attribute,
494 $video_wrapper,
495 $figcaption,
496 $maybe_premium_script
497 );
498 }
499
500 /**
501 * Register the VideoPress block editor block,
502 * AKA "VideoPress Block v6".
503 *
504 * @return void
505 */
506 public static function register_videopress_video_block() {
507 /*
508 * If only Jetpack is active, and if the VideoPress module is not active,
509 * we can register the block just to display a placeholder to turn on the module.
510 * That invitation is only useful for admins though.
511 */
512 if (
513 Status::is_jetpack_plugin_without_videopress_module_active()
514 && ! Status::is_standalone_plugin_active()
515 && ! current_user_can( 'jetpack_activate_modules' )
516 ) {
517 return;
518 }
519
520 $videopress_video_metadata_file = __DIR__ . '/../build/block-editor/blocks/video/block.json';
521 $videopress_video_metadata_file_exists = file_exists( $videopress_video_metadata_file );
522 if ( ! $videopress_video_metadata_file_exists ) {
523 return;
524 }
525
526 $videopress_video_metadata = json_decode(
527 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
528 file_get_contents( $videopress_video_metadata_file )
529 );
530
531 // Pick the block name straight from the block metadata .json file.
532 $videopress_video_block_name = $videopress_video_metadata->name;
533
534 // Is the block already registered?
535 $is_block_registered = \WP_Block_Type_Registry::get_instance()->is_registered( $videopress_video_block_name );
536
537 // Do not register if the block is already registered.
538 if ( $is_block_registered ) {
539 return;
540 }
541
542 $registration = register_block_type(
543 $videopress_video_metadata_file,
544 array(
545 'render_callback' => array( __CLASS__, 'render_videopress_video_block' ),
546 'render_email_callback' => array( Video_Block_Email_Renderer::class, 'render' ),
547 'uses_context' => array( 'premium-content/planId', 'isPremiumContentChild', 'selectedPlanId' ),
548 )
549 );
550
551 // Do not enqueue scripts if the block could not be registered.
552 if ( empty( $registration ) || empty( $registration->editor_script_handles ) ) {
553 return;
554 }
555
556 // Extensions use Connection_Initial_State::render_script with script handle as parameter.
557 if ( is_array( $registration->editor_script_handles ) ) {
558 $script_handle = $registration->editor_script_handles[0];
559 } else {
560 $script_handle = $registration->editor_script_handles;
561 }
562
563 // Register and enqueue scripts used by the VideoPress video block.
564 Block_Editor_Extensions::init( $script_handle );
565 }
566
567 /**
568 * Register the Video Playlist block.
569 *
570 * @param string|null $metadata_file Path to the block.json metadata file. Defaults to the
571 * package build output; tests can point it at a fixture.
572 *
573 * @return void
574 */
575 public static function register_videopress_playlist_block( $metadata_file = null ) {
576 /*
577 * Unlike the video block, the playlist block has no "activate the module"
578 * placeholder, so it is only registered where VideoPress can play videos.
579 */
580 if (
581 Status::is_jetpack_plugin_without_videopress_module_active()
582 && ! Status::is_standalone_plugin_active()
583 ) {
584 return;
585 }
586
587 if ( null === $metadata_file ) {
588 $metadata_file = __DIR__ . '/../build/block-editor/blocks/playlist/block.json';
589 }
590
591 if ( ! file_exists( $metadata_file ) ) {
592 return;
593 }
594
595 $metadata = json_decode(
596 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
597 file_get_contents( $metadata_file )
598 );
599
600 if ( empty( $metadata->name )
601 || \WP_Block_Type_Registry::get_instance()->is_registered( $metadata->name )
602 ) {
603 return;
604 }
605
606 register_block_type(
607 $metadata_file,
608 array(
609 'render_callback' => array( __CLASS__, 'render_videopress_playlist_block' ),
610 )
611 );
612 }
613
614 /**
615 * Sanitize the playlist block's videos attribute into rendering-ready entries.
616 *
617 * @param mixed $videos Raw attribute value.
618 *
619 * @return array Entries with guid, title, durationMs, height and poster keys.
620 */
621 private static function sanitize_playlist_entries( $videos ) {
622 if ( ! is_array( $videos ) ) {
623 return array();
624 }
625
626 $entries = array();
627 foreach ( $videos as $video ) {
628 if ( ! is_array( $video ) || empty( $video['guid'] ) || ! is_string( $video['guid'] ) ) {
629 continue;
630 }
631
632 // VideoPress GUIDs are 8 alphanumeric characters; drop anything else.
633 if ( ! preg_match( '/^[a-zA-Z0-9]{8}$/', $video['guid'] ) ) {
634 continue;
635 }
636
637 /*
638 * Only the video reference and numeric metadata are stored. Display
639 * metadata (title, poster) is always live: the view script reads it
640 * from the video data after the page loads.
641 */
642 $entries[] = array(
643 'guid' => $video['guid'],
644 'durationMs' => isset( $video['durationMs'] ) && is_numeric( $video['durationMs'] ) ? max( 0, (int) $video['durationMs'] ) : 0,
645 'height' => isset( $video['height'] ) && is_numeric( $video['height'] ) ? max( 0, (int) $video['height'] ) : 0,
646 );
647 }
648
649 return $entries;
650 }
651
652 /**
653 * Build the VideoPress embed URL for a playlist entry.
654 *
655 * @param string $guid Video GUID.
656 * @param bool $autoplay Whether the video should start playing once loaded.
657 * @param bool $muted Whether playback should start muted.
658 *
659 * @return string Embed URL.
660 */
661 private static function playlist_embed_url( $guid, $autoplay, $muted = false ) {
662 $args = array(
663 'cover' => 1,
664 'preloadContent' => 'metadata',
665 'autoPlay' => $autoplay ? 1 : 0,
666 );
667 if ( $muted ) {
668 $args['muted'] = 1;
669 }
670
671 return add_query_arg(
672 $args,
673 'https://videopress.com/embed/' . rawurlencode( $guid )
674 );
675 }
676
677 /**
678 * Format a duration in milliseconds as a timecode, m:ss or h:mm:ss.
679 *
680 * @param int $duration_ms Duration in milliseconds.
681 *
682 * @return string Timecode, or an empty string when the duration is unknown.
683 */
684 private static function playlist_timecode( $duration_ms ) {
685 if ( $duration_ms <= 0 ) {
686 return '';
687 }
688
689 $total_seconds = (int) round( $duration_ms / 1000 );
690 $hours = intdiv( $total_seconds, 3600 );
691 $minutes = intdiv( $total_seconds % 3600, 60 );
692 $seconds = $total_seconds % 60;
693
694 if ( $hours > 0 ) {
695 return sprintf( '%d:%02d:%02d', $hours, $minutes, $seconds );
696 }
697
698 return sprintf( '%d:%02d', $minutes, $seconds );
699 }
700
701 /**
702 * Format a duration in milliseconds as a long runtime, e.g. "1 hr 13 min".
703 *
704 * @param int $duration_ms Duration in milliseconds.
705 *
706 * @return string Runtime label, or an empty string when the duration is unknown.
707 */
708 private static function playlist_runtime_label( $duration_ms ) {
709 if ( $duration_ms <= 0 ) {
710 return '';
711 }
712
713 $total_minutes = max( 1, (int) round( $duration_ms / 60000 ) );
714 $hours = intdiv( $total_minutes, 60 );
715 $minutes = $total_minutes % 60;
716
717 if ( $hours > 0 && $minutes > 0 ) {
718 /* translators: 1: number of hours. 2: number of minutes. */
719 return sprintf( __( '%1$d hr %2$d min', 'jetpack-videopress-pkg' ), $hours, $minutes );
720 }
721
722 if ( $hours > 0 ) {
723 /* translators: %d: number of hours. */
724 return sprintf( __( '%d hr', 'jetpack-videopress-pkg' ), $hours );
725 }
726
727 /* translators: %d: number of minutes. */
728 return sprintf( __( '%d min', 'jetpack-videopress-pkg' ), $minutes );
729 }
730
731 /**
732 * Map a video's pixel height to a resolution label, e.g. "1080p" or "4K".
733 *
734 * @param int $height Video height in pixels.
735 *
736 * @return string Resolution label, or an empty string when the height is unknown.
737 */
738 private static function playlist_resolution_label( $height ) {
739 if ( $height <= 0 ) {
740 return '';
741 }
742
743 return $height >= 2160 ? '4K' : $height . 'p';
744 }
745
746 /**
747 * Video Playlist block render callback.
748 *
749 * @param array $block_attributes Block attributes.
750 *
751 * @return string Block markup, or an empty string when the playlist has no playable entries.
752 */
753 public static function render_videopress_playlist_block( $block_attributes ) {
754 $entries = self::sanitize_playlist_entries( $block_attributes['videos'] ?? null );
755
756 if ( ! $entries ) {
757 return '';
758 }
759
760 $enabled = function ( $key, $default_value = true ) use ( $block_attributes ) {
761 return isset( $block_attributes[ $key ] ) ? (bool) $block_attributes[ $key ] : $default_value;
762 };
763
764 $layout = isset( $block_attributes['layout'] ) && in_array( $block_attributes['layout'], array( 'side-rail', 'grid', 'strip' ), true )
765 ? $block_attributes['layout']
766 : 'side-rail';
767
768 $show_thumbnail = $enabled( 'showThumbnail' );
769 $show_title = $enabled( 'showTitle' );
770 $show_res = $enabled( 'showResolution' );
771 $show_duration = $enabled( 'showDuration' );
772 $show_number = $enabled( 'showPositionNumber', false );
773 $show_runtime = $enabled( 'showTotalRuntime' );
774 $muted = $enabled( 'muteByDefault', false );
775
776 $classes = array( 'videopress-playlist', 'is-layout-' . $layout );
777 if ( $enabled( 'darkPlayer', false ) ) {
778 $classes[] = 'is-dark';
779 }
780 if ( ! $show_thumbnail ) {
781 $classes[] = 'hide-thumbnails';
782 }
783 if ( ! $show_title ) {
784 $classes[] = 'hide-titles';
785 }
786 if ( ! $show_res ) {
787 $classes[] = 'hide-resolutions';
788 }
789 if ( ! $show_duration ) {
790 $classes[] = 'hide-durations';
791 }
792 if ( ! $show_runtime ) {
793 $classes[] = 'hide-runtime';
794 }
795
796 // Lets the embed play private videos for authorized viewers.
797 Jwt_Token_Bridge::enqueue_jwt_token_bridge();
798
799 $count = count( $entries );
800 $total_ms = 0;
801 foreach ( $entries as $entry ) {
802 $total_ms += $entry['durationMs'];
803 }
804
805 $total_timecode = self::playlist_timecode( $total_ms );
806 /* translators: %d: number of videos in the playlist. */
807 $count_label = sprintf( _n( '%d video', '%d videos', $count, 'jetpack-videopress-pkg' ), $count );
808
809 $items = '';
810 foreach ( $entries as $index => $entry ) {
811 /*
812 * Positional fallback only: the view script replaces titles (and
813 * adds posters) with live video data once the page loads.
814 */
815 /* translators: %d: position of the video in the playlist. */
816 $title = sprintf( __( 'Video %d', 'jetpack-videopress-pkg' ), $index + 1 );
817
818 $timecode = self::playlist_timecode( $entry['durationMs'] );
819 $resolution = self::playlist_resolution_label( $entry['height'] );
820 /* translators: 1: position of the video in the playlist. 2: number of videos in the playlist. */
821 $position = sprintf( __( '%1$d of %2$d', 'jetpack-videopress-pkg' ), $index + 1, $count );
822 $details = implode( ' · ', array_filter( array( $resolution, $timecode ) ) );
823 $progress = '' !== $total_timecode
824 /* translators: 1: position of the video in the playlist. 2: number of videos. 3: total playlist timecode. */
825 ? sprintf( __( '%1$d / %2$d · %3$s total', 'jetpack-videopress-pkg' ), $index + 1, $count, $total_timecode )
826 /* translators: 1: position of the video in the playlist. 2: number of videos. */
827 : sprintf( __( '%1$d / %2$d', 'jetpack-videopress-pkg' ), $index + 1, $count );
828
829 $number_markup = $show_number
830 ? sprintf(
831 '<span class="videopress-playlist__entry-number">%s</span>',
832 esc_html( str_pad( (string) ( $index + 1 ), 2, '0', STR_PAD_LEFT ) )
833 )
834 : '';
835
836 $time_markup = '' !== $timecode
837 ? sprintf( '<span class="videopress-playlist__entry-time">%s</span>', esc_html( $timecode ) )
838 : '';
839
840 $resolution_markup = '' !== $resolution
841 ? sprintf( '<span class="videopress-playlist__entry-resolution">%s</span>', esc_html( $resolution ) )
842 : '';
843
844 $duration_markup = '' !== $timecode
845 ? sprintf( '<span class="videopress-playlist__entry-duration">%s</span>', esc_html( $timecode ) )
846 : '';
847
848 $items .= sprintf(
849 '<li class="videopress-playlist__entry"><button type="button" class="videopress-playlist__select%1$s"%2$s data-guid="%3$s" data-embed-url="%4$s" data-title="%5$s" data-position="%6$s" data-details="%7$s" data-progress="%8$s">' .
850 '%9$s<span class="videopress-playlist__entry-thumb"><span class="videopress-playlist__entry-flag">%10$s</span>%11$s</span>' .
851 '<span class="videopress-playlist__entry-body"><span class="videopress-playlist__entry-title">%12$s</span><span class="videopress-playlist__entry-meta">%13$s%14$s</span></span>' .
852 '</button></li>',
853 0 === $index ? ' is-current' : '',
854 0 === $index ? ' aria-current="true"' : '',
855 esc_attr( $entry['guid'] ),
856 esc_url( self::playlist_embed_url( $entry['guid'], true, $muted ) ),
857 esc_attr( $title ),
858 esc_attr( $position ),
859 esc_attr( $details ),
860 esc_attr( $progress ),
861 $number_markup,
862 esc_html__( 'Playing', 'jetpack-videopress-pkg' ),
863 $time_markup,
864 esc_html( $title ),
865 $resolution_markup,
866 $duration_markup
867 );
868 }
869
870 $first = $entries[0];
871 $first_title = __( 'Video 1', 'jetpack-videopress-pkg' );
872 $runtime_label = self::playlist_runtime_label( $total_ms );
873 $runtime_markup = $show_runtime && '' !== $runtime_label
874 ? sprintf( '<span class="videopress-playlist__runtime">%s</span>', esc_html( $runtime_label ) )
875 : '';
876
877 // Count · runtime meta line, shown by the side-rail layout in the list header.
878 $list_meta_markup = sprintf(
879 '<span class="videopress-playlist__list-meta"><span class="videopress-playlist__count">%s</span>%s</span>',
880 esc_html( $count_label ),
881 $runtime_markup
882 );
883
884 /*
885 * The player renders its own title overlay, so the stage carries no
886 * duplicate now-playing text — only the grid layout's runtime line.
887 */
888 $now_markup = $show_runtime && '' !== $runtime_label
889 ? sprintf(
890 '<div class="videopress-playlist__now"><span class="videopress-playlist__now-runtime">%s</span></div>',
891 esc_html( $count_label . ' · ' . $runtime_label )
892 )
893 : '';
894
895 $stage_markup = sprintf(
896 '<div class="videopress-playlist__stage">' .
897 '<div class="videopress-playlist__player"><iframe class="videopress-playlist__iframe" title="%1$s" src="%2$s" allowfullscreen allow="clipboard-write"></iframe></div>%3$s</div>',
898 esc_attr( $first_title ),
899 esc_url( self::playlist_embed_url( $first['guid'], false, $muted ) ),
900 $now_markup
901 );
902
903 $progress_markup = '' !== $total_timecode
904 ? sprintf(
905 '<span class="videopress-playlist__list-progress">%s</span>',
906 /* translators: 1: position of the current video. 2: number of videos. 3: total playlist timecode. */
907 esc_html( sprintf( __( '%1$d / %2$d · %3$s total', 'jetpack-videopress-pkg' ), 1, $count, $total_timecode ) )
908 )
909 : '';
910
911 $list_markup = sprintf(
912 '<div class="videopress-playlist__list">' .
913 '<div class="videopress-playlist__list-header">' .
914 '<span class="videopress-playlist__list-label videopress-playlist__list-label--rail">%1$s</span>' .
915 '<span class="videopress-playlist__list-label videopress-playlist__list-label--strip">%2$s</span>%3$s%4$s</div>' .
916 '<ol class="videopress-playlist__entries">%5$s</ol></div>',
917 esc_html__( 'Up next', 'jetpack-videopress-pkg' ),
918 /* translators: %s: number of videos in the playlist, e.g. "5 videos". */
919 esc_html( sprintf( __( 'Playlist — %s', 'jetpack-videopress-pkg' ), $count_label ) ),
920 $list_meta_markup,
921 $progress_markup,
922 $items
923 );
924
925 /*
926 * User-selected theme font presets (theme.json slugs) for the
927 * customizable titles, exposed as CSS custom properties the
928 * stylesheet reads. Anything but a plain preset slug is dropped.
929 */
930 $font_style = '';
931 foreach ( array(
932 'entryTitleFontFamily' => '--vpp-entry-title-font',
933 ) as $font_attribute => $css_variable ) {
934 if ( ! empty( $block_attributes[ $font_attribute ] )
935 && is_string( $block_attributes[ $font_attribute ] )
936 && preg_match( '/^[a-zA-Z0-9-]+$/', $block_attributes[ $font_attribute ] )
937 ) {
938 $font_style .= $css_variable . ':var(--wp--preset--font-family--' . $block_attributes[ $font_attribute ] . ');';
939 }
940 }
941
942 // Looping implies auto-advancing, so it forces the autoplay-next flag on.
943 $loop_playlist = $enabled( 'loopPlaylist', false );
944
945 $wrapper_extra_attributes = array(
946 'class' => implode( ' ', $classes ),
947 'data-autoplay-next' => $enabled( 'autoplayNext', false ) || $loop_playlist ? '1' : '0',
948 'data-loop' => $loop_playlist ? '1' : '0',
949 );
950 if ( '' !== $font_style ) {
951 $wrapper_extra_attributes['style'] = $font_style;
952 }
953
954 $wrapper_attributes = get_block_wrapper_attributes( $wrapper_extra_attributes );
955
956 return sprintf(
957 '<figure %1$s><div class="videopress-playlist__body">%2$s%3$s</div></figure>',
958 $wrapper_attributes,
959 $stage_markup,
960 $list_markup
961 );
962 }
963
964 /**
965 * Enqueue the VideoPress Iframe API script
966 * when the URL of oEmbed HTML is a VideoPress URL.
967 *
968 * @param string|false $cache The cached HTML result, stored in post meta.
969 * @param string $url The attempted embed URL.
970 * @param array $attr An array of shortcode attributes.
971 * @param int $post_ID Post ID.
972 *
973 * @return string|false
974 */
975 public static function enqueue_videopress_iframe_api_script( $cache, $url, $attr, $post_ID ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
976 if ( Utils::is_videopress_url( $url ) ) {
977 // Enqueue the VideoPress IFrame API in the front-end.
978 wp_enqueue_script(
979 self::JETPACK_VIDEOPRESS_IFRAME_API_HANDLER,
980 'https://s0.wp.com/wp-content/plugins/video/assets/js/videojs/videopress-iframe-api.js',
981 array(),
982 gmdate( 'YW' ),
983 false
984 );
985 }
986
987 return $cache;
988 }
989 }
990