@@ -6400,3 +6400,234 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) {
64006400 return apply_filters ( 'image_editor_output_format ' , $ output_format , $ filename , $ mime_type );
64016401}
64026402
6403+ /**
6404+ * Checks whether client-side media processing is enabled.
6405+ *
6406+ * Client-side media processing uses the browser's capabilities to handle
6407+ * tasks like image resizing and compression before uploading to the server.
6408+ *
6409+ * @since 7.0.0
6410+ *
6411+ * @return bool Whether client-side media processing is enabled.
6412+ */
6413+ function wp_is_client_side_media_processing_enabled (): bool {
6414+ // This is due to SharedArrayBuffer requiring a secure context.
6415+ $ host = strtolower ( (string ) strtok ( $ _SERVER ['HTTP_HOST ' ] ?? '' , ': ' ) );
6416+ $ enabled = ( is_ssl () || 'localhost ' === $ host || str_ends_with ( $ host , '.localhost ' ) );
6417+
6418+ /**
6419+ * Filters whether client-side media processing is enabled.
6420+ *
6421+ * @since 7.0.0
6422+ *
6423+ * @param bool $enabled Whether client-side media processing is enabled. Default true if the page is served in a secure context.
6424+ */
6425+ return (bool ) apply_filters ( 'wp_client_side_media_processing_enabled ' , $ enabled );
6426+ }
6427+
6428+ /**
6429+ * Sets a global JS variable to indicate that client-side media processing is enabled.
6430+ *
6431+ * @since 7.0.0
6432+ */
6433+ function wp_set_client_side_media_processing_flag (): void {
6434+ if ( ! wp_is_client_side_media_processing_enabled () ) {
6435+ return ;
6436+ }
6437+
6438+ wp_add_inline_script ( 'wp-block-editor ' , 'window.__clientSideMediaProcessing = true; ' , 'before ' );
6439+
6440+ $ chromium_version = wp_get_chromium_major_version ();
6441+
6442+ if ( null !== $ chromium_version && $ chromium_version >= 137 ) {
6443+ wp_add_inline_script ( 'wp-block-editor ' , 'window.__documentIsolationPolicy = true; ' , 'before ' );
6444+ }
6445+
6446+ /*
6447+ * Register the @wordpress/vips/worker script module as a dynamic dependency
6448+ * of the wp-upload-media classic script. This ensures it is included in the
6449+ * import map so that the dynamic import() in upload-media.js can resolve it.
6450+ */
6451+ wp_scripts ()->add_data (
6452+ 'wp-upload-media ' ,
6453+ 'module_dependencies ' ,
6454+ array ( '@wordpress/vips/worker ' )
6455+ );
6456+ }
6457+
6458+ /**
6459+ * Returns the major Chrome/Chromium version from the current request's User-Agent.
6460+ *
6461+ * Matches all Chromium-based browsers (Chrome, Edge, Opera, Brave).
6462+ *
6463+ * @since 7.0.0
6464+ *
6465+ * @return int|null The major Chrome version, or null if not a Chromium browser.
6466+ */
6467+ function wp_get_chromium_major_version (): ?int {
6468+ if ( empty ( $ _SERVER ['HTTP_USER_AGENT ' ] ) ) {
6469+ return null ;
6470+ }
6471+ if ( preg_match ( '#Chrome/(\d+)# ' , $ _SERVER ['HTTP_USER_AGENT ' ], $ matches ) ) {
6472+ return (int ) $ matches [1 ];
6473+ }
6474+ return null ;
6475+ }
6476+
6477+ /**
6478+ * Enables cross-origin isolation in the block editor.
6479+ *
6480+ * Required for enabling SharedArrayBuffer for WebAssembly-based
6481+ * media processing in the editor. Uses Document-Isolation-Policy
6482+ * on supported browsers (Chromium 137+).
6483+ *
6484+ * Skips setup when a third-party page builder overrides the block
6485+ * editor via a custom `action` query parameter, as DIP would block
6486+ * same-origin iframe access that these editors rely on.
6487+ *
6488+ * @since 7.0.0
6489+ */
6490+ function wp_set_up_cross_origin_isolation (): void {
6491+ if ( ! wp_is_client_side_media_processing_enabled () ) {
6492+ return ;
6493+ }
6494+
6495+ $ screen = get_current_screen ();
6496+
6497+ if ( ! $ screen ) {
6498+ return ;
6499+ }
6500+
6501+ if ( ! $ screen ->is_block_editor () && 'site-editor ' !== $ screen ->id && ! ( 'widgets ' === $ screen ->id && wp_use_widgets_block_editor () ) ) {
6502+ return ;
6503+ }
6504+
6505+ /*
6506+ * Skip when a third-party page builder overrides the block editor.
6507+ * DIP isolates the document into its own agent cluster,
6508+ * which blocks same-origin iframe access that these editors rely on.
6509+ */
6510+ if ( isset ( $ _GET ['action ' ] ) && 'edit ' !== $ _GET ['action ' ] ) {
6511+ return ;
6512+ }
6513+
6514+ // Cross-origin isolation is not needed if users can't upload files anyway.
6515+ if ( ! current_user_can ( 'upload_files ' ) ) {
6516+ return ;
6517+ }
6518+
6519+ wp_start_cross_origin_isolation_output_buffer ();
6520+ }
6521+
6522+ /**
6523+ * Sends the Document-Isolation-Policy header for cross-origin isolation.
6524+ *
6525+ * Uses an output buffer to add crossorigin="anonymous" where needed.
6526+ *
6527+ * @since 7.0.0
6528+ */
6529+ function wp_start_cross_origin_isolation_output_buffer (): void {
6530+ $ chromium_version = wp_get_chromium_major_version ();
6531+
6532+ if ( null === $ chromium_version || $ chromium_version < 137 ) {
6533+ return ;
6534+ }
6535+
6536+ ob_start (
6537+ static function ( string $ output ): string {
6538+ header ( 'Document-Isolation-Policy: isolate-and-credentialless ' );
6539+
6540+ return wp_add_crossorigin_attributes ( $ output );
6541+ }
6542+ );
6543+ }
6544+
6545+ /**
6546+ * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
6547+ *
6548+ * @since 7.0.0
6549+ *
6550+ * @param string $html HTML input.
6551+ * @return string Modified HTML.
6552+ */
6553+ function wp_add_crossorigin_attributes ( string $ html ): string {
6554+ $ site_url = site_url ();
6555+
6556+ $ processor = new WP_HTML_Tag_Processor ( $ html );
6557+
6558+ // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
6559+ $ cross_origin_tag_attributes = array (
6560+ 'AUDIO ' => array ( 'src ' => false ),
6561+ 'LINK ' => array ( 'href ' => false ),
6562+ 'SCRIPT ' => array ( 'src ' => false ),
6563+ 'VIDEO ' => array (
6564+ 'src ' => false ,
6565+ 'poster ' => false ,
6566+ ),
6567+ 'SOURCE ' => array ( 'src ' => false ),
6568+ );
6569+
6570+ while ( $ processor ->next_tag () ) {
6571+ $ tag = $ processor ->get_tag ();
6572+
6573+ if ( ! isset ( $ cross_origin_tag_attributes [ $ tag ] ) ) {
6574+ continue ;
6575+ }
6576+
6577+ if ( 'AUDIO ' === $ tag || 'VIDEO ' === $ tag ) {
6578+ $ processor ->set_bookmark ( 'audio-video-parent ' );
6579+ }
6580+
6581+ $ processor ->set_bookmark ( 'resume ' );
6582+
6583+ $ sought = false ;
6584+
6585+ $ crossorigin = $ processor ->get_attribute ( 'crossorigin ' );
6586+
6587+ $ is_cross_origin = false ;
6588+
6589+ foreach ( $ cross_origin_tag_attributes [ $ tag ] as $ attr => $ is_srcset ) {
6590+ if ( $ is_srcset ) {
6591+ $ srcset = $ processor ->get_attribute ( $ attr );
6592+ if ( is_string ( $ srcset ) ) {
6593+ foreach ( explode ( ', ' , $ srcset ) as $ candidate ) {
6594+ $ candidate_url = strtok ( trim ( $ candidate ), ' ' );
6595+ if ( is_string ( $ candidate_url ) && '' !== $ candidate_url && ! str_starts_with ( $ candidate_url , $ site_url ) && ! str_starts_with ( $ candidate_url , '/ ' ) ) {
6596+ $ is_cross_origin = true ;
6597+ break ;
6598+ }
6599+ }
6600+ }
6601+ } else {
6602+ $ url = $ processor ->get_attribute ( $ attr );
6603+ if ( is_string ( $ url ) && ! str_starts_with ( $ url , $ site_url ) && ! str_starts_with ( $ url , '/ ' ) ) {
6604+ $ is_cross_origin = true ;
6605+ }
6606+ }
6607+
6608+ if ( $ is_cross_origin ) {
6609+ break ;
6610+ }
6611+ }
6612+
6613+ if ( $ is_cross_origin && ! is_string ( $ crossorigin ) ) {
6614+ if ( 'SOURCE ' === $ tag ) {
6615+ $ sought = $ processor ->seek ( 'audio-video-parent ' );
6616+
6617+ if ( $ sought ) {
6618+ $ processor ->set_attribute ( 'crossorigin ' , 'anonymous ' );
6619+ }
6620+ } else {
6621+ $ processor ->set_attribute ( 'crossorigin ' , 'anonymous ' );
6622+ }
6623+
6624+ if ( $ sought ) {
6625+ $ processor ->seek ( 'resume ' );
6626+ $ processor ->release_bookmark ( 'audio-video-parent ' );
6627+ }
6628+ }
6629+ }
6630+
6631+ return $ processor ->get_updated_html ();
6632+ }
6633+
0 commit comments