0, 'local' => 0 ]; $total = count( $export['manifest']['plugins'] ) + count( $export['manifest']['themes'] ); foreach ( [ 'plugins' => 'plugin', 'themes' => 'theme' ] as $group => $kind ) { $versions = []; $resolved = []; $package_key = static fn( $package ) => 'plugin' === $kind && '.' === dirname( $package['file'] ) ? 'file:' . $package['file'] : 'directory:' . $package['slug']; foreach ( $export['manifest'][ $group ] as $package ) { $versions[ $package_key( $package ) ][ $package['version'] ] = true; } foreach ( $export['manifest'][ $group ] as &$package ) { if ( $progress ) { $progress( 'repository', array_sum( $report ), $total, 'packages' ); } $slug = $package['slug']; $key = $package_key( $package ); // A directory can contain multiple plugin entry points. Resolve // it once before removing bytes shared by those entries. if ( isset( $resolved[ $key ] ) ) { $package['source'] = $resolved[ $key ]; $report[ 'wordpress.org' === $package['source'] ? 'repository' : 'local' ]++; continue; } // Root-level plugin files cannot reliably identify a repository slug. $eligible = count( $versions[ $key ] ) === 1 && ( 'theme' === $kind || '.' !== dirname( $package['file'] ) ); $prefix = $group . '/' . $slug . '/'; $local = []; foreach ( $export['manifest']['files'] as $file ) { if ( str_starts_with( $file['path'], $prefix ) ) { $local[ substr( $file['path'], strlen( $prefix ) ) ] = $file['sha256']; } } $official = $eligible ? self::checksums( $kind, $slug, $package['version'] ) : []; ksort( $local ); ksort( $official ); if ( $local && $local === $official ) { $package['source'] = 'wordpress.org'; $export['manifest']['files'] = array_values( array_filter( $export['manifest']['files'], static fn( $file ) => ! str_starts_with( $file['path'], $prefix ) ) ); foreach ( array_keys( $export['paths'] ) as $path ) { if ( str_starts_with( $path, $prefix ) ) { unset( $export['paths'][ $path ] ); } } $report['repository']++; } else { $package['source'] = 'local'; $report['local']++; } $resolved[ $key ] = $package['source']; } unset( $package ); } return $report; } private static function checksums( string $kind, string $slug, string $version ): array { if ( ! class_exists( '\ZipArchive' ) || ! preg_match( '/^[a-z0-9-]+$/D', $slug ) || ! preg_match( '/^[a-zA-Z0-9][a-zA-Z0-9.+_-]{0,49}$/D', $version ) ) { return []; } $key = 'smooth_sandbox_package_' . hash( 'sha256', $kind . '/' . $slug . '/' . $version ); $cached = get_transient( $key ); if ( is_array( $cached ) ) { return $cached; } $tmp = tempnam( sys_get_temp_dir(), 'ss-package-' ); if ( ! $tmp ) { return []; } Filesystem::local()->chmod( $tmp, 0600 ); $zip = new \ZipArchive(); $opened = false; $hashes = []; try { $response = wp_safe_remote_get( 'https://downloads.wordpress.org/' . $kind . '/' . $slug . '.' . $version . '.zip', [ 'timeout' => 30, 'redirection' => 0, 'stream' => true, 'filename' => $tmp, 'limit_response_size' => Environment::MAX_BYTES + 1 ] ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) || filesize( $tmp ) > Environment::MAX_BYTES || true !== $zip->open( $tmp ) ) { return []; } $opened = true; $bytes = 0; if ( $zip->numFiles > Environment::MAX_FILES ) { return []; } for ( $i = 0; $i < $zip->numFiles; $i++ ) { $entry = $zip->statIndex( $i ); if ( ! $entry || ! str_starts_with( $entry['name'], $slug . '/' ) ) { return []; } if ( str_ends_with( $entry['name'], '/' ) ) { continue; } $relative = substr( $entry['name'], strlen( $slug ) + 1 ); $bytes += $entry['size']; if ( $entry['size'] > 20971520 || $bytes > Environment::MAX_BYTES ) { return []; } if ( ! Environment::safe_path( $relative ) ) { continue; } if ( isset( $hashes[ $relative ] ) ) { return []; } $stream = $zip->getStream( $entry['name'] ); if ( ! $stream ) { return []; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Close the native ZipArchive entry stream after streaming its checksum. $hash = hash_init( 'sha256' ); $read = hash_update_stream( $hash, $stream ); fclose( $stream ); if ( $read !== $entry['size'] ) { return []; } $hashes[ $relative ] = hash_final( $hash ); } set_transient( $key, $hashes, DAY_IN_SECONDS ); return $hashes; } finally { if ( $opened ) { $zip->close(); } Filesystem::local()->delete( $tmp, false, 'f' ); } } }