Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
36.79% covered (danger)
36.79%
117 / 318
45.16% covered (danger)
45.16%
14 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
Admin_UI
36.79% covered (danger)
36.79%
117 / 318
45.16% covered (danger)
45.16%
14 / 31
1418.71
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 maybe_load_wp_build
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 get_dashboard_render_callback
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 enable_menu
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 enable_dashboard_menu
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 init_inactive_menu
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 enable_inactive_menu
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 add_wp_admin_submenu
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 get_admin_page_url
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_allowed_video_extensions
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 admin_init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_admin_shell_member
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 load_as_admin_shell_guest
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_guest_initial_state
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 remove_jetpack_hooks
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 enqueue_admin_scripts
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 edit_video_link
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 get_logo_svg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 attachment_details_two_column_template
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
2
 attachment_details_template
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
2
 attachment_info_template_part
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 load_wp_build
20.00% covered (danger)
20.00%
3 / 15
0.00% covered (danger)
0.00%
0 / 1
7.61
 load_wp_build_with_screen_alias
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
2.06
 maybe_strip_chapters_editor_routes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 strip_chapters_editor_routes
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 alias_screen_id_for_wp_build
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 restore_screen_id_after_wp_build
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 is_modernized
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_trim_cut_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_chapters_editor_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_videopress_admin_request
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * The initializer class for Admin UI elements
4 *
5 * @package automattic/jetpack-videopress
6 */
7
8namespace Automattic\Jetpack\VideoPress;
9
10use Automattic\Jetpack\Admin_UI\Admin_Menu;
11use Automattic\Jetpack\Admin_UI\Admin_Shell;
12use Automattic\Jetpack\Connection\Manager as Connection_Manager;
13use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
14use Automattic\Jetpack\Status\Host;
15use Automattic\Jetpack\VideoPress\Status as VideoPress_Status;
16
17/**
18 * Initialized the VideoPress package
19 */
20class Admin_UI {
21
22    const JETPACK_VIDEOPRESS_PKG_NAMESPACE = 'jetpack-videopress-pkg';
23
24    const ADMIN_PAGE_SLUG = 'jetpack-videopress';
25
26    /**
27     * The name hosts use for the VideoPress sidebar item in the visibility filter.
28     *
29     * Deliberately not ADMIN_PAGE_SLUG: this is a name hosts write into their own code, so
30     * it has to survive a slug change. Both registrations below share it, since they are
31     * mutually exclusive and a host hiding VideoPress should not have to know which is live.
32     */
33    const VISIBILITY_KEY = 'jetpack-videopress';
34
35    /**
36     * The My Jetpack interstitial where VideoPress can be activated, relative to wp-admin.
37     *
38     * Used as the target of the "Jetpack > VideoPress" menu item when VideoPress
39     * is not active.
40     */
41    const MY_JETPACK_ADD_VIDEOPRESS_URI = 'admin.php?page=my-jetpack#/add-videopress';
42
43    /**
44     * Filter name that gates the wp-build–based dashboard.
45     *
46     * When this filter returns false, no "Jetpack > VideoPress" menu is registered.
47     */
48    const MODERNIZATION_FILTER = 'rsm_jetpack_ui_modernization_videopress';
49
50    /**
51     * Filter name that gates the chapters editor.
52     *
53     * The chapters editor is generally available, so this defaults to true and
54     * the filter serves as a kill switch: returning false withdraws it from both
55     * places it lives — the dashboard's Editor tab (and the `/video/$id/editor`
56     * route behind it) and the block editor's chapter manager modal.
57     */
58    const CHAPTERS_EDITOR_FILTER = 'jetpack_videopress_chapters_editor';
59
60    /**
61     * Route paths that belong to the chapters editor.
62     *
63     * Kept in sync with the wp-build route registry (`build/routes/registry.php`);
64     * entries with these paths are stripped from the registry when the chapters
65     * editor filter is off, so the routes never register.
66     */
67    const CHAPTERS_EDITOR_ROUTE_PATHS = array( '/video/$id/editor' );
68
69    /**
70     * Carries the initial state when the dashboard loads as an admin shell guest.
71     */
72    const GUEST_DATA_HANDLE = 'jetpack-videopress-dashboard-data';
73
74    /**
75     * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored.
76     *
77     * @var string|null
78     */
79    private static $wp_build_original_screen_id = null;
80
81    /**
82     * Initializes the Admin UI of VideoPress
83     *
84     * This method is called only once by the Initializer class
85     *
86     * @return void
87     */
88    public static function init() {
89
90        // Load the wp-build dashboard assets on the VideoPress admin page, on every
91        // host. Registered before enable_menu() below — both hook admin_menu:1, and
92        // same-priority callbacks fire in registration order — so the render function
93        // and SCRIPT_HANDLE exist by the time the menu callback is selected: on
94        // standalone/Atomic by enable_menu(), on wpcom Simple by add_wp_admin_submenu()
95        // (wpcom-admin-menu.php, admin_menu:999999). maybe_load_wp_build() self-gates
96        // to the VideoPress admin request, so it's inert on every other page.
97        add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
98
99        if ( class_exists( Admin_Shell::class ) ) {
100            add_filter( 'jetpack_admin_shell_members', array( __CLASS__, 'add_admin_shell_member' ) );
101        }
102
103        // On WordPress.com Simple the standalone menu system (Admin_Menu) is not used:
104        // the Jetpack parent menu is created late (admin_menu priority 999999) by
105        // wpcom-admin-menu.php, which registers the VideoPress submenu directly via
106        // Admin_UI::add_wp_admin_submenu(). So on Simple we skip enable_menu() and the
107        // Jetpack/Atomic media-library hooks below — the media hooks are Phase 3 media
108        // parity, out of scope for menu + boot, and would patch a media library wpcom
109        // owns (their VideoPress branches are inert on Simple anyway: videos keep their
110        // original mime, never video/videopress).
111        if ( ( new Host() )->is_wpcom_simple() ) {
112            return;
113        }
114
115        add_action( 'admin_menu', array( __CLASS__, 'enable_menu' ), 1 ); // Akismet uses 4, so we use 1 to ensure both menus are added when only they exist.
116
117        add_action( 'admin_footer-upload.php', array( __CLASS__, 'attachment_details_two_column_template' ) );
118        add_action( 'admin_footer-post.php', array( __CLASS__, 'attachment_details_template' ), 20 );
119
120        add_filter( 'get_edit_post_link', array( __CLASS__, 'edit_video_link' ), 10, 3 );
121
122        add_action( 'admin_init', array( __CLASS__, 'remove_jetpack_hooks' ) );
123    }
124
125    /**
126     * Load the wp-build dashboard assets when modernization is enabled and the current
127     * request targets the VideoPress admin page.
128     *
129     * The single wp-build load path on every host, hooked at admin_menu:1 from init().
130     * Requiring build.php here defines the render function that enable_menu() /
131     * add_wp_admin_submenu() select later in the same hook, and registers the
132     * SCRIPT_HANDLE that Initial_State::enqueue() hydrates from.
133     *
134     * @return void
135     */
136    public static function maybe_load_wp_build() {
137        if ( ! self::is_modernized() || ! self::is_videopress_admin_request() ) {
138            return;
139        }
140
141        self::load_wp_build_with_screen_alias();
142    }
143
144    /**
145     * Select the dashboard render callback, or null when the dashboard can't render.
146     *
147     * The wp-build render function is only loaded on the VideoPress page itself (see
148     * maybe_load_wp_build()), so it is only checked there; other admin pages still
149     * register the menu item.
150     *
151     * @return string|null Callback for Admin_Menu::add_menu()/add_submenu_page().
152     */
153    private static function get_dashboard_render_callback() {
154        if ( ! self::is_modernized() ) {
155            return null;
156        }
157
158        $callback = 'jetpack_videopress_jetpack_videopress_dashboard_wp_admin_render_page';
159        if ( self::is_videopress_admin_request() && ! function_exists( $callback ) ) {
160            return null;
161        }
162
163        return $callback;
164    }
165
166    /**
167     * Register the "Jetpack > VideoPress" menu item, choosing its target dynamically.
168     *
169     * Hooked on admin_menu from both init() and init_inactive_menu(), so the item
170     * reflects the module state when the menu renders rather than when the package
171     * initialized: when VideoPress is active it opens the VideoPress library
172     * (admin.php?page=jetpack-videopress); when it is not, it links to the
173     * My Jetpack interstitial where VideoPress can be activated.
174     *
175     * Registered separately to init due to translations needing to run early for
176     * the page suffix.
177     *
178     * @return void
179     */
180    public static function enable_menu() {
181        if ( VideoPress_Status::is_active() ) {
182            self::enable_dashboard_menu();
183            return;
184        }
185
186        self::enable_inactive_menu();
187    }
188
189    /**
190     * Register the VideoPress library dashboard page and its menu item.
191     *
192     * @return void
193     */
194    private static function enable_dashboard_menu() {
195        $callback = self::get_dashboard_render_callback();
196        if ( null === $callback ) {
197            return;
198        }
199
200        $page_suffix = Admin_Menu::add_menu(
201            // "VideoPress" is a product name, do not translate.
202            'Jetpack VideoPress',
203            'VideoPress',
204            'manage_options',
205            self::ADMIN_PAGE_SLUG,
206            $callback,
207            null,
208            array(
209                'product' => 'videopress',
210                'key'     => self::VISIBILITY_KEY,
211            )
212        );
213        add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
214    }
215
216    /**
217     * Initializes the "Jetpack > VideoPress" menu item when VideoPress is not active.
218     *
219     * This method is called by the Initializer class instead of init() when the
220     * VideoPress module is not active. It hooks the same enable_menu() callback as
221     * init(), which picks the item's target from the module state at admin_menu
222     * time — so if the state changed since package initialization, the rendered
223     * link is still correct.
224     *
225     * @return void
226     */
227    public static function init_inactive_menu() {
228        // On WordPress.com Simple the standalone menu system (Admin_Menu) is not used;
229        // the Jetpack menu is fully managed by wpcom-admin-menu.php. See init().
230        if ( ( new Host() )->is_wpcom_simple() ) {
231            return;
232        }
233
234        add_action( 'admin_menu', array( __CLASS__, 'enable_menu' ), 1 );
235    }
236
237    /**
238     * Register the inactive-state menu item: a direct link to the My Jetpack
239     * "add VideoPress" interstitial.
240     *
241     * A slug that is not a registered page makes WordPress render the item as a
242     * plain link to that URI (same mechanism as the "Upgrade Jetpack" menu item).
243     *
244     * @return void
245     */
246    public static function enable_inactive_menu() {
247        // Activating VideoPress from My Jetpack requires a connected user, so only
248        // offer the item to users with their own Jetpack connection. Checked here,
249        // at admin_menu time, so the item starts rendering as soon as the current
250        // user connects.
251        $connection = new Connection_Manager();
252        if ( ! $connection->is_connected() || ! $connection->is_user_connected() ) {
253            return;
254        }
255
256        // The link targets My Jetpack; don't render a dead link when that page
257        // is not registered (e.g. offline mode).
258        if ( ! My_Jetpack_Initializer::should_initialize() ) {
259            return;
260        }
261
262        Admin_Menu::add_menu(
263            // "VideoPress" is a product name, do not translate.
264            'Jetpack VideoPress',
265            'VideoPress',
266            'manage_options',
267            self::MY_JETPACK_ADD_VIDEOPRESS_URI,
268            null,
269            null,
270            // Hidden while VideoPress is off, unless a host forces the shared key visible.
271            array(
272                'product' => 'videopress',
273                'key'     => self::VISIBILITY_KEY,
274            )
275        );
276    }
277
278    /**
279     * Add the VideoPress submenu directly under the Jetpack menu on wpcom Simple.
280     *
281     * Called from wpcom-admin-menu.php at a late priority (999999), once the Jetpack
282     * parent menu exists. On Simple enable_menu() is skipped (see init()), so this is
283     * the only place the submenu is registered there. The callback mirrors enable_menu():
284     * the wp-build render function (loaded by maybe_load_wp_build() at admin_menu:1,
285     * which runs first), and no menu when the dashboard can't render.
286     *
287     * @return void
288     */
289    public static function add_wp_admin_submenu() {
290        $callback = self::get_dashboard_render_callback();
291        if ( null === $callback ) {
292            return;
293        }
294
295        $page_suffix = add_submenu_page(
296            'jetpack',
297            // "VideoPress" is a product name, do not translate.
298            'Jetpack VideoPress',
299            'VideoPress',
300            'manage_options',
301            self::ADMIN_PAGE_SLUG,
302            $callback
303        );
304
305        if ( $page_suffix ) {
306            add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
307        }
308    }
309
310    /**
311     * Gets the URL for the VideoPress admin page
312     *
313     * @return string
314     */
315    public static function get_admin_page_url() {
316        return admin_url( 'admin.php?page=' . self::ADMIN_PAGE_SLUG );
317    }
318
319    /**
320     * Gets the list of allowed video extensions
321     *
322     * @return array
323     */
324    public static function get_allowed_video_extensions() {
325        return array(
326            '3g2'  => 'video/3gpp2',
327            '3gp'  => 'video/3gpp',
328            '3gp2' => 'video/3gpp2',
329            '3gpp' => 'video/3gpp',
330            'avi'  => 'video/avi',
331            'm4v'  => 'video/mp4',
332            'mov'  => 'video/quicktime',
333            'mp4'  => 'video/mp4',
334            'mpe'  => 'video/mpeg',
335            'mpeg' => 'video/mpeg',
336            'mpg'  => 'video/mpeg',
337            'ogv'  => 'video/ogg',
338            'wmv'  => 'video/x-ms-wmv',
339        );
340    }
341
342    /**
343     * Initialize the admin resources.
344     */
345    public static function admin_init() {
346        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_scripts' ) );
347    }
348
349    /**
350     * Register VideoPress with the admin shell, which switches between Jetpack dashboards without a page load.
351     *
352     * @since $$next-version$$
353     *
354     * @param array $members Shell members keyed by admin page slug.
355     * @return array
356     */
357    public static function add_admin_shell_member( $members ) {
358        // The same gates enable_dashboard_menu() applies, since admin-ajax never builds the menu.
359        if (
360            ( new Host() )->is_wpcom_simple()
361            || ! self::is_modernized()
362            || ! VideoPress_Status::is_active()
363            || ! file_exists( dirname( __DIR__ ) . '/build/build.php' )
364        ) {
365            return $members;
366        }
367
368        $members[ self::ADMIN_PAGE_SLUG ] = array(
369            'build_prefix' => 'jetpack_videopress',
370            'build_page'   => 'jetpack-videopress-dashboard',
371            'init_modules' => array( '@jetpack-videopress/init' ),
372            // "VideoPress" is a product name, do not translate.
373            'title'        => 'Jetpack VideoPress',
374            'capability'   => 'manage_options',
375            'handles'      => array( self::GUEST_DATA_HANDLE, 'jetpack-videopress-dashboard-shell', Admin_Menu::DESIGN_TOKENS_HANDLE, 'jetpack-script-data', 'wp-jp-i18n-loader' ),
376            'media'        => true,
377            'load'         => array( __CLASS__, 'load_as_admin_shell_guest' ),
378        );
379        return $members;
380    }
381
382    /**
383     * Ready the VideoPress routes and page data while another dashboard's page is open, without the screen ID alias.
384     *
385     * @since $$next-version$$
386     *
387     * @return void
388     */
389    public static function load_as_admin_shell_guest() {
390        self::load_wp_build();
391        // The page init action that runs this never fires for a guest, and the shell reads the routes first.
392        self::maybe_strip_chapters_editor_routes();
393        self::admin_init();
394        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_guest_initial_state' ) );
395    }
396
397    /**
398     * Attach the initial state to a script of its own: the prerequisites script only exists on the VideoPress page.
399     *
400     * @since $$next-version$$
401     *
402     * @return void
403     */
404    public static function enqueue_guest_initial_state() {
405        wp_register_script( self::GUEST_DATA_HANDLE, false, array(), false, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion -- No source, so nothing to cache.
406        wp_enqueue_script( self::GUEST_DATA_HANDLE );
407        Initial_State::attach_to( self::GUEST_DATA_HANDLE );
408    }
409
410    /**
411     * Remove extra fields from Attachment details modal
412     *
413     * @return void
414     */
415    public static function remove_jetpack_hooks() {
416        if ( class_exists( '\VideoPress_Edit_Attachment' ) ) {
417            $edit_attachment = \VideoPress_Edit_Attachment::init();
418            remove_filter( 'attachment_fields_to_edit', array( $edit_attachment, 'fields_to_edit' ) );
419            remove_filter( 'attachment_fields_to_save', array( $edit_attachment, 'save_fields' ) );
420        }
421    }
422
423    /**
424     * Enqueue plugin admin scripts and styles.
425     */
426    public static function enqueue_admin_scripts() {
427        // This callback is registered via `load-{$page_suffix}` in `enable_menu()`,
428        // so it only fires on the VideoPress admin page — no need to re-check the page here.
429        Admin_Menu::enqueue_design_tokens();
430
431        // Page-level shell stylesheet: scopes the shared `jetpack-admin-page-layout`
432        // mixin to the dashboard body so every route inherits the proper
433        // scrollable chrome (fixed `#wpbody-content`, scrollable middle, pinned
434        // footer) regardless of whether it uses DashboardLayout. Without this,
435        // non-tabbed routes (e.g. Video details) would only get the layout
436        // after a sibling route's chunk happened to inject the same CSS.
437        // Pick the flipped build ourselves on RTL locales rather than leaning on
438        // `wp_style_add_data( …, 'rtl', 'replace' )`: core resolves that by
439        // rewriting `index.css` to `index-rtl.css` (hyphenated) *and* dropping
440        // the LTR tag, but webpack emits the flipped file as `index.rtl.css`
441        // (dotted, the same convention `Assets::register_script()`'s `css_path`
442        // follows). The hyphenated file never exists, so RTL sites used to load
443        // no shell stylesheet at all — the layout flex chain never formed and
444        // the dashboard rendered as a blank page.
445        $shell_dir  = dirname( __DIR__ ) . '/build/dashboard-shell/';
446        $shell_file = is_rtl() && file_exists( $shell_dir . 'index.rtl.css' ) ? 'index.rtl.css' : 'index.css';
447        if ( file_exists( $shell_dir . $shell_file ) ) {
448            wp_register_style(
449                'jetpack-videopress-dashboard-shell',
450                plugins_url( 'build/dashboard-shell/' . $shell_file, __DIR__ ),
451                array(),
452                (string) filemtime( $shell_dir . $shell_file )
453            );
454            wp_enqueue_style( 'jetpack-videopress-dashboard-shell' );
455        }
456
457        // The Video details screen's thumbnail editor opens the WordPress
458        // media library (via window.wp.media) for the "Upload image"
459        // action, so the media scripts must be present here too.
460        wp_enqueue_media();
461
462        // The i18n loader is registered on every admin page by jetpack-assets but
463        // only enqueued when depended on; the esbuild bundles don't pull it in.
464        if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
465            wp_enqueue_script( 'wp-jp-i18n-loader' );
466        }
467
468        // Beyond the shell stylesheet and the media library, wp-build
469        // manages its own enqueue pipeline.
470    }
471
472    /**
473     * Replaces the edit link for videopress videos
474     *
475     * @param string $link - the post link.
476     * @param int    $post_id - the post ID.
477     * @param string $context - the context.
478     *
479     * @return string
480     */
481    public static function edit_video_link( $link, $post_id, $context ) {
482        $post_id = (int) $post_id;
483        if ( ! $post_id ) {
484            return $link;
485        }
486
487        $post = get_post( $post_id );
488        if ( ! $post ) {
489            return $link;
490        }
491
492        if ( 'attachment' !== $post->post_type || 'video/videopress' !== $post->post_mime_type ) {
493            return $link;
494        }
495
496        $url = add_query_arg( 'p', sprintf( '/video/%d', $post_id ), self::get_admin_page_url() );
497
498        if ( 'display' === $context ) {
499            return esc_url( $url );
500        }
501
502        return esc_url_raw( $url );
503    }
504
505    /**
506     * Gets the SVG for the sub brand Jetpack VideoPress logo
507     *
508     * @return string
509     */
510    protected static function get_logo_svg() {
511        return '<svg width="212" height="27" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M110.441 16.294c.479-1.458.995-2.93 1.547-4.414l2.545-6.87h1.687l-5.09 13.361h-1.427l-5.09-13.361h1.687l2.545 6.87a109.628 109.628 0 0 1 1.556 4.414h.04ZM117.919 18.371V8.884h1.547v9.487h-1.547Zm-.04-11.694V4.95h1.627v1.727h-1.627ZM123.331 13.728c0 .546.06 1.042.179 1.488.127.439.306.815.539 1.128.233.306.519.543.858.71.34.166.729.249 1.168.249.326 0 .622-.044.888-.13.266-.087.503-.196.709-.33a3.045 3.045 0 0 0 .898-.838V11.04a3.706 3.706 0 0 0-1.108-.779c-.419-.2-.871-.3-1.357-.3a2.83 2.83 0 0 0-.928.17 2.26 2.26 0 0 0-.898.59c-.273.286-.499.672-.679 1.158-.179.48-.269 1.095-.269 1.848Zm-1.617.02c0-.66.063-1.242.19-1.748.133-.513.306-.959.519-1.338.212-.38.462-.696.748-.949.286-.26.582-.47.888-.629.306-.16.616-.273.928-.34.32-.073.619-.11.898-.11.579 0 1.082.097 1.507.29.426.193.809.476 1.148.85h.03V3.91h1.547v14.46h-1.168l-.319-1.058h-.04c-.12.153-.26.31-.419.47-.16.152-.349.292-.569.419a2.83 2.83 0 0 1-.749.3 3.57 3.57 0 0 1-.978.119 4.63 4.63 0 0 1-1.526-.26 3.666 3.666 0 0 1-1.338-.849c-.386-.392-.698-.895-.938-1.507-.239-.62-.359-1.372-.359-2.257ZM136.626 9.963c-.352 0-.682.07-.988.21a2.57 2.57 0 0 0-.788.569c-.226.24-.409.522-.549.849-.14.32-.226.662-.259 1.028h4.79c0-.373-.05-.719-.15-1.038a2.474 2.474 0 0 0-.419-.85 1.935 1.935 0 0 0-.689-.559 2.046 2.046 0 0 0-.948-.21Zm.849 7.31c.512 0 .978-.04 1.397-.12a8.775 8.775 0 0 0 1.267-.36v1.289c-.339.166-.758.296-1.257.39-.499.1-1.035.149-1.607.149a6.871 6.871 0 0 1-1.916-.26 4.108 4.108 0 0 1-1.557-.838c-.446-.387-.798-.89-1.058-1.508-.253-.626-.379-1.379-.379-2.257 0-.866.12-1.615.359-2.247.24-.64.559-1.172.958-1.598.4-.426.859-.746 1.378-.959a4.238 4.238 0 0 1 1.626-.32c.532 0 1.028.09 1.487.27.466.173.868.45 1.208.83.339.379.605.868.798 1.467.2.6.299 1.318.299 2.157v.21c0 .053-.003.18-.009.38h-6.507c0 .605.09 1.121.269 1.547.187.42.436.763.749 1.029.319.26.692.45 1.117.57.426.119.885.179 1.378.179ZM149.623 13.638c0-.586-.074-1.105-.22-1.558a3.288 3.288 0 0 0-.599-1.159 2.421 2.421 0 0 0-.908-.718 2.674 2.674 0 0 0-1.157-.25c-.42 0-.806.083-1.158.25a2.5 2.5 0 0 0-.918.719c-.253.312-.453.699-.599 1.158-.14.453-.21.972-.21 1.558 0 .579.07 1.098.21 1.558.146.452.349.835.609 1.148.259.313.565.553.918.72.352.159.738.239 1.157.239.42 0 .802-.08 1.148-.24a2.56 2.56 0 0 0 .908-.719c.26-.313.459-.696.599-1.148.146-.46.22-.979.22-1.558Zm1.616 0a6.17 6.17 0 0 1-.319 2.037 4.617 4.617 0 0 1-.908 1.578 3.987 3.987 0 0 1-1.407 1.009c-.552.24-1.171.359-1.857.359-.705 0-1.337-.12-1.896-.36a4.09 4.09 0 0 1-1.417-1.008 4.562 4.562 0 0 1-.888-1.578 6.361 6.361 0 0 1-.309-2.037c0-.752.106-1.435.319-2.047a4.697 4.697 0 0 1 .898-1.578c.393-.44.865-.78 1.417-1.019a4.69 4.69 0 0 1 1.867-.36c.698 0 1.327.12 1.886.36.559.24 1.031.58 1.417 1.019.386.44.682.965.888 1.578.206.612.309 1.295.309 2.047ZM155.204 18.371h-1.627V5.01h3.703c.552 0 1.061.033 1.527.1.472.066.905.17 1.297.309.798.286 1.404.716 1.816 1.288.413.566.619 1.265.619 2.097 0 .68-.136 1.279-.409 1.798a3.473 3.473 0 0 1-1.168 1.288c-.512.346-1.137.61-1.876.79-.732.172-1.563.259-2.495.259-.446 0-.908-.02-1.387-.06v5.492Zm0-6.98a12.234 12.234 0 0 0 1.367.08c.765 0 1.417-.06 1.956-.18.539-.12.978-.293 1.318-.52.339-.232.585-.512.738-.838.153-.333.23-.71.23-1.129 0-.519-.127-.945-.38-1.278-.246-.333-.598-.586-1.057-.759a4.097 4.097 0 0 0-.988-.22 10.59 10.59 0 0 0-1.298-.07h-1.886v4.914ZM169.62 10.143h-.12c-.365 0-.725.033-1.077.1a4.45 4.45 0 0 0-.998.309c-.306.133-.586.3-.839.5a2.74 2.74 0 0 0-.618.708v6.611h-1.547V8.884h1.197l.3 1.508h.03c.153-.246.339-.476.559-.689.226-.213.479-.4.758-.56a3.72 3.72 0 0 1 .918-.369c.333-.093.682-.14 1.048-.14.067 0 .133.004.2.01.073 0 .136.004.189.01v1.489ZM174.673 9.963c-.353 0-.682.07-.988.21-.3.14-.562.33-.789.569-.226.24-.409.522-.549.849-.139.32-.226.662-.259 1.028h4.79c0-.373-.05-.719-.149-1.038a2.476 2.476 0 0 0-.42-.85 1.931 1.931 0 0 0-.688-.559 2.052 2.052 0 0 0-.948-.21Zm.848 7.31c.512 0 .978-.04 1.397-.12a8.824 8.824 0 0 0 1.268-.36v1.289c-.34.166-.759.296-1.258.39-.499.1-1.034.149-1.607.149a6.876 6.876 0 0 1-1.916-.26 4.108 4.108 0 0 1-1.557-.838c-.445-.387-.798-.89-1.057-1.508-.253-.626-.38-1.379-.38-2.257 0-.866.12-1.615.36-2.247.239-.64.559-1.172.958-1.598a3.9 3.9 0 0 1 1.377-.959 4.243 4.243 0 0 1 1.627-.32c.532 0 1.027.09 1.487.27.465.173.868.45 1.207.83.339.379.606.868.798 1.467.2.6.3 1.318.3 2.157v.21c0 .053-.003.18-.01.38h-6.507c0 .605.09 1.121.27 1.547.186.42.435.763.748 1.029.319.26.692.45 1.118.57.426.119.885.179 1.377.179ZM181.831 11.41c0 .26.056.473.17.64.113.166.262.303.449.41.193.1.412.182.658.25.253.066.513.126.779.179.366.08.712.17 1.038.27.326.093.625.236.898.429.279.193.502.456.668.789.173.326.26.752.26 1.278 0 .48-.093.906-.28 1.278a2.64 2.64 0 0 1-.788.929 3.66 3.66 0 0 1-1.217.56c-.473.132-.992.199-1.557.199-.346 0-.662-.02-.948-.06a5.2 5.2 0 0 1-.749-.14 4.56 4.56 0 0 1-.549-.17 3.646 3.646 0 0 1-.379-.17v-1.297c.399.16.795.286 1.188.379.392.093.825.14 1.297.14.373 0 .709-.034 1.008-.1.299-.067.555-.163.768-.29.22-.133.386-.296.499-.489.12-.2.18-.43.18-.69 0-.265-.053-.485-.16-.658a1.315 1.315 0 0 0-.429-.43 2.582 2.582 0 0 0-.629-.28 9.795 9.795 0 0 0-.738-.199c-.339-.073-.685-.16-1.038-.26-.346-.1-.662-.24-.948-.419a2.102 2.102 0 0 1-.699-.729c-.173-.313-.259-.712-.259-1.198 0-.5.09-.932.269-1.299.18-.366.423-.669.729-.908.306-.24.662-.42 1.068-.54.406-.12.835-.18 1.287-.18.526 0 1.011.05 1.457.15.453.1.862.227 1.228.38v1.318a8.85 8.85 0 0 0-1.218-.37 5.61 5.61 0 0 0-1.257-.16c-.366 0-.679.037-.938.11a1.78 1.78 0 0 0-.639.31c-.166.127-.289.28-.369.46a1.46 1.46 0 0 0-.11.579ZM189.787 11.41c0 .26.057.473.17.64.113.166.263.303.449.41.193.1.413.182.659.25.253.066.512.126.778.179.366.08.712.17 1.038.27.326.093.626.236.898.429.28.193.503.456.669.789.173.326.259.752.259 1.278 0 .48-.093.906-.279 1.278a2.64 2.64 0 0 1-.788.929c-.34.246-.746.433-1.218.56-.472.132-.991.199-1.557.199-.346 0-.662-.02-.948-.06a5.185 5.185 0 0 1-.748-.14 4.56 4.56 0 0 1-.549-.17 3.552 3.552 0 0 1-.379-.17v-1.297c.399.16.795.286 1.187.379.393.093.825.14 1.298.14.372 0 .708-.034 1.007-.1.3-.067.556-.163.769-.29.219-.133.386-.296.499-.489.12-.2.18-.43.18-.69 0-.265-.054-.485-.16-.658a1.334 1.334 0 0 0-.429-.43 2.6 2.6 0 0 0-.629-.28 9.925 9.925 0 0 0-.738-.199c-.34-.073-.686-.16-1.038-.26-.346-.1-.662-.24-.948-.419a2.102 2.102 0 0 1-.699-.729c-.173-.313-.259-.712-.259-1.198 0-.5.089-.932.269-1.299.18-.366.422-.669.729-.908a3.16 3.16 0 0 1 1.067-.54c.406-.12.835-.18 1.288-.18.525 0 1.011.05 1.457.15.452.1.861.227 1.227.38v1.318a8.837 8.837 0 0 0-1.217-.37 5.612 5.612 0 0 0-1.258-.16 3.49 3.49 0 0 0-.938.11 1.797 1.797 0 0 0-.639.31c-.166.127-.289.28-.369.46-.073.173-.11.366-.11.579Z" fill="#000"/><path d="M13.491 27c7.451 0 13.492-6.044 13.492-13.5 0-7.457-6.04-13.5-13.492-13.5C6.041 0 0 6.044 0 13.5 0 20.957 6.04 27 13.491 27Z" fill="#069E08"/><path d="M14.16 11.233v13.088l6.746-13.088H14.16ZM12.798 15.742V2.679l-6.72 13.063h6.72Z" fill="#fff"/><path d="M34.834 22.414c-.387-.593-.747-1.184-1.107-1.751 1.905-1.16 2.548-2.087 2.548-3.84V6.699h-2.24V4.767H38.8v11.542c0 2.937-.85 4.586-3.965 6.105ZM54.787 15.51c0 .979.695 1.082 1.159 1.082.464 0 1.133-.155 1.647-.31v1.804c-.72.232-1.467.412-2.497.412-1.236 0-2.678-.464-2.678-2.628v-5.308h-1.313v-1.83h1.313V6.029h2.369v2.706h2.986v1.83h-2.986v4.946ZM59.73 23.316V8.708h2.266v.876c.901-.696 1.906-1.134 3.141-1.134 2.137 0 3.837 1.494 3.837 4.715 0 3.195-1.853 5.307-4.918 5.307-.746 0-1.338-.103-1.957-.232v5.05h-2.368v.026Zm4.789-12.908c-.695 0-1.57.335-2.395 1.057v4.973a8.59 8.59 0 0 0 1.777.18c1.673 0 2.626-1.057 2.626-3.272 0-2.036-.695-2.938-2.008-2.938ZM78.294 18.266H76.08V17.21h-.052c-.772.592-1.725 1.236-3.14 1.236-1.237 0-2.576-.902-2.576-2.731 0-2.447 2.086-2.911 3.553-3.117l2.085-.283v-.283c0-1.289-.514-1.7-1.725-1.7-.593 0-1.982.18-3.115.643l-.206-1.906a11.91 11.91 0 0 1 3.63-.618c2.317 0 3.811.928 3.811 3.684v6.131h-.05Zm-2.369-4.457-1.957.31c-.592.076-1.21.437-1.21 1.313 0 .773.438 1.21 1.081 1.21.696 0 1.442-.411 2.085-.875v-1.958h.001ZM88.079 17.957c-.978.335-1.853.541-2.961.541-3.553 0-4.97-2.035-4.97-4.998 0-3.117 1.958-5.05 5.124-5.05 1.184 0 1.905.207 2.703.465v2.01c-.695-.259-1.7-.542-2.677-.542-1.442 0-2.678.773-2.678 2.989 0 2.447 1.236 3.195 2.806 3.195.747 0 1.571-.155 2.678-.593v1.983h-.025ZM92.558 12.83c.205-.231.36-.463 3.346-4.096h3.09l-3.863 4.535 4.222 5.023h-3.089l-3.681-4.535v4.535h-2.368V4.767h2.369v8.064h-.026ZM49.328 17.957c-1.236.387-2.292.541-3.528.541-3.038 0-4.917-1.52-4.917-5.076 0-2.602 1.596-4.972 4.66-4.972 3.038 0 4.094 2.112 4.094 4.122 0 .67-.052 1.03-.077 1.416h-6.128c.052 2.087 1.236 2.577 3.013 2.577.978 0 1.853-.232 2.858-.593v1.983h.025v.002Zm-2.162-5.54c0-1.159-.387-2.164-1.647-2.164-1.184 0-1.906.85-2.06 2.165h3.707Z" fill="#000"/></svg>';
512    }
513
514    // phpcs:disable WordPress.Security.EscapeOutput.UnsafePrintingFunction
515    // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
516
517    /**
518     * Overwrites the backbone template for the attachment details modal
519     *
520     * This template is originally added in WP core in wp-includes/media-templates.php
521     *
522     * We override the initialize method of the TwoColumn view class (located at core's js/media/view/attachment/detail-two-column.js)
523     * and use the custom template only for VideoPress videos.
524     *
525     * @return void
526     */
527    public static function attachment_details_two_column_template() {
528
529        ?>
530        <script type="text/html" id="tmpl-videopress_iframe_vnext">
531            <iframe class="videopress-iframe" style="display: block; max-width: 100%; max-height: 100%;" width="{{ data.width }}" height="{{ data.height }}" src="https://videopress.com/embed/{{ data.guid }}?{{ data.urlargs }}" frameborder='0' allowfullscreen></iframe>
532        </script>
533        <script type="text/html" id="tmpl-attachment-details-two-column-videopress">
534            <div class="attachment-media-view {{ data.orientation }}">
535                <h2 class="screen-reader-text"><?php _e( 'Attachment Preview', 'jetpack-videopress-pkg' ); ?></h2>
536                <div class="thumbnail thumbnail-{{ data.type }}">
537                </div>
538            </div>
539            <div class="attachment-info">
540                <h2>
541                    <?php echo self::get_logo_svg(); ?>
542                </h2>
543                <?php self::attachment_info_template_part(); ?>
544            </div>
545        </script>
546        <script>
547            jQuery(document).ready( function($) {
548                if( typeof wp.media.view.Attachment.Details.TwoColumn != 'undefined' ){
549                    var TwoColumn   = wp.media.view.Attachment.Details.TwoColumn,
550                        old_render  = TwoColumn.prototype.render,
551                        vp_template = wp.template('videopress_iframe_vnext');
552
553                    TwoColumn.prototype.initialize = function() {
554                        if ( 'video' === this.model.attributes.type && 'videopress' === this.model.attributes.subtype ) {
555                            this.template = wp.template( 'attachment-details-two-column-videopress' );
556                            this.listenTo( this.model, 'change:videopress_status', this.render );
557                        } else {
558                            this.template = wp.template( 'attachment-details-two-column' );
559                        }
560                        // From this point on, we are just copying the function from core.
561                        this.controller.on( 'content:activate:edit-details', _.bind( this.editAttachment, this ) );
562                        wp.media.view.Attachment.Details.prototype.initialize.apply( this, arguments );
563                    }
564
565                    // Add the VideoPress player
566                    TwoColumn.prototype.render = function() {
567                        // Have the original renderer run first.
568                        old_render.apply( this, arguments );
569
570                        // Now our stuff!
571                        if ( 'video' === this.model.get('type') ) {
572                            if ( 'failed' === this.model.get('videopress_status') ) {
573                                // The legacy Jetpack renderer can insert a player during old_render.
574                                this.$('.attachment-media-view .thumbnail-video').empty();
575                            } else if ( this.model.get('videopress_guid') ) {
576                                this.$('.attachment-media-view .thumbnail-video').html( vp_template( {
577                                    guid   : this.model.get('videopress_guid'),
578                                    width  : this.model.get('width') > 0 ? this.model.get('width') : '100%',
579                                    height : this.model.get('height') > 0 ? this.model.get('height') : '100%'
580                                }));
581                            }
582                        }
583                    };
584                }
585            });
586        </script>
587        <?php
588    }
589
590    /**
591     * Overwrites the backbone template for the attachment details modal
592     *
593     * This template is originally added in WP core in wp-includes/media-templates.php
594     *
595     * We override the initialize method of the TwoColumn view class (located at core's js/media/view/attachment/detail-two-column.js)
596     * and use the custom template only for VideoPress videos.
597     *
598     * @return void
599     */
600    public static function attachment_details_template() {
601        ?>
602        <style>
603            .attachment-details.videopress-attachment-details ~ .compat-item .compat-field-videopress_conversion_error {
604                display: none;
605            }
606        </style>
607        <script type="text/html" id="tmpl-videopress_iframe_vnext">
608            <iframe class="videopress-iframe" style="display: block; max-width: 100%; max-height: 180px;" width="{{ data.width }}" height="{{ data.height }}" src="https://videopress.com/embed/{{ data.guid }}?{{ data.urlargs }}" frameborder='0' allowfullscreen></iframe>
609        </script>
610        <script type="text/html" id="tmpl-attachment-details-videopress">
611            <h2>
612                <?php echo self::get_logo_svg(); ?>
613            </h2>
614            <div class="attachment-info">
615                <div class="wp-media-wrapper wp-video">
616                </div>
617                <?php self::attachment_info_template_part(); ?>
618            </div>
619        </script>
620        <script>
621            jQuery(document).ready( function($) {
622                if( typeof wp.media.view.Attachment.Details != 'undefined' ){
623                    var DetailsTemplate = wp.media.view.Attachment.Details,
624                        old_render      = DetailsTemplate.prototype.render,
625                        vp_template     = wp.template('videopress_iframe_vnext');
626
627                        DetailsTemplate.prototype.initialize = function() {
628                            if ( 'video' === this.model.attributes.type && 'videopress' === this.model.attributes.subtype ) {
629                                this.template = wp.template( 'attachment-details-videopress' );
630                                this.$el.addClass( 'videopress-attachment-details' );
631                                this.listenTo( this.model, 'change:videopress_status', this.render );
632                            } else {
633                                this.template = wp.template( 'attachment-details' );
634                            }
635                            // From this point on, we are just copying the function from core.
636                            this.options = _.defaults( this.options, {
637                                rerenderOnModelChange: false
638                            });
639
640                            // Call 'initialize' directly on the parent class.
641                            wp.media.view.Attachment.prototype.initialize.apply( this, arguments );
642
643                            this.copyAttachmentDetailsURLClipboard();
644                        }
645
646                        // Add the VideoPress player
647                        DetailsTemplate.prototype.render = function() {
648                            // Have the original renderer run first.
649                            old_render.apply( this, arguments );
650
651                            // Now our stuff!
652                            if ( 'video' === this.model.get('type') ) {
653                                if ( this.model.get('videopress_guid') && 'failed' !== this.model.get('videopress_status') ) {
654                                    this.$('.attachment-info .wp-video').html( vp_template( {
655                                        guid   : this.model.get('videopress_guid'),
656                                        // width  : this.model.get('width') > 0 ? this.model.get('width') : '100%',
657                                        // height : this.model.get('height') > 0 ? this.model.get('height') : '100%'
658                                    }));
659                                }
660                            }
661                        };
662                }
663            });
664        </script>
665        <?php
666    }
667
668    /**
669     * Echoes the piece of the custom template that is shared between the two templates above
670     *
671     * @return void
672     */
673    protected static function attachment_info_template_part() {
674        ?>
675        <# if ( 'failed' === data.videopress_status ) { #>
676            <div class="notice notice-error" role="alert">
677                <p><?php esc_html_e( 'Conversion failed. Delete this video and upload it again.', 'jetpack-videopress-pkg' ); ?></p>
678            </div>
679        <# } #>
680        <span class="setting" data-setting="title">
681            <label for="attachment-details-title" class="name"><?php _e( 'Title', 'jetpack-videopress-pkg' ); ?></label>
682            <input type="text" id="attachment-details-title" value="{{ data.title }}" readonly />
683        </span>
684        <span class="setting" data-setting="filename">
685            <label for="attachment-details-filename" class="name"><?php _e( 'File name', 'jetpack-videopress-pkg' ); ?></label>
686            <input type="text" id="attachment-details-filename" value="{{ data.filename }}" readonly />
687        </span>
688        <span class="setting" data-setting="fileurl">
689            <label for="attachment-details-copy-link" class="name"><?php _e( 'File URL:', 'jetpack-videopress-pkg' ); ?></label>
690            <input type="text" class="attachment-details-copy-link" id="attachment-details-copy-link" value="{{ data.url }}" readonly />
691            <div class="copy-to-clipboard-container">
692                <button type="button" class="button button-small copy-attachment-url" data-clipboard-target="#attachment-details-copy-link"><?php _e( 'Copy URL to clipboard', 'jetpack-videopress-pkg' ); ?></button>
693                <span class="success hidden" aria-hidden="true"><?php _e( 'Copied!', 'jetpack-videopress-pkg' ); ?></span>
694            </div>
695        </span>
696        <p><a href="{{ data.editLink }}" class="button button-medium" target="_blank"><?php _e( 'Edit video details', 'jetpack-videopress-pkg' ); ?></a></p>
697        <?php
698    }
699    // phpcs:enable WordPress.Security.EscapeOutput.UnsafePrintingFunction
700    // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
701
702    /**
703     * Load the wp-build entry file and register its polyfills.
704     *
705     * Only called on `?page=jetpack-videopress` admin requests when the
706     * modernization filter is enabled. Keeps wp-build off every other request.
707     *
708     * @return void
709     */
710    private static function load_wp_build() {
711        $build_index = dirname( __DIR__ ) . '/build/build.php';
712
713        if ( ! file_exists( $build_index ) ) {
714            return;
715        }
716
717        require_once $build_index;
718
719        // wp-build registers standalone modules (e.g. the init module) on
720        // wp_default_scripts, which has already fired by admin_menu. Register them
721        // directly so the init module makes it into the import map.
722        if ( function_exists( 'jetpack_videopress_register_script_modules' ) ) {
723            jetpack_videopress_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Checked with function_exists(); defined in the generated build/modules.php, which Phan excludes.
724        }
725
726        // Defer the chapters-editor route stripping to the page init actions, at a
727        // priority ahead of the `build/routes.php` readers (default 10). Those
728        // actions fire from the page's `admin_enqueue_scripts` callback, one
729        // priority before `Initial_State` mirrors the chapters-editor filter to
730        // the client (priority 11), so both evaluations of the filter happen at
731        // the same request stage and a filter registered after this method
732        // runs (`init`, `admin_init`, …) is honored consistently by both.
733        add_action( 'jetpack-videopress-dashboard_init', array( __CLASS__, 'maybe_strip_chapters_editor_routes' ), 5 );
734        add_action( 'jetpack-videopress-dashboard-wp-admin_init', array( __CLASS__, 'maybe_strip_chapters_editor_routes' ), 5 );
735
736        \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::register(
737            'jetpack-videopress',
738            array_merge(
739                \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::SCRIPT_HANDLES,
740                \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills::MODULE_IDS
741            )
742        );
743    }
744
745    /**
746     * Load wp-build with the screen ID aliased across its generated enqueue check.
747     *
748     * @see WP_Build_Screen_Id::load_with_alias()
749     * @return void
750     */
751    private static function load_wp_build_with_screen_alias() {
752        // Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
753        if ( method_exists( \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
754            \Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id::load_with_alias(
755                array( __CLASS__, 'alias_screen_id_for_wp_build' ),
756                array( __CLASS__, 'restore_screen_id_after_wp_build' ),
757                function () {
758                    self::load_wp_build();
759                }
760            );
761            return;
762        }
763
764        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
765        self::load_wp_build();
766        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
767    }
768
769    /**
770     * Remove the chapters editor routes from the generated route registry when
771     * the chapters editor filter is off.
772     *
773     * The wp-build routes loader (`build/routes.php`) stores each page's route
774     * list in `$GLOBALS['jetpack_videopress_jetpack_videopress_dashboard_routes_data']`
775     * and reads it back on the page init hooks at default priority, so
776     * `load_wp_build()` hooks this on those same actions at priority 5 —
777     * filtering the global just before it is consumed keeps the chapters editor
778     * route modules from ever being registered.
779     *
780     * @since 0.45.0
781     *
782     * @internal Only public so the wp-build page init actions can invoke it.
783     *
784     * @return void
785     */
786    public static function maybe_strip_chapters_editor_routes() {
787        if ( self::is_chapters_editor_enabled() || self::is_trim_cut_enabled() ) {
788            return;
789        }
790
791        $global_name = 'jetpack_videopress_jetpack_videopress_dashboard_routes_data';
792
793        // Guard against a build change renaming the generated global: the
794        // chapters editor route would leak, but nothing fatals.
795        if ( empty( $GLOBALS[ $global_name ] ) || ! is_array( $GLOBALS[ $global_name ] ) ) {
796            return;
797        }
798
799        $GLOBALS[ $global_name ] = self::strip_chapters_editor_routes( $GLOBALS[ $global_name ] );
800    }
801
802    /**
803     * Filter the chapters editor route entries out of a wp-build route registry array.
804     *
805     * Pure helper extracted from `maybe_strip_chapters_editor_routes()` so the
806     * filtering logic is unit-testable without the generated global.
807     *
808     * @since 0.45.0
809     *
810     * @internal For use by `maybe_strip_chapters_editor_routes()` and tests only.
811     *
812     * @param array $routes Route entries as generated by `build/routes.php`, each an array with a `path` key.
813     * @return array Route entries with the chapters editor paths removed.
814     */
815    public static function strip_chapters_editor_routes( $routes ) {
816        return array_values(
817            array_filter(
818                $routes,
819                static function ( $route ) {
820                    return ! isset( $route['path'] ) || ! in_array( $route['path'], self::CHAPTERS_EDITOR_ROUTE_PATHS, true );
821                }
822            )
823        );
824    }
825
826    /**
827     * Alias the current screen ID to satisfy wp-build's auto-generated enqueue check.
828     *
829     * Wp-build's `<page>-wp-admin` enqueue callback enqueues only when the screen ID
830     * matches the wp-build page slug (`jetpack-videopress-dashboard`). Our WP-admin
831     * menu slug stays `jetpack-videopress`, so we mutate the screen object in place
832     * to make the check pass without changing the user-facing URL.
833     *
834     * Hooked only when modernization is on AND we're on the VideoPress admin page,
835     * so this never affects any other request.
836     *
837     * @since 0.51.1 Takes no argument; hooked on `admin_enqueue_scripts`.
838     *
839     * @return void
840     */
841    public static function alias_screen_id_for_wp_build() {
842        $screen = get_current_screen();
843        if ( ! $screen ) {
844            return;
845        }
846
847        self::$wp_build_original_screen_id = $screen->id;
848        $screen->id                        = 'jetpack-videopress-dashboard';
849    }
850
851    /**
852     * Undo alias_screen_id_for_wp_build(), so code after the generated check sees the real screen ID.
853     *
854     * @since 0.51.1
855     *
856     * @return void
857     */
858    public static function restore_screen_id_after_wp_build() {
859        $screen = get_current_screen();
860        if ( ! $screen || null === self::$wp_build_original_screen_id ) {
861            return;
862        }
863
864        $screen->id                        = self::$wp_build_original_screen_id;
865        self::$wp_build_original_screen_id = null;
866    }
867
868    /**
869     * Returns true when the wp-build modernization filter is enabled.
870     *
871     * @return bool
872     */
873    public static function is_modernized() {
874        return (bool) apply_filters( self::MODERNIZATION_FILTER, true );
875    }
876
877    /**
878     * Whether the trim and cut editor and its REST endpoints are available.
879     *
880     * @since 0.53.0
881     * @return bool
882     */
883    public static function is_trim_cut_enabled() {
884        /**
885         * Whether the trim and cut editor is available for this site.
886         *
887         * @since 0.53.0
888         * @param bool $enabled Whether trim and cut is enabled. Default true.
889         */
890        return (bool) apply_filters( 'jetpack_videopress_trim_cut', true );
891    }
892
893    /**
894     * Returns true when the chapters editor feature filter is enabled.
895     *
896     * Note the default is true: the dashboard Editor tab, the
897     * `/video/$id/editor` route, the Details-tab deep link, and the block
898     * editor's "Manage chapters" toolbar button are all available unless a site
899     * explicitly opts out via the filter.
900     *
901     * @since 0.45.0
902     *
903     * @return bool
904     */
905    public static function is_chapters_editor_enabled() {
906        /**
907         * Whether the VideoPress chapters editor UI is available.
908         *
909         * Gates UI only — the chapters REST surface stays registered either way.
910         *
911         * @since 0.45.0
912         *
913         * @param bool $enabled Whether the chapters editor UI is enabled. Default true.
914         */
915        return (bool) apply_filters( self::CHAPTERS_EDITOR_FILTER, true );
916    }
917
918    /**
919     * Returns true when the current request targets the VideoPress admin page.
920     *
921     * Used to scope wp-build loading to the one page that needs it. The
922     * `$_GET['page']` value is populated by wp-admin/admin.php before any of
923     * our hooks fire, so this check is reliable from `init()` onwards.
924     *
925     * @return bool
926     */
927    private static function is_videopress_admin_request() {
928        if ( ! is_admin() || ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
929            return false;
930        }
931
932        return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === self::ADMIN_PAGE_SLUG; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
933    }
934}