From 55f37f638600cab1066cc0af888c8b694d0f7da8 Mon Sep 17 00:00:00 2001 From: noruzzamans Date: Wed, 14 Jan 2026 11:13:22 +0600 Subject: [PATCH 01/13] Trac 64439: Add theme download capability to theme editor --- src/wp-admin/theme-editor.php | 85 +++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 68d6f9a7e966a..a397d03b3f244 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -77,6 +77,81 @@ 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 (accept either _wpnonce or nonce parameter for compatibility). + $nonce_name = isset( $_REQUEST['_wpnonce'] ) ? '_wpnonce' : 'nonce'; + if ( ! check_admin_referer( 'download-theme_' . $stylesheet, $nonce_name, false ) ) { + wp_die( '

' . __( 'Security check failed.' ) . '

' ); + } + + $theme_dir = $theme->get_stylesheet_directory(); + + if ( ! is_dir( $theme_dir ) ) { + wp_die( '

' . __( 'Theme directory not found.' ) . '

' ); + } + + $zipname = sanitize_file_name( $theme->get( 'Name' ) ); + if ( ! $zipname ) { + $zipname = $stylesheet; + } + $tmpfile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $zipname . '-' . time() . '.zip'; + + // Try native ZipArchive first, fall back to PclZip if needed. + if ( class_exists( 'ZipArchive' ) ) { + $zip = new ZipArchive(); + if ( true !== $zip->open( $tmpfile, ZipArchive::CREATE ) ) { + wp_die( '

' . __( 'Could not create zip archive.' ) . '

' ); + } + + $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $theme_dir ) ); + foreach ( $files as $file ) { + if ( $file->isDir() ) { + continue; + } + $filePath = $file->getRealPath(); + $relativePath = substr( $filePath, strlen( $theme_dir ) + 1 ); + $zip->addFile( $filePath, $relativePath ); + } + + $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 ) ); + foreach ( $files as $file ) { + if ( $file->isDir() ) { + continue; + } + $filelist[] = $file->getRealPath(); + } + + $archive = new PclZip( $tmpfile ); + $result = $archive->create( $filelist, PCLZIP_OPT_REMOVE_PATH, $theme_dir ); + if ( 0 === $result ) { + wp_die( '

' . __( 'Could not create zip archive.' ) . ' ' . esc_html( $archive->errorInfo( true ) ) . '

' ); + } + } + + if ( ! file_exists( $tmpfile ) ) { + wp_die( '

' . __( 'Failed to create theme archive.' ) . '

' ); + } + + // Send the file to the browser. + header( 'Content-Type: application/zip' ); + header( 'Content-Disposition: attachment; filename="' . $zipname . '.zip"' ); + header( 'Content-Length: ' . filesize( $tmpfile ) ); + readfile( $tmpfile ); + @unlink( $tmpfile ); + exit; +} + $allowed_files = array(); $style_files = array(); @@ -304,6 +379,8 @@ + +
@@ -415,6 +492,14 @@ + + +
+ + + + +
From 20294310b18257e355f792609863a8bd39c0d64b Mon Sep 17 00:00:00 2001 From: noruzzamans Date: Wed, 14 Jan 2026 12:10:21 +0600 Subject: [PATCH 02/13] Trac 64439: Use theme slug and version for zip filename Updates the downloaded zip filename to follow the 'slug.version.zip' format (e.g., 'twentytwentyfour.1.0.zip'), aligning with WordPress standards and improving file organization. --- src/wp-admin/theme-editor.php | 172 +++++++++++++++++----------------- 1 file changed, 87 insertions(+), 85 deletions(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index a397d03b3f244..3172aca1c42ee 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -77,81 +77,83 @@ 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 (accept either _wpnonce or nonce parameter for compatibility). - $nonce_name = isset( $_REQUEST['_wpnonce'] ) ? '_wpnonce' : 'nonce'; - if ( ! check_admin_referer( 'download-theme_' . $stylesheet, $nonce_name, false ) ) { - wp_die( '

' . __( 'Security check failed.' ) . '

' ); - } - - $theme_dir = $theme->get_stylesheet_directory(); - - if ( ! is_dir( $theme_dir ) ) { - wp_die( '

' . __( 'Theme directory not found.' ) . '

' ); - } - - $zipname = sanitize_file_name( $theme->get( 'Name' ) ); - if ( ! $zipname ) { - $zipname = $stylesheet; - } - $tmpfile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $zipname . '-' . time() . '.zip'; - - // Try native ZipArchive first, fall back to PclZip if needed. - if ( class_exists( 'ZipArchive' ) ) { - $zip = new ZipArchive(); - if ( true !== $zip->open( $tmpfile, ZipArchive::CREATE ) ) { - wp_die( '

' . __( 'Could not create zip archive.' ) . '

' ); - } - - $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $theme_dir ) ); - foreach ( $files as $file ) { - if ( $file->isDir() ) { - continue; - } - $filePath = $file->getRealPath(); - $relativePath = substr( $filePath, strlen( $theme_dir ) + 1 ); - $zip->addFile( $filePath, $relativePath ); - } - - $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 ) ); - foreach ( $files as $file ) { - if ( $file->isDir() ) { - continue; - } - $filelist[] = $file->getRealPath(); - } - - $archive = new PclZip( $tmpfile ); - $result = $archive->create( $filelist, PCLZIP_OPT_REMOVE_PATH, $theme_dir ); - if ( 0 === $result ) { - wp_die( '

' . __( 'Could not create zip archive.' ) . ' ' . esc_html( $archive->errorInfo( true ) ) . '

' ); - } - } - - if ( ! file_exists( $tmpfile ) ) { - wp_die( '

' . __( 'Failed to create theme archive.' ) . '

' ); - } - - // Send the file to the browser. - header( 'Content-Type: application/zip' ); - header( 'Content-Disposition: attachment; filename="' . $zipname . '.zip"' ); - header( 'Content-Length: ' . filesize( $tmpfile ) ); - readfile( $tmpfile ); - @unlink( $tmpfile ); - exit; -} - +// 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 (accept either _wpnonce or nonce parameter for compatibility). + $nonce_name = isset( $_REQUEST['_wpnonce'] ) ? '_wpnonce' : 'nonce'; + if ( ! check_admin_referer( 'download-theme_' . $stylesheet, $nonce_name, false ) ) { + 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 = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $zipname . '-' . time() . '.zip'; + + // Try native ZipArchive first, fall back to PclZip if needed. + if ( class_exists( 'ZipArchive' ) ) { + $zip = new ZipArchive(); + if ( true !== $zip->open( $tmpfile, ZipArchive::CREATE ) ) { + wp_die( '

' . __( 'Could not create zip archive.' ) . '

' ); + } + + $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $theme_dir ) ); + foreach ( $files as $file ) { + if ( $file->isDir() ) { + continue; + } + $filePath = $file->getRealPath(); + $relativePath = substr( $filePath, strlen( $theme_dir ) + 1 ); + $zip->addFile( $filePath, $relativePath ); + } + + $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 ) ); + foreach ( $files as $file ) { + if ( $file->isDir() ) { + continue; + } + $filelist[] = $file->getRealPath(); + } + + $archive = new PclZip( $tmpfile ); + $result = $archive->create( $filelist, PCLZIP_OPT_REMOVE_PATH, $theme_dir ); + if ( 0 === $result ) { + wp_die( '

' . __( 'Could not create zip archive.' ) . ' ' . esc_html( $archive->errorInfo( true ) ) . '

' ); + } + } + + if ( ! file_exists( $tmpfile ) ) { + wp_die( '

' . __( 'Failed to create theme archive.' ) . '

' ); + } + + // Send the file to the browser. + header( 'Content-Type: application/zip' ); + header( 'Content-Disposition: attachment; filename="' . $zipname . '.zip"' ); + header( 'Content-Length: ' . filesize( $tmpfile ) ); + readfile( $tmpfile ); + @unlink( $tmpfile ); + exit; +} + $allowed_files = array(); $style_files = array(); @@ -379,8 +381,8 @@ - - + +
@@ -492,14 +494,14 @@ - - -
- - - - -
+ + +
+ + + + +
From 5571e67e1d3d016b1668774f86387b54d64458a5 Mon Sep 17 00:00:00 2001 From: noruzzamans Date: Wed, 14 Jan 2026 12:42:12 +0600 Subject: [PATCH 03/13] Trac 64439: Ensure zip archive contains theme directory Fixes the directory structure within the downloaded zip file. Previously, files were added to the root of the zip. This change ensures all files are nested within a folder named after the theme slug (stylesheet), adhering to standard theme packaging conventions. --- src/wp-admin/theme-editor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 3172aca1c42ee..72c0a586aaa3a 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -117,7 +117,7 @@ } $filePath = $file->getRealPath(); $relativePath = substr( $filePath, strlen( $theme_dir ) + 1 ); - $zip->addFile( $filePath, $relativePath ); + $zip->addFile( $filePath, $stylesheet . '/' . $relativePath ); } $zip->close(); @@ -135,7 +135,7 @@ } $archive = new PclZip( $tmpfile ); - $result = $archive->create( $filelist, PCLZIP_OPT_REMOVE_PATH, $theme_dir ); + $result = $archive->create( $filelist, PCLZIP_OPT_REMOVE_PATH, $theme_dir, PCLZIP_OPT_ADD_PATH, $stylesheet ); if ( 0 === $result ) { wp_die( '

' . __( 'Could not create zip archive.' ) . ' ' . esc_html( $archive->errorInfo( true ) ) . '

' ); } @@ -557,4 +557,4 @@ Date: Thu, 15 Jan 2026 10:16:46 +0600 Subject: [PATCH 04/13] Update src/wp-admin/theme-editor.php Co-authored-by: Weston Ruter --- src/wp-admin/theme-editor.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 72c0a586aaa3a..10e79c1f3ff76 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -381,8 +381,6 @@ - -
From 8a7c128246d135c22d97e846a10c23d620ed54bc Mon Sep 17 00:00:00 2001 From: Noruzzaman Date: Thu, 15 Jan 2026 10:17:27 +0600 Subject: [PATCH 05/13] Update src/wp-admin/theme-editor.php Co-authored-by: Weston Ruter --- src/wp-admin/theme-editor.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 10e79c1f3ff76..8780b3b89644c 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -493,7 +493,6 @@ -
From e3e909d07879d5cc4370ea8fcdfb3589d1e682ba Mon Sep 17 00:00:00 2001 From: Noruzzaman Date: Thu, 15 Jan 2026 10:18:04 +0600 Subject: [PATCH 06/13] Update src/wp-admin/theme-editor.php Co-authored-by: Weston Ruter --- src/wp-admin/theme-editor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 8780b3b89644c..36892b5895591 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -494,7 +494,7 @@
- + From b260e24eab1a060d9a4426183333e3afae39e6e7 Mon Sep 17 00:00:00 2001 From: Noruzzaman Date: Thu, 15 Jan 2026 10:18:42 +0600 Subject: [PATCH 07/13] Update src/wp-admin/theme-editor.php Co-authored-by: Weston Ruter --- src/wp-admin/theme-editor.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 36892b5895591..62f8a5c4eda76 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -84,8 +84,7 @@ } // Verify nonce (accept either _wpnonce or nonce parameter for compatibility). - $nonce_name = isset( $_REQUEST['_wpnonce'] ) ? '_wpnonce' : 'nonce'; - if ( ! check_admin_referer( 'download-theme_' . $stylesheet, $nonce_name, false ) ) { + if ( ! check_admin_referer( 'download-theme_' . $stylesheet ) ) { wp_die( '

' . __( 'Security check failed.' ) . '

' ); } From 0b5b47626a9b0ad18ed3382eff510bab42aa5881 Mon Sep 17 00:00:00 2001 From: noruzzamans Date: Thu, 15 Jan 2026 10:35:28 +0600 Subject: [PATCH 08/13] Theme Editor: Address code review feedback - Replace inline style on download form with CSS class. - Restore newline at end of file. --- src/wp-admin/css/common.css | 4 ++++ src/wp-admin/theme-editor.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 62f8a5c4eda76..6aab97120ad45 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -492,7 +492,7 @@ -
+ @@ -553,4 +553,4 @@ Date: Thu, 15 Jan 2026 13:58:17 +0600 Subject: [PATCH 09/13] Update src/wp-admin/theme-editor.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-admin/theme-editor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 6aab97120ad45..bd660ca6c6404 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -100,7 +100,7 @@ $zipname .= '.' . $version; } - $tmpfile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $zipname . '-' . time() . '.zip'; + $tmpfile = get_temp_dir() . DIRECTORY_SEPARATOR . $zipname . '-' . time() . '.zip'; // Try native ZipArchive first, fall back to PclZip if needed. if ( class_exists( 'ZipArchive' ) ) { From 458f3a8f1e2ebf03b8bcfb8e0f3cefe4080214ac Mon Sep 17 00:00:00 2001 From: Noruzzaman Date: Thu, 15 Jan 2026 13:58:45 +0600 Subject: [PATCH 10/13] Update src/wp-admin/theme-editor.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-admin/theme-editor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index bd660ca6c6404..5d31e7ae470b4 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -83,7 +83,7 @@ wp_die( '

' . __( 'Sorry, you are not allowed to download themes for this site.' ) . '

' ); } - // Verify nonce (accept either _wpnonce or nonce parameter for compatibility). + // Verify nonce. if ( ! check_admin_referer( 'download-theme_' . $stylesheet ) ) { wp_die( '

' . __( 'Security check failed.' ) . '

' ); } From fa4a3088dd5bb993a259b621e8ec919c3df5b4e7 Mon Sep 17 00:00:00 2001 From: Noruzzaman Date: Thu, 15 Jan 2026 13:59:14 +0600 Subject: [PATCH 11/13] Update src/wp-admin/theme-editor.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-admin/theme-editor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 5d31e7ae470b4..92cb6736e4478 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -114,9 +114,9 @@ if ( $file->isDir() ) { continue; } - $filePath = $file->getRealPath(); - $relativePath = substr( $filePath, strlen( $theme_dir ) + 1 ); - $zip->addFile( $filePath, $stylesheet . '/' . $relativePath ); + $file_path = $file->getRealPath(); + $relative_path = substr( $file_path, strlen( $theme_dir ) + 1 ); + $zip->addFile( $file_path, $stylesheet . '/' . $relative_path ); } $zip->close(); From 011801ea8fb0ee4ecd23fc4b5ca9c55f9a9167ab Mon Sep 17 00:00:00 2001 From: Noruzzaman Date: Thu, 15 Jan 2026 13:59:44 +0600 Subject: [PATCH 12/13] Update src/wp-admin/theme-editor.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-admin/theme-editor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 92cb6736e4478..e58aae6458aac 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -149,6 +149,7 @@ header( 'Content-Disposition: attachment; filename="' . $zipname . '.zip"' ); header( 'Content-Length: ' . filesize( $tmpfile ) ); readfile( $tmpfile ); + // Best-effort cleanup of the temporary archive; failure to delete is non-critical. @unlink( $tmpfile ); exit; } From 7c48a79a3539979caef8363e83892a0014d7ddd3 Mon Sep 17 00:00:00 2001 From: noruzzamans Date: Thu, 15 Jan 2026 15:59:18 +0600 Subject: [PATCH 13/13] Address Copilot feedback: improve file iteration and sanitize zip filename --- src/wp-admin/theme-editor.php | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index e58aae6458aac..aedd3213688f3 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -109,11 +109,13 @@ wp_die( '

' . __( 'Could not create zip archive.' ) . '

' ); } - $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $theme_dir ) ); + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $theme_dir, FilesystemIterator::SKIP_DOTS ), + RecursiveIteratorIterator::LEAVES_ONLY + ); + foreach ( $files as $file ) { - if ( $file->isDir() ) { - continue; - } + // With SKIP_DOTS and LEAVES_ONLY, we don't need to check isDir(). $file_path = $file->getRealPath(); $relative_path = substr( $file_path, strlen( $theme_dir ) + 1 ); $zip->addFile( $file_path, $stylesheet . '/' . $relative_path ); @@ -125,16 +127,17 @@ require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; $filelist = array(); - $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $theme_dir ) ); + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $theme_dir, FilesystemIterator::SKIP_DOTS ), + RecursiveIteratorIterator::LEAVES_ONLY + ); + foreach ( $files as $file ) { - if ( $file->isDir() ) { - continue; - } $filelist[] = $file->getRealPath(); } $archive = new PclZip( $tmpfile ); - $result = $archive->create( $filelist, PCLZIP_OPT_REMOVE_PATH, $theme_dir, PCLZIP_OPT_ADD_PATH, $stylesheet ); + $result = $archive->create( $filelist, PCLZIP_OPT_REMOVE_PATH, $theme_dir, PCLZIP_OPT_ADD_PATH, $stylesheet ); if ( 0 === $result ) { wp_die( '

' . __( 'Could not create zip archive.' ) . ' ' . esc_html( $archive->errorInfo( true ) ) . '

' ); } @@ -146,7 +149,7 @@ // Send the file to the browser. header( 'Content-Type: application/zip' ); - header( 'Content-Disposition: attachment; filename="' . $zipname . '.zip"' ); + header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $zipname ) . '.zip"' ); header( 'Content-Length: ' . filesize( $tmpfile ) ); readfile( $tmpfile ); // Best-effort cleanup of the temporary archive; failure to delete is non-critical.