diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index e062a471d7150..4e255a48843fd 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -3572,6 +3572,10 @@ img { line-height: 180%; } +.fileedit-sub .download-theme-form { + margin-top: 8px; +} + #file-editor-warning .file-editor-warning-content { margin: 25px; } diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 68d6f9a7e966a..72dde4f6709c5 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -77,6 +77,126 @@ wp_die( __( 'The requested theme does not exist.' ) . ' ' . $theme->errors()->get_error_message() ); } +// Handle theme download action: create a zip of the theme and send it to the browser. +if ( 'download_theme' === $action ) { + if ( ! current_user_can( 'edit_themes' ) ) { + wp_die( '
' . __( 'Sorry, you are not allowed to download themes for this site.' ) . '
' ); + } + + // Verify nonce. + if ( ! check_admin_referer( 'download-theme_' . $stylesheet ) ) { + wp_die( '' . __( 'Security check failed.' ) . '
' ); + } + + $theme_dir = $theme->get_stylesheet_directory(); + + if ( ! is_dir( $theme_dir ) ) { + wp_die( '' . __( 'Theme directory not found.' ) . '
' ); + } + + $zipname = $stylesheet; + $version = $theme->get( 'Version' ); + if ( $version ) { + $zipname .= '.' . $version; + } + + $tmpfile = get_temp_dir() . DIRECTORY_SEPARATOR . $zipname . '-' . time() . '.zip'; + + // Attempt to extend execution time to allow large theme archives to be created. + if ( function_exists( 'set_time_limit' ) ) { + @set_time_limit( 300 ); + } + // Try native ZipArchive first, fall back to PclZip if needed. + if ( class_exists( 'ZipArchive' ) ) { + $zip = new ZipArchive(); + if ( true !== $zip->open( $tmpfile, ZipArchive::CREATE ) ) { + @unlink( $tmpfile ); + wp_die( '' . __( 'Could not create zip archive.' ) . '
' ); + } + + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $theme_dir, FilesystemIterator::SKIP_DOTS ), + RecursiveIteratorIterator::LEAVES_ONLY + ); + $files_added = 0; + + foreach ( $files as $file ) { + // With SKIP_DOTS and LEAVES_ONLY, we don't need to check isDir(). + $file_path = $file->getRealPath(); + + // Ensure path is valid and within theme directory. + if ( ! $file_path || ! str_starts_with( $file_path, $theme_dir ) ) { + continue; + } + + $relative_path = substr( $file_path, strlen( $theme_dir ) + 1 ); + + if ( ! $zip->addFile( $file_path, $stylesheet . '/' . $relative_path ) ) { + $zip->close(); + @unlink( $tmpfile ); + wp_die( '' . __( 'Could not create zip archive.' ) . '
' ); + } + + $files_added++; + } + + if ( 0 === $files_added ) { + $zip->close(); + @unlink( $tmpfile ); + wp_die( '' . __( 'No files found to compress.' ) . '
' ); + } + $zip->close(); + } else { + // Use PclZip fallback bundled with WordPress admin. + require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; + + $filelist = array(); + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $theme_dir, FilesystemIterator::SKIP_DOTS ), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ( $files as $file ) { + $file_path = $file->getRealPath(); + + // Ensure path is valid and within theme directory. + if ( ! $file_path || ! str_starts_with( $file_path, $theme_dir ) ) { + continue; + } + + $filelist[] = $file_path; + } + + if ( empty( $filelist ) ) { + @unlink( $tmpfile ); + wp_die( '' . __( 'No files found to compress.' ) . '
' ); + } + + $archive = new PclZip( $tmpfile ); + $result = $archive->create( $filelist, PCLZIP_OPT_REMOVE_PATH, $theme_dir, PCLZIP_OPT_ADD_PATH, $stylesheet ); + if ( 0 === $result ) { + @unlink( $tmpfile ); + wp_die( '' . __( 'Could not create zip archive.' ) . ' ' . esc_html( $archive->errorInfo( true ) ) . '
' ); + } + } + + if ( ! file_exists( $tmpfile ) ) { + @unlink( $tmpfile ); + wp_die( '' . __( 'Failed to create theme archive.' ) . '
' ); + } + + // Send the file to the browser. + header( 'Content-Type: application/zip' ); + $download_filename = sanitize_file_name( $zipname ) . '.zip'; + $header_filename = str_replace( array( '\\\\', '"' ), array( '\\\\\\\\', '\\"' ), $download_filename ); + header( 'Content-Disposition: attachment; filename="' . $header_filename . '"' ); + header( 'Content-Length: ' . filesize( $tmpfile ) ); + readfile( $tmpfile ); + // Best-effort cleanup of the temporary archive; failure to delete is non-critical. + @unlink( $tmpfile ); + exit; +} + $allowed_files = array(); $style_files = array(); @@ -415,6 +535,13 @@ + +