Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
55f37f6
Trac 64439: Add theme download capability to theme editor
noruzzamans Jan 14, 2026
2029431
Trac 64439: Use theme slug and version for zip filename
noruzzamans Jan 14, 2026
5571e67
Trac 64439: Ensure zip archive contains theme directory
noruzzamans Jan 14, 2026
1c6e301
Update src/wp-admin/theme-editor.php
noruzzamans Jan 15, 2026
8a7c128
Update src/wp-admin/theme-editor.php
noruzzamans Jan 15, 2026
e3e909d
Update src/wp-admin/theme-editor.php
noruzzamans Jan 15, 2026
b260e24
Update src/wp-admin/theme-editor.php
noruzzamans Jan 15, 2026
0b5b476
Theme Editor: Address code review feedback
noruzzamans Jan 15, 2026
001683d
Update src/wp-admin/theme-editor.php
noruzzamans Jan 15, 2026
458f3a8
Update src/wp-admin/theme-editor.php
noruzzamans Jan 15, 2026
fa4a308
Update src/wp-admin/theme-editor.php
noruzzamans Jan 15, 2026
011801e
Update src/wp-admin/theme-editor.php
noruzzamans Jan 15, 2026
945ae0e
Merge branch 'trunk' into trac-64439-theme-editor
noruzzamans Jan 15, 2026
7c48a79
Address Copilot feedback: improve file iteration and sanitize zip fil…
noruzzamans Jan 15, 2026
a35ab57
Update src/wp-admin/theme-editor.php
noruzzamans Jan 19, 2026
8f532f7
Update src/wp-admin/theme-editor.php
noruzzamans Jan 19, 2026
723bc35
Update src/wp-admin/theme-editor.php
noruzzamans Jan 19, 2026
3fa369b
Admin: Implement security, cleanup, and reliability fixes for Downloa…
noruzzamans Jan 19, 2026
868b87f
Merge branch 'trunk' into trac-64439-theme-editor
noruzzamans Jan 19, 2026
d74bd6c
Merge branch 'trunk' into trac-64439-theme-editor
noruzzamans Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/wp-admin/css/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
127 changes: 127 additions & 0 deletions src/wp-admin/theme-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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( '<p>' . __( 'Sorry, you are not allowed to download themes for this site.' ) . '</p>' );
}

// Verify nonce.
if ( ! check_admin_referer( 'download-theme_' . $stylesheet ) ) {
wp_die( '<p>' . __( 'Security check failed.' ) . '</p>' );
}

$theme_dir = $theme->get_stylesheet_directory();

if ( ! is_dir( $theme_dir ) ) {
wp_die( '<p>' . __( 'Theme directory not found.' ) . '</p>' );
}

$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( '<p>' . __( 'Could not create zip archive.' ) . '</p>' );
}

$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( '<p>' . __( 'Could not create zip archive.' ) . '</p>' );
}

$files_added++;
}

if ( 0 === $files_added ) {
$zip->close();
@unlink( $tmpfile );
wp_die( '<p>' . __( 'No files found to compress.' ) . '</p>' );
}
$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( '<p>' . __( 'No files found to compress.' ) . '</p>' );
}

$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( '<p>' . __( 'Could not create zip archive.' ) . ' ' . esc_html( $archive->errorInfo( true ) ) . '</p>' );
}
}

if ( ! file_exists( $tmpfile ) ) {
@unlink( $tmpfile );
wp_die( '<p>' . __( 'Failed to create theme archive.' ) . '</p>' );
}

// 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();

Expand Down Expand Up @@ -415,6 +535,13 @@

<?php wp_print_file_editor_templates(); ?>
</form>

<form action="theme-editor.php" method="post" class="download-theme-form">
<?php wp_nonce_field( 'download-theme_' . $stylesheet ); ?>
<input type="hidden" name="action" value="download_theme" />
<input type="hidden" name="theme" value="<?php echo esc_attr( $stylesheet ); ?>" />
<?php submit_button( __( 'Download Theme' ), 'secondary', '', false ); ?>
</form>
<?php
endif; // End if $error.
?>
Expand Down
Loading