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