From bba8251c32056b085e9844350b77533311ab5489 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 11 May 2026 17:17:14 +1000 Subject: [PATCH 01/17] Plugin Directory: Extract asset image dimensions during plugin import. Reads width/height from screenshots, banners and icons in the importer and stores them alongside the existing per-asset metadata. Reuses the cached dimensions when an asset SVN revision is unchanged, falling back to a 128 KB Range request before a full download to keep import bandwidth in check. Records that fail to parse are flagged so a later run can audit them without re-fetching every file. Includes a bin/backfill-asset-dimensions.php script that walks every plugin with non-empty asset metadata and fills in dimensions in bulk. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bin/backfill-asset-dimensions.php | 176 ++++++++++++++++++ .../plugin-directory/cli/class-import.php | 158 +++++++++++++++- 2 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php new file mode 100644 index 0000000000..15c3b1b8f7 --- /dev/null +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php @@ -0,0 +1,176 @@ + 'assets_screenshots', + 'banner' => 'assets_banners', + 'icon' => 'assets_icons', +); + +if ( ! empty( $opts['plugin'] ) ) { + $plugin = Plugin_Directory::get_plugin_post( $opts['plugin'] ); + if ( ! $plugin ) { + fwrite( STDERR, "Plugin '{$opts['plugin']}' not found.\n" ); + exit( 1 ); + } + $post_ids = array( $plugin->ID ); +} else { + $post_ids = $wpdb->get_col( $wpdb->prepare( + "SELECT DISTINCT post_id + FROM {$wpdb->postmeta} + WHERE meta_key IN ( %s, %s, %s ) + AND meta_value != '' + AND meta_value != 'a:0:{}' + ORDER BY post_id ASC", + $meta_keys['screenshot'], + $meta_keys['banner'], + $meta_keys['icon'] + ) ); + + if ( $opts['limit'] > 0 ) { + $post_ids = array_slice( $post_ids, 0, $opts['limit'] ); + } +} + +$total_plugins = count( $post_ids ); +$plugins_updated = 0; +$counts = array( + 'reused' => 0, + 'extracted' => 0, + 'failed' => 0, + 'skipped' => 0, // SVG / non-raster. +); +$failed_files = array(); + +echo "Scanning {$total_plugins} plugins" . ( $opts['dry-run'] ? ' (dry-run)' : '' ) . "...\n"; + +foreach ( $post_ids as $i => $post_id ) { + $plugin = get_post( $post_id ); + if ( ! $plugin || 'plugin' !== $plugin->post_type ) { + continue; + } + + $slug = $plugin->post_name; + $plugin_dirty = false; + + foreach ( $meta_keys as $type => $meta_key ) { + $records = get_post_meta( $post_id, $meta_key, true ); + if ( ! is_array( $records ) || ! $records ) { + continue; + } + + $meta_dirty = false; + + foreach ( $records as $filename => $record ) { + if ( ! is_array( $record ) || empty( $record['filename'] ) || ! isset( $record['revision'] ) ) { + continue; + } + + $ext = strtolower( pathinfo( $record['filename'], PATHINFO_EXTENSION ) ); + $is_raster = in_array( $ext, array( 'png', 'jpg', 'jpeg', 'gif' ), true ); + $has_dims = ! empty( $record['width'] ) && ! empty( $record['height'] ); + $tried_fail = ! empty( $record['dimensions_failed'] ); + + if ( ! $is_raster ) { + ++$counts['skipped']; + continue; + } + if ( $has_dims ) { + ++$counts['reused']; + continue; + } + if ( $tried_fail ) { + ++$counts['failed']; + $failed_files[] = "{$slug} {$type} {$record['filename']}"; + continue; + } + + // Pass the existing record as the prior so the helper short-circuits + // for cases it already knows the answer for. + $updated = Import::enrich_asset_dimensions( $record, $record, $slug ); + + if ( ! empty( $updated['width'] ) && ! empty( $updated['height'] ) ) { + ++$counts['extracted']; + } else { + ++$counts['failed']; + $failed_files[] = "{$slug} {$type} {$record['filename']}"; + } + + if ( $updated !== $record ) { + $records[ $filename ] = $updated; + $meta_dirty = true; + } + } + + if ( $meta_dirty ) { + $plugin_dirty = true; + if ( ! $opts['dry-run'] ) { + update_post_meta( $post_id, $meta_key, wp_slash( $records ) ); + } + } + } + + if ( $plugin_dirty ) { + ++$plugins_updated; + } + + if ( ( $i + 1 ) % 100 === 0 ) { + echo sprintf( + " [%d/%d] reused=%d extracted=%d failed=%d skipped=%d\n", + $i + 1, + $total_plugins, + $counts['reused'], + $counts['extracted'], + $counts['failed'], + $counts['skipped'] + ); + } +} + +echo "\nDone.\n"; +echo " Plugins scanned: {$total_plugins}\n"; +echo " Plugins updated: {$plugins_updated}" . ( $opts['dry-run'] ? ' (would update)' : '' ) . "\n"; +echo " Assets with cached dimensions reused: {$counts['reused']}\n"; +echo " Assets newly extracted: {$counts['extracted']}\n"; +echo " Assets failed extraction: {$counts['failed']}\n"; +echo " Non-raster assets skipped (SVG/etc.): {$counts['skipped']}\n"; + +if ( $failed_files ) { + echo "\nFailed assets:\n " . implode( "\n ", $failed_files ) . "\n"; +} diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 3644ba7fbf..5d339ee482 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -827,6 +827,20 @@ protected function export_and_parse_plugin( $plugin_slug ) { 'blueprint' => 100 * KB_IN_BYTES, ); + // Previously-imported asset metadata, used to skip re-reading any file whose + // SVN revision hasn't changed. The cached `width`/`height` from the prior + // import is reused as-is when the revision matches. + $prior_assets = array( + 'screenshot' => array(), + 'banner' => array(), + 'icon' => array(), + ); + if ( $this->plugin ) { + $prior_assets['screenshot'] = get_post_meta( $this->plugin->ID, 'assets_screenshots', true ) ?: array(); + $prior_assets['banner'] = get_post_meta( $this->plugin->ID, 'assets_banners', true ) ?: array(); + $prior_assets['icon'] = get_post_meta( $this->plugin->ID, 'assets_icons', true ) ?: array(); + } + $svn_blueprints_folder = null; $svn_assets_folder = SVN::ls( self::PLUGIN_SVN_BASE . "/{$plugin_slug}/assets/", true /* verbose */ ); if ( $svn_assets_folder ) { // /assets/ may not exist. @@ -862,7 +876,15 @@ protected function export_and_parse_plugin( $plugin_slug ) { $resolution = preg_replace( '/[^0-9]/u', 'x', $resolution ); } - $assets[ $type ][ $asset['filename'] ] = compact( 'filename', 'revision', 'resolution', 'location', 'locale' ); + $record = compact( 'filename', 'revision', 'resolution', 'location', 'locale' ); + + $record = self::enrich_asset_dimensions( + $record, + $prior_assets[ $type ][ $filename ] ?? null, + $plugin_slug + ); + + $assets[ $type ][ $asset['filename'] ] = $record; } } @@ -921,12 +943,21 @@ protected function export_and_parse_plugin( $plugin_slug ) { continue; } - $assets['screenshot'][ $filename ] = array( + $record = array( 'filename' => $filename, 'revision' => $svn_export['revision'], 'resolution' => $screenshot_id, 'location' => 'plugin', ); + + $record = self::enrich_asset_dimensions( + $record, + $prior_assets['screenshot'][ $filename ] ?? null, + $plugin_slug, + $plugin_screenshot + ); + + $assets['screenshot'][ $filename ] = $record; } if ( 'trunk' === $stable_tag ) { @@ -1068,6 +1099,129 @@ function( $block ) { ); } + /** + * Populate `width` and `height` on an asset record. + * + * Reuses the cached values from the prior import when the SVN revision + * hasn't changed; otherwise reads the image (from a local path if one is + * supplied, or by fetching the file from SVN) and parses dimensions via + * `getimagesize()`. To keep import bandwidth in check, the remote path + * first tries the leading 128 KB via a Range request and only falls + * back to the full body when that prefix isn't enough to decode the + * header (e.g. progressive JPEGs with the SOF segment deep in the + * stream). + * + * SVG and unknown formats are left untouched — they have no fixed + * pixel dimensions to record. When extraction fails for a raster + * format, `dimensions_failed` is set on the record so a later run + * can identify the offenders without re-fetching every file. + * + * @param array $record The asset record (`filename`, `revision`, …). + * @param array|null $prior Matching record from the prior import, or null. + * @param string $slug Plugin slug, used to construct the SVN URL. + * @param string|null $local Optional local path; preferred over a remote + * fetch when the file is already on disk. + * @return array The record, with `width` and `height` added when known. + */ + public static function enrich_asset_dimensions( $record, $prior, $slug, $local = null ) { + if ( is_array( $prior ) && isset( $prior['revision'] ) && (string) $prior['revision'] === (string) $record['revision'] ) { + if ( isset( $prior['width'], $prior['height'] ) && $prior['width'] > 0 && $prior['height'] > 0 ) { + $record['width'] = (int) $prior['width']; + $record['height'] = (int) $prior['height']; + + return $record; + } + + // Same revision and we already tried — don't re-fetch only to fail again. + if ( ! empty( $prior['dimensions_failed'] ) ) { + $record['dimensions_failed'] = true; + + return $record; + } + } + + $ext = strtolower( pathinfo( $record['filename'], PATHINFO_EXTENSION ) ); + if ( ! in_array( $ext, array( 'png', 'jpg', 'jpeg', 'gif' ), true ) ) { + return $record; + } + + $size = false; + + if ( $local && file_exists( $local ) ) { + $size = @getimagesize( $local ); + } else { + // 'plugin'-located screenshots live in /trunk/; everything + // else (banners, icons, /assets/-located screenshots) lives + // in /assets/. + $folder = ( isset( $record['location'] ) && 'plugin' === $record['location'] ) ? 'trunk' : 'assets'; + $url = add_query_arg( + 'rev', + $record['revision'], + sprintf( + '%s/%s/%s/%s', + self::PLUGIN_SVN_BASE, + $slug, + $folder, + rawurlencode( $record['filename'] ) + ) + ); + + // Try the first 128 KB first; it covers all PNG/GIF and the + // overwhelming majority of JPEGs without paying for the full + // download of multi-megabyte screenshots. + $prefix = self::fetch_asset_bytes( $url, 131072 ); + if ( '' !== $prefix ) { + $size = @getimagesizefromstring( $prefix ); + } + + if ( ! $size ) { + $full = self::fetch_asset_bytes( $url ); + if ( '' !== $full ) { + $size = @getimagesizefromstring( $full ); + } + } + } + + if ( $size && ! empty( $size[0] ) && ! empty( $size[1] ) ) { + $record['width'] = (int) $size[0]; + $record['height'] = (int) $size[1]; + } else { + $record['dimensions_failed'] = true; + } + + return $record; + } + + /** + * Fetch the bytes of an asset over HTTP. + * + * Pass `$range` to request only a prefix via a `Range` header. The body + * is returned for both 200 and 206 responses; an empty string indicates + * the request failed or returned no usable bytes. + * + * @param string $url The asset URL. + * @param int $range Optional. The number of leading bytes to request. + * @return string The response body, or an empty string on failure. + */ + protected static function fetch_asset_bytes( $url, $range = 0 ) { + $args = array( 'timeout' => 15 ); + if ( $range > 0 ) { + $args['headers'] = array( 'Range' => 'bytes=0-' . ( $range - 1 ) ); + } + + $response = wp_remote_get( $url, $args ); + if ( is_wp_error( $response ) ) { + return ''; + } + + $code = wp_remote_retrieve_response_code( $response ); + if ( 200 !== $code && 206 !== $code ) { + return ''; + } + + return (string) wp_remote_retrieve_body( $response ); + } + /** * Find the plugin readme file. * From 6fdd329cfb410f7a80b41fe6c0be46e523b9ed36 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 11 May 2026 17:20:47 +1000 Subject: [PATCH 02/17] Plugin Directory: Drop the dimensions_failed flag on asset records. Failures are no longer persisted to post meta. The importer simply omits width/height when extraction fails; the next import retries from scratch and the backfill script still surfaces a list of unparseable files at the end of each run. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bin/backfill-asset-dimensions.php | 12 ++------ .../plugin-directory/cli/class-import.php | 28 +++++++------------ 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php index 15c3b1b8f7..aa215e5497 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php @@ -102,10 +102,9 @@ continue; } - $ext = strtolower( pathinfo( $record['filename'], PATHINFO_EXTENSION ) ); - $is_raster = in_array( $ext, array( 'png', 'jpg', 'jpeg', 'gif' ), true ); - $has_dims = ! empty( $record['width'] ) && ! empty( $record['height'] ); - $tried_fail = ! empty( $record['dimensions_failed'] ); + $ext = strtolower( pathinfo( $record['filename'], PATHINFO_EXTENSION ) ); + $is_raster = in_array( $ext, array( 'png', 'jpg', 'jpeg', 'gif' ), true ); + $has_dims = ! empty( $record['width'] ) && ! empty( $record['height'] ); if ( ! $is_raster ) { ++$counts['skipped']; @@ -115,11 +114,6 @@ ++$counts['reused']; continue; } - if ( $tried_fail ) { - ++$counts['failed']; - $failed_files[] = "{$slug} {$type} {$record['filename']}"; - continue; - } // Pass the existing record as the prior so the helper short-circuits // for cases it already knows the answer for. diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 5d339ee482..5b3e57e0c6 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1112,9 +1112,7 @@ function( $block ) { * stream). * * SVG and unknown formats are left untouched — they have no fixed - * pixel dimensions to record. When extraction fails for a raster - * format, `dimensions_failed` is set on the record so a later run - * can identify the offenders without re-fetching every file. + * pixel dimensions to record. * * @param array $record The asset record (`filename`, `revision`, …). * @param array|null $prior Matching record from the prior import, or null. @@ -1124,20 +1122,16 @@ function( $block ) { * @return array The record, with `width` and `height` added when known. */ public static function enrich_asset_dimensions( $record, $prior, $slug, $local = null ) { - if ( is_array( $prior ) && isset( $prior['revision'] ) && (string) $prior['revision'] === (string) $record['revision'] ) { - if ( isset( $prior['width'], $prior['height'] ) && $prior['width'] > 0 && $prior['height'] > 0 ) { - $record['width'] = (int) $prior['width']; - $record['height'] = (int) $prior['height']; - - return $record; - } - - // Same revision and we already tried — don't re-fetch only to fail again. - if ( ! empty( $prior['dimensions_failed'] ) ) { - $record['dimensions_failed'] = true; + if ( + is_array( $prior ) && + isset( $prior['revision'], $prior['width'], $prior['height'] ) && + (string) $prior['revision'] === (string) $record['revision'] && + $prior['width'] > 0 && $prior['height'] > 0 + ) { + $record['width'] = (int) $prior['width']; + $record['height'] = (int) $prior['height']; - return $record; - } + return $record; } $ext = strtolower( pathinfo( $record['filename'], PATHINFO_EXTENSION ) ); @@ -1185,8 +1179,6 @@ public static function enrich_asset_dimensions( $record, $prior, $slug, $local = if ( $size && ! empty( $size[0] ) && ! empty( $size[1] ) ) { $record['width'] = (int) $size[0]; $record['height'] = (int) $size[1]; - } else { - $record['dimensions_failed'] = true; } return $record; From ed88db908cd5aba74f59d3d39f6b97ac6afe1d61 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 11 May 2026 17:21:28 +1000 Subject: [PATCH 03/17] Plugin Directory: Use wp_remote_get limit_response_size instead of a Range header. Drops the Range/206 plumbing and the fetch_asset_bytes helper. The 128 KB-then-full retry now lives inline as a two-step loop driven by the limit_response_size arg. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plugin-directory/cli/class-import.php | 61 +++++++------------ 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 5b3e57e0c6..b415c2a02a 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1160,18 +1160,29 @@ public static function enrich_asset_dimensions( $record, $prior, $slug, $local = ) ); - // Try the first 128 KB first; it covers all PNG/GIF and the + // Cap the first read at 128 KB — covers all PNG/GIF and the // overwhelming majority of JPEGs without paying for the full - // download of multi-megabyte screenshots. - $prefix = self::fetch_asset_bytes( $url, 131072 ); - if ( '' !== $prefix ) { - $size = @getimagesizefromstring( $prefix ); - } + // download of multi-megabyte screenshots. Fall back to a full + // read only when the prefix isn't enough to decode the header. + foreach ( array( 131072, 0 ) as $limit ) { + $args = array( 'timeout' => 15 ); + if ( $limit > 0 ) { + $args['limit_response_size'] = $limit; + } + + $response = wp_remote_get( $url, $args ); + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + break; + } - if ( ! $size ) { - $full = self::fetch_asset_bytes( $url ); - if ( '' !== $full ) { - $size = @getimagesizefromstring( $full ); + $body = wp_remote_retrieve_body( $response ); + if ( '' === $body ) { + break; + } + + $size = @getimagesizefromstring( $body ); + if ( $size ) { + break; } } } @@ -1184,36 +1195,6 @@ public static function enrich_asset_dimensions( $record, $prior, $slug, $local = return $record; } - /** - * Fetch the bytes of an asset over HTTP. - * - * Pass `$range` to request only a prefix via a `Range` header. The body - * is returned for both 200 and 206 responses; an empty string indicates - * the request failed or returned no usable bytes. - * - * @param string $url The asset URL. - * @param int $range Optional. The number of leading bytes to request. - * @return string The response body, or an empty string on failure. - */ - protected static function fetch_asset_bytes( $url, $range = 0 ) { - $args = array( 'timeout' => 15 ); - if ( $range > 0 ) { - $args['headers'] = array( 'Range' => 'bytes=0-' . ( $range - 1 ) ); - } - - $response = wp_remote_get( $url, $args ); - if ( is_wp_error( $response ) ) { - return ''; - } - - $code = wp_remote_retrieve_response_code( $response ); - if ( 200 !== $code && 206 !== $code ) { - return ''; - } - - return (string) wp_remote_retrieve_body( $response ); - } - /** * Find the plugin readme file. * From 7267d20c36af634c834a308faf88472bea0b324b Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 11 May 2026 17:22:57 +1000 Subject: [PATCH 04/17] Plugin Directory: Use Template::get_asset_url() to build the asset URL. Drops the manual sprintf + add_query_arg in enrich_asset_dimensions and reuses the existing helper, which already honors the assets/trunk location split and the revision cache-buster. The helper now takes the plugin post rather than the slug. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bin/backfill-asset-dimensions.php | 4 +-- .../plugin-directory/cli/class-import.php | 34 ++++++------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php index aa215e5497..1c35b55bea 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php @@ -86,7 +86,7 @@ continue; } - $slug = $plugin->post_name; + $slug = $plugin->post_name; // Only used for the failure list. $plugin_dirty = false; foreach ( $meta_keys as $type => $meta_key ) { @@ -117,7 +117,7 @@ // Pass the existing record as the prior so the helper short-circuits // for cases it already knows the answer for. - $updated = Import::enrich_asset_dimensions( $record, $record, $slug ); + $updated = Import::enrich_asset_dimensions( $record, $record, $plugin ); if ( ! empty( $updated['width'] ) && ! empty( $updated['height'] ) ) { ++$counts['extracted']; diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index b415c2a02a..05a21a720c 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -881,7 +881,7 @@ protected function export_and_parse_plugin( $plugin_slug ) { $record = self::enrich_asset_dimensions( $record, $prior_assets[ $type ][ $filename ] ?? null, - $plugin_slug + $this->plugin ); $assets[ $type ][ $asset['filename'] ] = $record; @@ -953,7 +953,7 @@ protected function export_and_parse_plugin( $plugin_slug ) { $record = self::enrich_asset_dimensions( $record, $prior_assets['screenshot'][ $filename ] ?? null, - $plugin_slug, + $this->plugin, $plugin_screenshot ); @@ -1114,14 +1114,14 @@ function( $block ) { * SVG and unknown formats are left untouched — they have no fixed * pixel dimensions to record. * - * @param array $record The asset record (`filename`, `revision`, …). - * @param array|null $prior Matching record from the prior import, or null. - * @param string $slug Plugin slug, used to construct the SVN URL. - * @param string|null $local Optional local path; preferred over a remote - * fetch when the file is already on disk. + * @param array $record The asset record (`filename`, `revision`, …). + * @param array|null $prior Matching record from the prior import, or null. + * @param int|\WP_Post|null $post The plugin post, used to construct the SVN URL. + * @param string|null $local Optional local path; preferred over a remote + * fetch when the file is already on disk. * @return array The record, with `width` and `height` added when known. */ - public static function enrich_asset_dimensions( $record, $prior, $slug, $local = null ) { + public static function enrich_asset_dimensions( $record, $prior, $post, $local = null ) { if ( is_array( $prior ) && isset( $prior['revision'], $prior['width'], $prior['height'] ) && @@ -1144,21 +1144,9 @@ public static function enrich_asset_dimensions( $record, $prior, $slug, $local = if ( $local && file_exists( $local ) ) { $size = @getimagesize( $local ); } else { - // 'plugin'-located screenshots live in /trunk/; everything - // else (banners, icons, /assets/-located screenshots) lives - // in /assets/. - $folder = ( isset( $record['location'] ) && 'plugin' === $record['location'] ) ? 'trunk' : 'assets'; - $url = add_query_arg( - 'rev', - $record['revision'], - sprintf( - '%s/%s/%s/%s', - self::PLUGIN_SVN_BASE, - $slug, - $folder, - rawurlencode( $record['filename'] ) - ) - ); + // Fetch direct from SVN rather than the CDN, so the byte + // stream is always the revision we just listed. + $url = Template::get_asset_url( $post, $record, false /* no CDN */ ); // Cap the first read at 128 KB — covers all PNG/GIF and the // overwhelming majority of JPEGs without paying for the full From 1165377f4132a9959f1f314916d25b25190ab06c Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 11 May 2026 17:23:55 +1000 Subject: [PATCH 05/17] Plugin Directory: Slim down the enrich_asset_dimensions() docblock. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plugin-directory/cli/class-import.php | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 05a21a720c..68db4305bf 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1100,26 +1100,14 @@ function( $block ) { } /** - * Populate `width` and `height` on an asset record. + * Populate `width` and `height` on an asset record, reusing the prior + * import's values when the SVN revision hasn't changed. * - * Reuses the cached values from the prior import when the SVN revision - * hasn't changed; otherwise reads the image (from a local path if one is - * supplied, or by fetching the file from SVN) and parses dimensions via - * `getimagesize()`. To keep import bandwidth in check, the remote path - * first tries the leading 128 KB via a Range request and only falls - * back to the full body when that prefix isn't enough to decode the - * header (e.g. progressive JPEGs with the SOF segment deep in the - * stream). - * - * SVG and unknown formats are left untouched — they have no fixed - * pixel dimensions to record. - * - * @param array $record The asset record (`filename`, `revision`, …). - * @param array|null $prior Matching record from the prior import, or null. - * @param int|\WP_Post|null $post The plugin post, used to construct the SVN URL. - * @param string|null $local Optional local path; preferred over a remote - * fetch when the file is already on disk. - * @return array The record, with `width` and `height` added when known. + * @param array $record The asset record. + * @param array|null $prior Matching record from the prior import. + * @param int|\WP_Post|null $post The plugin post. + * @param string|null $local Optional local path to read instead of fetching from SVN. + * @return array */ public static function enrich_asset_dimensions( $record, $prior, $post, $local = null ) { if ( From 0b039e6bea2bf8a4b0965bfc92ef1a4378c32a5a Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 11 May 2026 17:29:21 +1000 Subject: [PATCH 06/17] Plugin Directory: Fix phpcs violations. - Trim one extra space in the enrich_asset_dimensions() docblock so the parameter type column aligns correctly. - Fall through to the remote fetch when getimagesize() on the local export fails (per Copilot review feedback). - Mark the new CLI backfill script with phpcs:ignoreFile, matching the convention in .github/bin/phpcs-branch.php; CLI scripts intentionally echo unescaped output and shadow WP global names. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bin/backfill-asset-dimensions.php | 1 + .../plugins/plugin-directory/cli/class-import.php | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php index 1c35b55bea..1a23baf0ba 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php @@ -1,4 +1,5 @@ Date: Mon, 11 May 2026 17:31:18 +1000 Subject: [PATCH 07/17] Plugin Directory: Send a real Range header on the first asset fetch. Switches the prefix read to a proper Range: bytes=0-131071 request so the server stops sending after the requested bytes, accepting both 200 and 206 responses. limit_response_size stays as a memory backstop when the server ignores Range and streams the full body. Per Copilot review feedback. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plugins/plugin-directory/cli/class-import.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 0e2b933388..33b437e5fa 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1138,18 +1138,23 @@ public static function enrich_asset_dimensions( $record, $prior, $post, $local = // stream is always the revision we just listed. $url = Template::get_asset_url( $post, $record, false /* no CDN */ ); - // Cap the first read at 128 KB — covers all PNG/GIF and the - // overwhelming majority of JPEGs without paying for the full - // download of multi-megabyte screenshots. Fall back to a full - // read only when the prefix isn't enough to decode the header. + // Ask for just the first 128 KB via Range — enough for all + // PNG/GIF and the overwhelming majority of JPEGs, saving the + // bandwidth of downloading multi-megabyte screenshots. Fall + // back to a full read when the prefix isn't enough to decode + // the header (e.g. JPEGs with the SOF marker past 128 KB). + // `limit_response_size` is a memory backstop in case the + // server ignores Range and streams the whole body. foreach ( array( 131072, 0 ) as $limit ) { $args = array( 'timeout' => 15 ); if ( $limit > 0 ) { + $args['headers'] = array( 'Range' => 'bytes=0-' . ( $limit - 1 ) ); $args['limit_response_size'] = $limit; } $response = wp_remote_get( $url, $args ); - if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + $code = wp_remote_retrieve_response_code( $response ); + if ( is_wp_error( $response ) || ( 200 !== $code && 206 !== $code ) ) { break; } From 2ae4dc7e99c3e7e7b26872632fd9310f8af0cc05 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 11 May 2026 17:34:03 +1000 Subject: [PATCH 08/17] Plugin Directory: Bump HTTP timeout to 30s for the backfill bin script. Bulk runs are more tolerant of slow SVN responses than the per-commit import path. Uses http_request_args (not http_request_timeout) so the override applies after enrich_asset_dimensions() sets its own timeout. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bin/backfill-asset-dimensions.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php index 1a23baf0ba..5f7647291a 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php @@ -38,6 +38,17 @@ global $wpdb; +// Be more patient with slow SVN responses during a bulk run than the +// per-request import path is. Uses http_request_args (not the timeout +// filter) so the helper's explicit `timeout => 15` is overridden. +add_filter( + 'http_request_args', + function ( $args ) { + $args['timeout'] = 30; + return $args; + } +); + $meta_keys = array( 'screenshot' => 'assets_screenshots', 'banner' => 'assets_banners', From 87d08da3c60e2b0c474053e43cb1def0a8bd1b01 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 11 May 2026 17:35:23 +1000 Subject: [PATCH 09/17] Plugin Directory: Trim the asset-fetch inline comments. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plugin-directory/cli/class-import.php | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 33b437e5fa..d522bd4ea0 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1134,17 +1134,11 @@ public static function enrich_asset_dimensions( $record, $prior, $post, $local = } if ( ! $size ) { - // Fetch direct from SVN rather than the CDN, so the byte - // stream is always the revision we just listed. - $url = Template::get_asset_url( $post, $record, false /* no CDN */ ); - - // Ask for just the first 128 KB via Range — enough for all - // PNG/GIF and the overwhelming majority of JPEGs, saving the - // bandwidth of downloading multi-megabyte screenshots. Fall - // back to a full read when the prefix isn't enough to decode - // the header (e.g. JPEGs with the SOF marker past 128 KB). - // `limit_response_size` is a memory backstop in case the - // server ignores Range and streams the whole body. + $url = Template::get_asset_url( $post, $record, false ); + + // Range the first read to 128 KB — enough for the headers of + // PNG/GIF and most JPEGs. Fall back to a full read only when + // the prefix isn't enough to decode the header. foreach ( array( 131072, 0 ) as $limit ) { $args = array( 'timeout' => 15 ); if ( $limit > 0 ) { From 9e73d9856619e03ada471d4ac99730dbbd9eb387 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 11 May 2026 17:35:49 +1000 Subject: [PATCH 10/17] Plugin Directory: Restore the /* no CDN */ inline comment on the URL builder. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../wp-content/plugins/plugin-directory/cli/class-import.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index d522bd4ea0..aca52d3d9f 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1134,7 +1134,7 @@ public static function enrich_asset_dimensions( $record, $prior, $post, $local = } if ( ! $size ) { - $url = Template::get_asset_url( $post, $record, false ); + $url = Template::get_asset_url( $post, $record, false /* no CDN */ ); // Range the first read to 128 KB — enough for the headers of // PNG/GIF and most JPEGs. Fall back to a full read only when From e9adac4e8e3a6d579f3d00d89b6bb503aee12540 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 12 May 2026 12:30:00 +1000 Subject: [PATCH 11/17] Plugin Directory: Drop null from the enrich_asset_dimensions() $post docblock. Template::get_asset_url() dereferences the post and would fatal on null. Both callers pass a non-null post (importer after the early throw, backfill after a guard), so the contract should reflect that. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plugins/plugin-directory/cli/class-import.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index aca52d3d9f..a1e489a0e8 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1103,10 +1103,10 @@ function( $block ) { * Populate `width` and `height` on an asset record, reusing the prior * import's values when the SVN revision hasn't changed. * - * @param array $record The asset record. - * @param array|null $prior Matching record from the prior import. - * @param int|\WP_Post|null $post The plugin post. - * @param string|null $local Optional local path to read instead of fetching from SVN. + * @param array $record The asset record. + * @param array|null $prior Matching record from the prior import. + * @param int|\WP_Post $post The plugin post. + * @param string|null $local Optional local path to read instead of fetching from SVN. * @return array */ public static function enrich_asset_dimensions( $record, $prior, $post, $local = null ) { From 2f9883ff78044edeb50334ba64a7f31ae0e70f5d Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 12 May 2026 12:39:07 +1000 Subject: [PATCH 12/17] Plugin Directory: Use KB_IN_BYTES for the asset-fetch range size. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../wp-content/plugins/plugin-directory/cli/class-import.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index a1e489a0e8..785fd02a33 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1139,7 +1139,7 @@ public static function enrich_asset_dimensions( $record, $prior, $post, $local = // Range the first read to 128 KB — enough for the headers of // PNG/GIF and most JPEGs. Fall back to a full read only when // the prefix isn't enough to decode the header. - foreach ( array( 131072, 0 ) as $limit ) { + foreach ( array( 128 * KB_IN_BYTES, 0 ) as $limit ) { $args = array( 'timeout' => 15 ); if ( $limit > 0 ) { $args['headers'] = array( 'Range' => 'bytes=0-' . ( $limit - 1 ) ); From a269d042822efe4686373ead211d6ab577548b1f Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 12 May 2026 12:40:00 +1000 Subject: [PATCH 13/17] Plugin Directory: Narrow enrich_asset_dimensions() $post docblock to WP_Post. Both callsites pass a WP_Post object; document the actual contract. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plugins/plugin-directory/cli/class-import.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 785fd02a33..134ac82720 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1103,10 +1103,10 @@ function( $block ) { * Populate `width` and `height` on an asset record, reusing the prior * import's values when the SVN revision hasn't changed. * - * @param array $record The asset record. - * @param array|null $prior Matching record from the prior import. - * @param int|\WP_Post $post The plugin post. - * @param string|null $local Optional local path to read instead of fetching from SVN. + * @param array $record The asset record. + * @param array|null $prior Matching record from the prior import. + * @param \WP_Post $post The plugin post. + * @param string|null $local Optional local path to read instead of fetching from SVN. * @return array */ public static function enrich_asset_dimensions( $record, $prior, $post, $local = null ) { From bf77b4dd0b0f3fd7612f6f72dd2d3c301d111c3b Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 12 May 2026 12:42:11 +1000 Subject: [PATCH 14/17] Plugin Directory: Inline get_post_meta() into the $prior_assets literal. $this->plugin is guaranteed non-null by import_from_svn() before this point, so the conditional fill is unnecessary. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plugins/plugin-directory/cli/class-import.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 134ac82720..13fedf09b6 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -828,18 +828,12 @@ protected function export_and_parse_plugin( $plugin_slug ) { ); // Previously-imported asset metadata, used to skip re-reading any file whose - // SVN revision hasn't changed. The cached `width`/`height` from the prior - // import is reused as-is when the revision matches. + // SVN revision hasn't changed. $prior_assets = array( - 'screenshot' => array(), - 'banner' => array(), - 'icon' => array(), + 'screenshot' => get_post_meta( $this->plugin->ID, 'assets_screenshots', true ) ?: array(), + 'banner' => get_post_meta( $this->plugin->ID, 'assets_banners', true ) ?: array(), + 'icon' => get_post_meta( $this->plugin->ID, 'assets_icons', true ) ?: array(), ); - if ( $this->plugin ) { - $prior_assets['screenshot'] = get_post_meta( $this->plugin->ID, 'assets_screenshots', true ) ?: array(); - $prior_assets['banner'] = get_post_meta( $this->plugin->ID, 'assets_banners', true ) ?: array(); - $prior_assets['icon'] = get_post_meta( $this->plugin->ID, 'assets_icons', true ) ?: array(); - } $svn_blueprints_folder = null; $svn_assets_folder = SVN::ls( self::PLUGIN_SVN_BASE . "/{$plugin_slug}/assets/", true /* verbose */ ); From 4bf9a744bfa57629f047c6466afbd8f0396c94c5 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 12 May 2026 12:45:54 +1000 Subject: [PATCH 15/17] Plugin Directory: Stop silencing getimagesize() warnings with @. Let the warnings propagate to the error log so malformed assets show up in reporting rather than being silently swallowed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../wp-content/plugins/plugin-directory/cli/class-import.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 13fedf09b6..add0ee29ed 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1124,7 +1124,7 @@ public static function enrich_asset_dimensions( $record, $prior, $post, $local = $size = false; if ( $local && file_exists( $local ) ) { - $size = @getimagesize( $local ); + $size = getimagesize( $local ); } if ( ! $size ) { @@ -1151,7 +1151,7 @@ public static function enrich_asset_dimensions( $record, $prior, $post, $local = break; } - $size = @getimagesizefromstring( $body ); + $size = getimagesizefromstring( $body ); if ( $size ) { break; } From c61131506a5bf8a7e50de97276e0258737343f75 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 12 May 2026 12:53:01 +1000 Subject: [PATCH 16/17] Plugin Directory: Document why the asset-fetch loop breaks on transport errors. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spell out that the implicit retry (full read) only fires on prefix-too-short-to-decode, and that wp_error / non-2xx / empty body deliberately exit instead of retrying — re-requesting without Range will not help any of those failure modes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plugins/plugin-directory/cli/class-import.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index add0ee29ed..30b5e44a51 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1132,7 +1132,12 @@ public static function enrich_asset_dimensions( $record, $prior, $post, $local = // Range the first read to 128 KB — enough for the headers of // PNG/GIF and most JPEGs. Fall back to a full read only when - // the prefix isn't enough to decode the header. + // the prefix isn't enough to decode the header — the falsy + // `$size` at the bottom of the loop is the implicit retry. + // Transport errors / non-2xx / empty body intentionally bail + // out via `break`: those failure modes (network down, 4xx, + // 5xx, empty file) won't be helped by re-requesting the same + // URL without Range. foreach ( array( 128 * KB_IN_BYTES, 0 ) as $limit ) { $args = array( 'timeout' => 15 ); if ( $limit > 0 ) { From 7a4ad261a7dbbf24e42080b4b0524ff223c1374f Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Tue, 12 May 2026 13:03:59 +1000 Subject: [PATCH 17/17] Plugin Directory: Switch to wp_getimagesize() and streamed downloads. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wp_getimagesize() prefers Imagick when available, covering SVG/WebP and other formats the plain getimagesize() / getimagesizefromstring() pair misses. Drops the PNG/JPG/GIF extension filter — let wp_getimagesize() decide what it can read. Remote fetches now stream into a temp file via wp_safe_remote_get( ... stream => true ) instead of buffering the body in memory; the same 128 KB Range / full-read two-pass loop is preserved against the on-disk file. The bin backfill drops the "skipped" counter accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bin/backfill-asset-dimensions.php | 21 +++-------- .../plugin-directory/cli/class-import.php | 36 +++++++++---------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php index 5f7647291a..c686d1eb0d 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/bin/backfill-asset-dimensions.php @@ -86,7 +86,6 @@ function ( $args ) { 'reused' => 0, 'extracted' => 0, 'failed' => 0, - 'skipped' => 0, // SVG / non-raster. ); $failed_files = array(); @@ -114,15 +113,7 @@ function ( $args ) { continue; } - $ext = strtolower( pathinfo( $record['filename'], PATHINFO_EXTENSION ) ); - $is_raster = in_array( $ext, array( 'png', 'jpg', 'jpeg', 'gif' ), true ); - $has_dims = ! empty( $record['width'] ) && ! empty( $record['height'] ); - - if ( ! $is_raster ) { - ++$counts['skipped']; - continue; - } - if ( $has_dims ) { + if ( ! empty( $record['width'] ) && ! empty( $record['height'] ) ) { ++$counts['reused']; continue; } @@ -158,13 +149,12 @@ function ( $args ) { if ( ( $i + 1 ) % 100 === 0 ) { echo sprintf( - " [%d/%d] reused=%d extracted=%d failed=%d skipped=%d\n", + " [%d/%d] reused=%d extracted=%d failed=%d\n", $i + 1, $total_plugins, $counts['reused'], $counts['extracted'], - $counts['failed'], - $counts['skipped'] + $counts['failed'] ); } } @@ -173,9 +163,8 @@ function ( $args ) { echo " Plugins scanned: {$total_plugins}\n"; echo " Plugins updated: {$plugins_updated}" . ( $opts['dry-run'] ? ' (would update)' : '' ) . "\n"; echo " Assets with cached dimensions reused: {$counts['reused']}\n"; -echo " Assets newly extracted: {$counts['extracted']}\n"; -echo " Assets failed extraction: {$counts['failed']}\n"; -echo " Non-raster assets skipped (SVG/etc.): {$counts['skipped']}\n"; +echo " Assets newly extracted: {$counts['extracted']}\n"; +echo " Assets failed extraction: {$counts['failed']}\n"; if ( $failed_files ) { echo "\nFailed assets:\n " . implode( "\n ", $failed_files ) . "\n"; diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php index 30b5e44a51..548809b5c0 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php @@ -1116,51 +1116,51 @@ public static function enrich_asset_dimensions( $record, $prior, $post, $local = return $record; } - $ext = strtolower( pathinfo( $record['filename'], PATHINFO_EXTENSION ) ); - if ( ! in_array( $ext, array( 'png', 'jpg', 'jpeg', 'gif' ), true ) ) { - return $record; - } - $size = false; if ( $local && file_exists( $local ) ) { - $size = getimagesize( $local ); + $size = wp_getimagesize( $local ); } if ( ! $size ) { - $url = Template::get_asset_url( $post, $record, false /* no CDN */ ); + $url = Template::get_asset_url( $post, $record, false /* no CDN */ ); + $temp_file = wp_tempnam( $record['filename'] ); // Range the first read to 128 KB — enough for the headers of - // PNG/GIF and most JPEGs. Fall back to a full read only when - // the prefix isn't enough to decode the header — the falsy - // `$size` at the bottom of the loop is the implicit retry. - // Transport errors / non-2xx / empty body intentionally bail - // out via `break`: those failure modes (network down, 4xx, - // 5xx, empty file) won't be helped by re-requesting the same + // most images. Fall back to a full read only when the prefix + // isn't enough to decode the header — the falsy `$size` at + // the bottom of the loop is the implicit retry. Transport + // errors / non-2xx intentionally bail out via `break`: those + // failure modes won't be helped by re-requesting the same // URL without Range. foreach ( array( 128 * KB_IN_BYTES, 0 ) as $limit ) { - $args = array( 'timeout' => 15 ); + $args = array( + 'timeout' => 15, + 'stream' => true, + 'filename' => $temp_file, + ); if ( $limit > 0 ) { $args['headers'] = array( 'Range' => 'bytes=0-' . ( $limit - 1 ) ); $args['limit_response_size'] = $limit; } - $response = wp_remote_get( $url, $args ); + $response = wp_safe_remote_get( $url, $args ); $code = wp_remote_retrieve_response_code( $response ); if ( is_wp_error( $response ) || ( 200 !== $code && 206 !== $code ) ) { break; } - $body = wp_remote_retrieve_body( $response ); - if ( '' === $body ) { + if ( ! file_exists( $temp_file ) || 0 === filesize( $temp_file ) ) { break; } - $size = getimagesizefromstring( $body ); + $size = wp_getimagesize( $temp_file ); if ( $size ) { break; } } + + unlink( $temp_file ); } if ( $size && ! empty( $size[0] ) && ! empty( $size[1] ) ) {