Make WordPress Core

Changeset 61703


Ignore:
Timestamp:
02/20/2026 11:49:50 AM (8 months 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.