Make WordPress Core

Changeset 61703


Ignore:
Timestamp:
02/20/2026 11:49:50 AM (5 weeks ago)
Author:
adamsilverstein
Message:

Editor: backport client side media PHP changes to core.

Bring over the changes required to implement client side media in core. This feature recently graduated from experiments and is ready for testing in beta.

Props adamsilverstein, westonruter, mamaduka, mukesh27, swissspidy, andrewserong, ellatrix, ramonjd.
Fixes #62243.

Location:
trunk
Files:
13 edited

Legend:

Unmodified
Added
Removed
  • trunk/package.json

    r61699 r61703  
    88    },
    99    "gutenberg": {
    10         "ref": "7a11a53377a95cba4d3786d71cadd4c2f0c5ac52"
     10        "ref": "b441348bb7e05af351c250b74283f253acaf9138"
    1111    },
    1212    "engines": {
  • trunk/src/wp-admin/edit-form-blocks.php

    r61568 r61703  
    9393            'gmt_offset',
    9494            'home',
     95            'image_sizes',
     96            'image_size_threshold',
     97            'image_output_formats',
     98            'jpeg_interlaced',
     99            'png_interlaced',
     100            'gif_interlaced',
    95101            'name',
    96102            'site_icon',
  • trunk/src/wp-admin/site-editor.php

    r61568 r61703  
    219219            'gmt_offset',
    220220            'home',
     221            'image_sizes',
     222            'image_size_threshold',
     223            'image_output_formats',
     224            'jpeg_interlaced',
     225            'png_interlaced',
     226            'gif_interlaced',
    221227            'name',
    222228            'site_icon',
  • trunk/src/wp-includes/default-filters.php

    r61689 r61703  
    676676add_filter( 'plupload_default_settings', 'wp_show_heic_upload_error' );
    677677
     678// Client-side media processing.
     679add_action( 'admin_init', 'wp_set_client_side_media_processing_flag' );
     680// Cross-origin isolation for client-side media processing.
     681add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' );
     682add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' );
     683add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' );
     684add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' );
    678685// Nav menu.
    679686add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 );
  • trunk/src/wp-includes/media-template.php

    r61651 r61703  
    157157    $class = 'media-modal wp-core-ui';
    158158
     159    $is_cross_origin_isolation_enabled = wp_is_client_side_media_processing_enabled();
     160
     161    if ( $is_cross_origin_isolation_enabled ) {
     162        ob_start();
     163    }
     164
    159165    $alt_text_description = sprintf(
    160166        /* translators: 1: Link to tutorial, 2: Additional link attributes, 3: Accessibility text. */
     
    15831589     */
    15841590    do_action( 'print_media_templates' );
     1591
     1592    if ( $is_cross_origin_isolation_enabled ) {
     1593        $html = (string) ob_get_clean();
     1594
     1595        /*
     1596         * The media templates are inside <script type="text/html"> tags,
     1597         * whose content is treated as raw text by the HTML Tag Processor.
     1598         * Extract each script block's content, process it separately,
     1599         * then reassemble the full output.
     1600         */
     1601        $script_processor = new WP_HTML_Tag_Processor( $html );
     1602        while ( $script_processor->next_tag( 'SCRIPT' ) ) {
     1603            if ( 'text/html' !== $script_processor->get_attribute( 'type' ) ) {
     1604                continue;
     1605            }
     1606            /*
     1607             * Unlike wp_add_crossorigin_attributes(), this does not check whether
     1608             * URLs are actually cross-origin. Media templates use Underscore.js
     1609             * template expressions (e.g. {{ data.url }}) as placeholder URLs,
     1610             * so actual URLs are not available at parse time.
     1611             * The crossorigin attribute is added unconditionally to all relevant
     1612             * media tags to ensure cross-origin isolation works regardless of
     1613             * the final URL value at render time.
     1614             */
     1615            $template_processor = new WP_HTML_Tag_Processor( $script_processor->get_modifiable_text() );
     1616            while ( $template_processor->next_tag() ) {
     1617                if (
     1618                    in_array( $template_processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true )
     1619                    && ! is_string( $template_processor->get_attribute( 'crossorigin' ) )
     1620                ) {
     1621                    $template_processor->set_attribute( 'crossorigin', 'anonymous' );
     1622                }
     1623            }
     1624            $script_processor->set_modifiable_text( $template_processor->get_updated_html() );
     1625        }
     1626
     1627        echo $script_processor->get_updated_html();
     1628    }
    15851629}
  • trunk/src/wp-includes/media.php

    r61699 r61703  
    63606360    return apply_filters( 'image_editor_output_format', $output_format, $filename, $mime_type );
    63616361}
     6362
     6363/**
     6364 * Checks whether client-side media processing is enabled.
     6365 *
     6366 * Client-side media processing uses the browser's capabilities to handle
     6367 * tasks like image resizing and compression before uploading to the server.
     6368 *
     6369 * @since 7.0.0
     6370 *
     6371 * @return bool Whether client-side media processing is enabled.
     6372 */
     6373function wp_is_client_side_media_processing_enabled(): bool {
     6374    /**
     6375     * Filters whether client-side media processing is enabled.
     6376     *
     6377     * @since 7.0.0
     6378     *
     6379     * @param bool $enabled Whether client-side media processing is enabled. Default true.
     6380     */
     6381    return (bool) apply_filters( 'wp_client_side_media_processing_enabled', true );
     6382}
     6383
     6384/**
     6385 * Sets a global JS variable to indicate that client-side media processing is enabled.
     6386 *
     6387 * @since 7.0.0
     6388 */
     6389function wp_set_client_side_media_processing_flag(): void {
     6390    if ( ! wp_is_client_side_media_processing_enabled() ) {
     6391        return;
     6392    }
     6393
     6394    wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' );
     6395
     6396    /*
     6397     * Register the @wordpress/vips/worker script module as a dynamic dependency
     6398     * of the wp-upload-media classic script. This ensures it is included in the
     6399     * import map so that the dynamic import() in upload-media.js can resolve it.
     6400     */
     6401    wp_scripts()->add_data(
     6402        'wp-upload-media',
     6403        'module_dependencies',
     6404        array( '@wordpress/vips/worker' )
     6405    );
     6406}
     6407
     6408/**
     6409 * Enables cross-origin isolation in the block editor.
     6410 *
     6411 * Required for enabling SharedArrayBuffer for WebAssembly-based
     6412 * media processing in the editor.
     6413 *
     6414 * @since 7.0.0
     6415 *
     6416 * @link https://web.dev/coop-coep/
     6417 */
     6418function wp_set_up_cross_origin_isolation(): void {
     6419    if ( ! wp_is_client_side_media_processing_enabled() ) {
     6420        return;
     6421    }
     6422
     6423    $screen = get_current_screen();
     6424
     6425    if ( ! $screen ) {
     6426        return;
     6427    }
     6428
     6429    if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) {
     6430        return;
     6431    }
     6432
     6433    // Cross-origin isolation is not needed if users can't upload files anyway.
     6434    if ( ! current_user_can( 'upload_files' ) ) {
     6435        return;
     6436    }
     6437
     6438    wp_start_cross_origin_isolation_output_buffer();
     6439}
     6440
     6441/**
     6442 * Starts an output buffer to send cross-origin isolation headers.
     6443 *
     6444 * Sends headers and uses an output buffer to add crossorigin="anonymous"
     6445 * attributes where needed.
     6446 *
     6447 * @since 7.0.0
     6448 *
     6449 * @link https://web.dev/coop-coep/
     6450 *
     6451 * @global bool $is_safari
     6452 */
     6453function wp_start_cross_origin_isolation_output_buffer(): void {
     6454    global $is_safari;
     6455
     6456    $coep = $is_safari ? 'require-corp' : 'credentialless';
     6457
     6458    ob_start(
     6459        static function ( string $output ) use ( $coep ): string {
     6460            header( 'Cross-Origin-Opener-Policy: same-origin' );
     6461            header( "Cross-Origin-Embedder-Policy: $coep" );
     6462
     6463            return wp_add_crossorigin_attributes( $output );
     6464        }
     6465    );
     6466}
     6467
     6468/**
     6469 * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
     6470 *
     6471 * @since 7.0.0
     6472 *
     6473 * @param string $html HTML input.
     6474 * @return string Modified HTML.
     6475 */
     6476function wp_add_crossorigin_attributes( string $html ): string {
     6477    $site_url = site_url();
     6478
     6479    $processor = new WP_HTML_Tag_Processor( $html );
     6480
     6481    // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
     6482    $cross_origin_tag_attributes = array(
     6483        'AUDIO'  => array( 'src' => false ),
     6484        'IMG'    => array(
     6485            'src'    => false,
     6486            'srcset' => true,
     6487        ),
     6488        'LINK'   => array(
     6489            'href'        => false,
     6490            'imagesrcset' => true,
     6491        ),
     6492        'SCRIPT' => array( 'src' => false ),
     6493        'VIDEO'  => array(
     6494            'src'    => false,
     6495            'poster' => false,
     6496        ),
     6497        'SOURCE' => array( 'src' => false ),
     6498    );
     6499
     6500    while ( $processor->next_tag() ) {
     6501        $tag = $processor->get_tag();
     6502
     6503        if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {
     6504            continue;
     6505        }
     6506
     6507        if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
     6508            $processor->set_bookmark( 'audio-video-parent' );
     6509        }
     6510
     6511        $processor->set_bookmark( 'resume' );
     6512
     6513        $sought = false;
     6514
     6515        $crossorigin = $processor->get_attribute( 'crossorigin' );
     6516
     6517        $is_cross_origin = false;
     6518
     6519        foreach ( $cross_origin_tag_attributes[ $tag ] as $attr => $is_srcset ) {
     6520            if ( $is_srcset ) {
     6521                $srcset = $processor->get_attribute( $attr );
     6522                if ( is_string( $srcset ) ) {
     6523                    foreach ( explode( ',', $srcset ) as $candidate ) {
     6524                        $candidate_url = strtok( trim( $candidate ), ' ' );
     6525                        if ( is_string( $candidate_url ) && '' !== $candidate_url && ! str_starts_with( $candidate_url, $site_url ) && ! str_starts_with( $candidate_url, '/' ) ) {
     6526                            $is_cross_origin = true;
     6527                            break;
     6528                        }
     6529                    }
     6530                }
     6531            } else {
     6532                $url = $processor->get_attribute( $attr );
     6533                if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) ) {
     6534                    $is_cross_origin = true;
     6535                }
     6536            }
     6537
     6538            if ( $is_cross_origin ) {
     6539                break;
     6540            }
     6541        }
     6542
     6543        if ( $is_cross_origin && ! is_string( $crossorigin ) ) {
     6544            if ( 'SOURCE' === $tag ) {
     6545                $sought = $processor->seek( 'audio-video-parent' );
     6546
     6547                if ( $sought ) {
     6548                    $processor->set_attribute( 'crossorigin', 'anonymous' );
     6549                }
     6550            } else {
     6551                $processor->set_attribute( 'crossorigin', 'anonymous' );
     6552            }
     6553
     6554            if ( $sought ) {
     6555                $processor->seek( 'resume' );
     6556                $processor->release_bookmark( 'audio-video-parent' );
     6557            }
     6558        }
     6559    }
     6560
     6561    return $processor->get_updated_html();
     6562}
     6563
  • trunk/src/wp-includes/rest-api/class-wp-rest-server.php

    r61463 r61703  
    13691369        );
    13701370
     1371        // Add media processing settings for users who can upload files.
     1372        if ( wp_is_client_side_media_processing_enabled() && current_user_can( 'upload_files' ) ) {
     1373            // Image sizes keyed by name for client-side media processing.
     1374            $available['image_sizes'] = array();
     1375            foreach ( wp_get_registered_image_subsizes() as $name => $size ) {
     1376                $available['image_sizes'][ $name ] = $size;
     1377            }
     1378
     1379            /** This filter is documented in wp-admin/includes/image.php */
     1380            $available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 );
     1381
     1382            // Image output formats.
     1383            $input_formats  = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic' );
     1384            $output_formats = array();
     1385            foreach ( $input_formats as $mime_type ) {
     1386                /** This filter is documented in wp-includes/class-wp-image-editor.php */
     1387                $output_formats = apply_filters( 'image_editor_output_format', $output_formats, '', $mime_type );
     1388            }
     1389            $available['image_output_formats'] = (object) $output_formats;
     1390
     1391            /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
     1392            $available['jpeg_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' );
     1393            /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
     1394            $available['png_interlaced']  = (bool) apply_filters( 'image_save_progressive', false, 'image/png' );
     1395            /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
     1396            $available['gif_interlaced']  = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' );
     1397        }
     1398
    13711399        $response = new WP_REST_Response( $available );
    13721400
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

    r61429 r61703  
    6464            )
    6565        );
     66
     67        if ( wp_is_client_side_media_processing_enabled() ) {
     68            $valid_image_sizes = array_keys( wp_get_registered_image_subsizes() );
     69            // Special case to set 'original_image' in attachment metadata.
     70            $valid_image_sizes[] = 'original';
     71            // Used for PDF thumbnails.
     72            $valid_image_sizes[] = 'full';
     73
     74            register_rest_route(
     75                $this->namespace,
     76                '/' . $this->rest_base . '/(?P<id>[\d]+)/sideload',
     77                array(
     78                    array(
     79                        'methods'             => WP_REST_Server::CREATABLE,
     80                        'callback'            => array( $this, 'sideload_item' ),
     81                        'permission_callback' => array( $this, 'sideload_item_permissions_check' ),
     82                        'args'                => array(
     83                            'id'             => array(
     84                                'description' => __( 'Unique identifier for the attachment.' ),
     85                                'type'        => 'integer',
     86                            ),
     87                            'image_size'     => array(
     88                                'description' => __( 'Image size.' ),
     89                                'type'        => 'string',
     90                                'enum'        => $valid_image_sizes,
     91                                'required'    => true,
     92                            ),
     93                            'convert_format' => array(
     94                                'type'        => 'boolean',
     95                                'default'     => true,
     96                                'description' => __( 'Whether to convert image formats.' ),
     97                            ),
     98                        ),
     99                    ),
     100                    'allow_batch' => $this->allow_batch,
     101                    'schema'      => array( $this, 'get_public_item_schema' ),
     102                )
     103            );
     104        }
     105    }
     106
     107    /**
     108     * Retrieves the query params for the attachments collection.
     109     *
     110     * @since 7.0.0
     111     *
     112     * @param string $method Optional. HTTP method of the request.
     113     *                       The arguments for `CREATABLE` requests are
     114     *                       checked for required values and may fall-back to a given default.
     115     *                       Default WP_REST_Server::CREATABLE.
     116     * @return array<string, array<string, mixed>> Endpoint arguments.
     117     */
     118    public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
     119        $args = parent::get_endpoint_args_for_item_schema( $method );
     120
     121        if ( WP_REST_Server::CREATABLE === $method && wp_is_client_side_media_processing_enabled() ) {
     122            $args['generate_sub_sizes'] = array(
     123                'type'        => 'boolean',
     124                'default'     => true,
     125                'description' => __( 'Whether to generate image sub sizes.' ),
     126            );
     127            $args['convert_format']     = array(
     128                'type'        => 'boolean',
     129                'default'     => true,
     130                'description' => __( 'Whether to convert image formats.' ),
     131            );
     132        }
     133
     134        return $args;
    66135    }
    67136
     
    193262     *
    194263     * @since 4.7.0
     264     * @since 7.0.0 Added `generate_sub_sizes` and `convert_format` parameters.
    195265     *
    196266     * @param WP_REST_Request $request Full details about the request.
     
    206276        }
    207277
     278        // Handle generate_sub_sizes parameter.
     279        if ( isset( $request['generate_sub_sizes'] ) && ! $request['generate_sub_sizes'] ) {
     280            add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
     281            add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
     282            // Disable server-side EXIF rotation so the client can handle it.
     283            // This preserves the original orientation value in the metadata.
     284            add_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );
     285        }
     286
     287        // Handle convert_format parameter.
     288        if ( isset( $request['convert_format'] ) && ! $request['convert_format'] ) {
     289            add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
     290        }
     291
    208292        $insert = $this->insert_attachment( $request );
    209293
    210294        if ( is_wp_error( $insert ) ) {
     295            $this->remove_client_side_media_processing_filters();
    211296            return $insert;
    212297        }
     
    226311
    227312            if ( is_wp_error( $thumbnail_update ) ) {
     313                $this->remove_client_side_media_processing_filters();
    228314                return $thumbnail_update;
    229315            }
     
    234320
    235321            if ( is_wp_error( $meta_update ) ) {
     322                $this->remove_client_side_media_processing_filters();
    236323                return $meta_update;
    237324            }
     
    242329
    243330        if ( is_wp_error( $fields_update ) ) {
     331            $this->remove_client_side_media_processing_filters();
    244332            return $fields_update;
    245333        }
     
    248336
    249337        if ( is_wp_error( $terms_update ) ) {
     338            $this->remove_client_side_media_processing_filters();
    250339            return $terms_update;
    251340        }
     
    284373        wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
    285374
     375        $this->remove_client_side_media_processing_filters();
     376
    286377        $response = $this->prepare_item_for_response( $attachment, $request );
    287378        $response = rest_ensure_response( $response );
     
    290381
    291382        return $response;
     383    }
     384
     385    /**
     386     * Removes filters added for client-side media processing.
     387     *
     388     * @since 7.0.0
     389     */
     390    private function remove_client_side_media_processing_filters() {
     391        remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
     392        remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
     393        remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );
     394        remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );
    292395    }
    293396
     
    9891092            require_once ABSPATH . 'wp-admin/includes/image.php';
    9901093            $data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) );
     1094
     1095            // Handle PDFs which don't use wp_get_missing_image_subsizes().
     1096            if ( empty( $data['missing_image_sizes'] ) && 'application/pdf' === get_post_mime_type( $post ) ) {
     1097                $metadata = wp_get_attachment_metadata( $post->ID, true );
     1098
     1099                if ( ! is_array( $metadata ) ) {
     1100                    $metadata = array();
     1101                }
     1102
     1103                $metadata['sizes'] = $metadata['sizes'] ?? array();
     1104
     1105                $fallback_sizes = array(
     1106                    'thumbnail',
     1107                    'medium',
     1108                    'large',
     1109                );
     1110
     1111                // The filter might have been added by ::create_item().
     1112                remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
     1113
     1114                /** This filter is documented in wp-admin/includes/image.php */
     1115                $fallback_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata );
     1116
     1117                $registered_sizes = wp_get_registered_image_subsizes();
     1118                $merged_sizes     = array_keys( array_intersect_key( $registered_sizes, array_flip( $fallback_sizes ) ) );
     1119
     1120                $data['missing_image_sizes'] = array_values( array_diff( $merged_sizes, array_keys( $metadata['sizes'] ) ) );
     1121            }
     1122        }
     1123
     1124        if ( in_array( 'filename', $fields, true ) ) {
     1125            $data['filename'] = $this->get_attachment_filename( $post->ID );
     1126        }
     1127
     1128        if ( in_array( 'filesize', $fields, true ) ) {
     1129            $data['filesize'] = $this->get_attachment_filesize( $post->ID );
     1130        }
     1131
     1132        if ( in_array( 'exif_orientation', $fields, true ) && wp_attachment_is_image( $post ) ) {
     1133            $metadata = wp_get_attachment_metadata( $post->ID, true );
     1134
     1135            // Default to 1 (no rotation needed) if orientation not set.
     1136            $orientation = 1;
     1137
     1138            if (
     1139                is_array( $metadata ) &&
     1140                isset( $metadata['image_meta']['orientation'] ) &&
     1141                (int) $metadata['image_meta']['orientation'] > 0
     1142            ) {
     1143                $orientation = (int) $metadata['image_meta']['orientation'];
     1144            }
     1145
     1146            $data['exif_orientation'] = $orientation;
    9911147        }
    9921148
     
    11561312            'type'        => 'array',
    11571313            'items'       => array( 'type' => 'string' ),
     1314            'context'     => array( 'edit' ),
     1315            'readonly'    => true,
     1316        );
     1317
     1318        $schema['properties']['filename'] = array(
     1319            'description' => __( 'Original attachment file name.' ),
     1320            'type'        => 'string',
     1321            'context'     => array( 'view', 'edit' ),
     1322            'readonly'    => true,
     1323        );
     1324
     1325        $schema['properties']['filesize'] = array(
     1326            'description' => __( 'Attachment file size in bytes.' ),
     1327            'type'        => 'integer',
     1328            'context'     => array( 'view', 'edit' ),
     1329            'readonly'    => true,
     1330        );
     1331
     1332        $schema['properties']['exif_orientation'] = array(
     1333            'description' => __( 'EXIF orientation value. Values 1-8 follow the EXIF specification, where 1 means no rotation needed.' ),
     1334            'type'        => 'integer',
    11581335            'context'     => array( 'edit' ),
    11591336            'readonly'    => true,
     
    17251902        return $args;
    17261903    }
     1904
     1905    /**
     1906     * Gets the attachment's original file name.
     1907     *
     1908     * @since 7.0.0
     1909     *
     1910     * @param int $attachment_id Attachment ID.
     1911     * @return string|null Attachment file name, or null if not found.
     1912     */
     1913    protected function get_attachment_filename( int $attachment_id ): ?string {
     1914        $path = wp_get_original_image_path( $attachment_id );
     1915
     1916        if ( $path ) {
     1917            return wp_basename( $path );
     1918        }
     1919
     1920        $path = get_attached_file( $attachment_id );
     1921
     1922        if ( $path ) {
     1923            return wp_basename( $path );
     1924        }
     1925
     1926        return null;
     1927    }
     1928
     1929    /**
     1930     * Gets the attachment's file size in bytes.
     1931     *
     1932     * @since 7.0.0
     1933     *
     1934     * @param int $attachment_id Attachment ID.
     1935     * @return int|null Attachment file size in bytes, or null if not available.
     1936     */
     1937    protected function get_attachment_filesize( int $attachment_id ): ?int {
     1938        $meta = wp_get_attachment_metadata( $attachment_id );
     1939
     1940        if ( isset( $meta['filesize'] ) ) {
     1941            return $meta['filesize'];
     1942        }
     1943
     1944        $original_path = wp_get_original_image_path( $attachment_id );
     1945        $attached_file = $original_path ? $original_path : get_attached_file( $attachment_id );
     1946
     1947        if ( is_string( $attached_file ) && is_readable( $attached_file ) ) {
     1948            return wp_filesize( $attached_file );
     1949        }
     1950
     1951        return null;
     1952    }
     1953
     1954    /**
     1955     * Checks if a given request has access to sideload a file.
     1956     *
     1957     * Sideloading a file for an existing attachment
     1958     * requires both update and create permissions.
     1959     *
     1960     * @since 7.0.0
     1961     *
     1962     * @param WP_REST_Request $request Full details about the request.
     1963     * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
     1964     */
     1965    public function sideload_item_permissions_check( $request ) {
     1966        return $this->edit_media_item_permissions_check( $request );
     1967    }
     1968
     1969    /**
     1970     * Side-loads a media file without creating a new attachment.
     1971     *
     1972     * @since 7.0.0
     1973     *
     1974     * @param WP_REST_Request $request Full details about the request.
     1975     * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
     1976     */
     1977    public function sideload_item( WP_REST_Request $request ) {
     1978        $attachment_id = $request['id'];
     1979
     1980        $post = $this->get_post( $attachment_id );
     1981
     1982        if ( is_wp_error( $post ) ) {
     1983            return $post;
     1984        }
     1985
     1986        if (
     1987            ! wp_attachment_is_image( $post ) &&
     1988            ! wp_attachment_is( 'pdf', $post )
     1989        ) {
     1990            return new WP_Error(
     1991                'rest_post_invalid_id',
     1992                __( 'Invalid post ID. Only images and PDFs can be sideloaded.' ),
     1993                array( 'status' => 400 )
     1994            );
     1995        }
     1996
     1997        if ( isset( $request['convert_format'] ) && ! $request['convert_format'] ) {
     1998            // Prevent image conversion as that is done client-side.
     1999            add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
     2000        }
     2001
     2002        // Get the file via $_FILES or raw data.
     2003        $files   = $request->get_file_params();
     2004        $headers = $request->get_headers();
     2005
     2006        /*
     2007         * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts.
     2008         * See /wp-includes/functions.php.
     2009         * With the following filter we can work around this safeguard.
     2010         */
     2011        $attachment_filename = get_attached_file( $attachment_id, true );
     2012        $attachment_filename = $attachment_filename ? wp_basename( $attachment_filename ) : null;
     2013
     2014        $filter_filename = static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) {
     2015            return self::filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename );
     2016        };
     2017
     2018        add_filter( 'wp_unique_filename', $filter_filename, 10, 6 );
     2019
     2020        $parent_post = get_post_parent( $attachment_id );
     2021
     2022        $time = null;
     2023
     2024        // Matches logic in media_handle_upload().
     2025        // The post date doesn't usually matter for pages, so don't backdate this upload.
     2026        if ( $parent_post && 'page' !== $parent_post->post_type && ! str_starts_with( $parent_post->post_date, '0000-00-00' ) ) {
     2027            $time = $parent_post->post_date;
     2028        }
     2029
     2030        if ( ! empty( $files ) ) {
     2031            $file = $this->upload_from_file( $files, $headers, $time );
     2032        } else {
     2033            $file = $this->upload_from_data( $request->get_body(), $headers, $time );
     2034        }
     2035
     2036        remove_filter( 'wp_unique_filename', $filter_filename );
     2037        remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );
     2038
     2039        if ( is_wp_error( $file ) ) {
     2040            return $file;
     2041        }
     2042
     2043        $type = $file['type'];
     2044        $path = $file['file'];
     2045
     2046        $image_size = $request['image_size'];
     2047
     2048        $metadata = wp_get_attachment_metadata( $attachment_id, true );
     2049
     2050        if ( ! $metadata ) {
     2051            $metadata = array();
     2052        }
     2053
     2054        if ( 'original' === $image_size ) {
     2055            $metadata['original_image'] = wp_basename( $path );
     2056        } else {
     2057            $metadata['sizes'] = $metadata['sizes'] ?? array();
     2058
     2059            $size = wp_getimagesize( $path );
     2060
     2061            $metadata['sizes'][ $image_size ] = array(
     2062                'width'     => $size ? $size[0] : 0,
     2063                'height'    => $size ? $size[1] : 0,
     2064                'file'      => wp_basename( $path ),
     2065                'mime-type' => $type,
     2066                'filesize'  => wp_filesize( $path ),
     2067            );
     2068        }
     2069
     2070        wp_update_attachment_metadata( $attachment_id, $metadata );
     2071
     2072        $response_request = new WP_REST_Request(
     2073            WP_REST_Server::READABLE,
     2074            rest_get_route_for_post( $attachment_id )
     2075        );
     2076
     2077        $response_request['context'] = 'edit';
     2078
     2079        if ( isset( $request['_fields'] ) ) {
     2080            $response_request['_fields'] = $request['_fields'];
     2081        }
     2082
     2083        $response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );
     2084
     2085        $response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );
     2086
     2087        return $response;
     2088    }
     2089
     2090    /**
     2091     * Filters wp_unique_filename during sideloads.
     2092     *
     2093     * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts.
     2094     * Adding this closure to the filter helps work around this safeguard.
     2095     *
     2096     * Example: when uploading myphoto.jpeg, WordPress normally creates myphoto-150x150.jpeg,
     2097     * and when uploading myphoto-150x150.jpeg, it will be renamed to myphoto-150x150-1.jpeg
     2098     * However, here it is desired not to add the suffix in order to maintain the same
     2099     * naming convention as if the file was uploaded regularly.
     2100     *
     2101     * @since 7.0.0
     2102     *
     2103     * @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582
     2104     *
     2105     * @param string      $filename            Unique file name.
     2106     * @param string      $dir                 Directory path.
     2107     * @param int|string  $number              The highest number that was used to make the file name unique
     2108     *                                         or an empty string if unused.
     2109     * @param string|null $attachment_filename Original attachment file name.
     2110     * @return string Filtered file name.
     2111     */
     2112    private static function filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename ) {
     2113        if ( empty( $number ) || ! $attachment_filename ) {
     2114            return $filename;
     2115        }
     2116
     2117        $ext       = pathinfo( $filename, PATHINFO_EXTENSION );
     2118        $name      = pathinfo( $filename, PATHINFO_FILENAME );
     2119        $orig_name = pathinfo( $attachment_filename, PATHINFO_FILENAME );
     2120
     2121        if ( ! $ext || ! $name ) {
     2122            return $filename;
     2123        }
     2124
     2125        $matches = array();
     2126        if ( preg_match( '/(.*)(-\d+x\d+)-' . $number . '$/', $name, $matches ) ) {
     2127            $filename_without_suffix = $matches[1] . $matches[2] . ".$ext";
     2128            if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) {
     2129                return $filename_without_suffix;
     2130            }
     2131        }
     2132
     2133        return $filename;
     2134    }
    17272135}
  • trunk/src/wp-includes/script-loader.php

    r61686 r61703  
    310310
    311311        $scripts->add( $handle, $path, $dependencies, $package_data['version'], 1 );
     312
     313        if ( ! empty( $package_data['module_dependencies'] ) ) {
     314            $scripts->add_data( $handle, 'module_dependencies', $package_data['module_dependencies'] );
     315        }
    312316
    313317        if ( in_array( 'wp-i18n', $dependencies, true ) ) {
  • trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php

    r61065 r61703  
    19401940        $data       = $response->get_data();
    19411941        $properties = $data['schema']['properties'];
    1942         $this->assertCount( 29, $properties );
     1942        $this->assertCount( 32, $properties );
    19431943        $this->assertArrayHasKey( 'author', $properties );
    19441944        $this->assertArrayHasKey( 'alt_text', $properties );
     1945        $this->assertArrayHasKey( 'exif_orientation', $properties );
     1946        $this->assertArrayHasKey( 'filename', $properties );
     1947        $this->assertArrayHasKey( 'filesize', $properties );
    19451948        $this->assertArrayHasKey( 'caption', $properties );
    19461949        $this->assertArrayHasKey( 'raw', $properties['caption']['properties'] );
  • trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php

    r61674 r61703  
    110110            '/wp/v2/media/(?P<id>[\\d]+)/post-process',
    111111            '/wp/v2/media/(?P<id>[\\d]+)/edit',
     112            '/wp/v2/media/(?P<id>[\\d]+)/sideload',
    112113            '/wp/v2/blocks',
    113114            '/wp/v2/blocks/(?P<id>[\d]+)',
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r61702 r61703  
    31483148                            "description": "The ID for the associated post of the attachment.",
    31493149                            "type": "integer",
     3150                            "required": false
     3151                        },
     3152                        "generate_sub_sizes": {
     3153                            "type": "boolean",
     3154                            "default": true,
     3155                            "description": "Whether to generate image sub sizes.",
     3156                            "required": false
     3157                        },
     3158                        "convert_format": {
     3159                            "type": "boolean",
     3160                            "default": true,
     3161                            "description": "Whether to convert image formats.",
    31503162                            "required": false
    31513163                        }
     
    36653677            ]
    36663678        },
     3679        "/wp/v2/media/(?P<id>[\\d]+)/sideload": {
     3680            "namespace": "wp/v2",
     3681            "methods": [
     3682                "POST"
     3683            ],
     3684            "endpoints": [
     3685                {
     3686                    "methods": [
     3687                        "POST"
     3688                    ],
     3689                    "args": {
     3690                        "id": {
     3691                            "description": "Unique identifier for the attachment.",
     3692                            "type": "integer",
     3693                            "required": false
     3694                        },
     3695                        "image_size": {
     3696                            "description": "Image size.",
     3697                            "type": "string",
     3698                            "enum": [
     3699                                "thumbnail",
     3700                                "medium",
     3701                                "medium_large",
     3702                                "large",
     3703                                "1536x1536",
     3704                                "2048x2048",
     3705                                "original",
     3706                                "full"
     3707                            ],
     3708                            "required": true
     3709                        },
     3710                        "convert_format": {
     3711                            "type": "boolean",
     3712                            "default": true,
     3713                            "description": "Whether to convert image formats.",
     3714                            "required": false
     3715                        }
     3716                    }
     3717                }
     3718            ]
     3719        },
    36673720        "/wp/v2/menu-items": {
    36683721            "namespace": "wp/v2",
     
    1270112754        }
    1270212755    },
     12756    "image_sizes": {
     12757        "thumbnail": {
     12758            "width": 150,
     12759            "height": 150,
     12760            "crop": true
     12761        },
     12762        "medium": {
     12763            "width": 300,
     12764            "height": 300,
     12765            "crop": false
     12766        },
     12767        "medium_large": {
     12768            "width": 768,
     12769            "height": 0,
     12770            "crop": false
     12771        },
     12772        "large": {
     12773            "width": 1024,
     12774            "height": 1024,
     12775            "crop": false
     12776        },
     12777        "1536x1536": {
     12778            "width": 1536,
     12779            "height": 1536,
     12780            "crop": false
     12781        },
     12782        "2048x2048": {
     12783            "width": 2048,
     12784            "height": 2048,
     12785            "crop": false
     12786        }
     12787    },
     12788    "image_size_threshold": 2560,
     12789    "image_output_formats": {},
     12790    "jpeg_interlaced": false,
     12791    "png_interlaced": false,
     12792    "gif_interlaced": false,
    1270312793    "site_logo": 0,
    1270412794    "site_icon": 0,
     
    1351313603        "post": null,
    1351413604        "source_url": "http://example.org/wp-content/uploads//tmp/canola.jpg",
     13605        "filename": "canola.jpg",
     13606        "filesize": null,
    1351513607        "_links": {
    1351613608            "self": [
     
    1359013682    "media_details": {},
    1359113683    "post": null,
    13592     "source_url": "http://example.org/wp-content/uploads//tmp/canola.jpg"
     13684    "source_url": "http://example.org/wp-content/uploads//tmp/canola.jpg",
     13685    "filename": "canola.jpg",
     13686    "filesize": null
    1359313687};
    1359413688
  • trunk/tools/gutenberg/copy-gutenberg-build.js

    r61677 r61703  
    442442                const assetData = parsePHPArray( match[ 1 ] );
    443443
    444                 // For regular scripts, use dependencies as-is
    445                 // Keep dependencies array (don't use module_dependencies)
     444                // For regular scripts, use dependencies as-is.
    446445                if ( ! assetData.dependencies ) {
    447446                    assetData.dependencies = [];
    448447                }
    449 
    450                 // Remove module_dependencies if present (not used for regular scripts)
    451                 delete assetData.module_dependencies;
    452448
    453449                // Create entries for both minified and non-minified versions
     
    921917    const scriptsDest = path.join( wpIncludesDir, scriptsConfig.destination );
    922918
    923     // Transform function to remove source map comments from all JS files
     919    // Transform function to remove source map comments from all JS files.
     920    // Only match actual source map comments at the start of a line (possibly
     921    // with whitespace), not occurrences inside string literals.
    924922    const removeSourceMaps = ( content ) => {
    925         return content.replace( /\/\/# sourceMappingURL=.*$/gm, '' ).trimEnd();
     923        return content.replace( /^\s*\/\/# sourceMappingURL=.*$/gm, '' ).trimEnd();
    926924    };
    927925
Note: See TracChangeset for help on using the changeset viewer.