Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
87 changes: 87 additions & 0 deletions src/wp-admin/theme-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,86 @@
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';

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

$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $theme_dir, FilesystemIterator::SKIP_DOTS ),
RecursiveIteratorIterator::LEAVES_ONLY
);

foreach ( $files as $file ) {
// 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 );
}

$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 ) {
$filelist[] = $file->getRealPath();
}

$archive = new PclZip( $tmpfile );
$result = $archive->create( $filelist, PCLZIP_OPT_REMOVE_PATH, $theme_dir, PCLZIP_OPT_ADD_PATH, $stylesheet );
if ( 0 === $result ) {
wp_die( '<p>' . __( 'Could not create zip archive.' ) . ' ' . esc_html( $archive->errorInfo( true ) ) . '</p>' );
}
}

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

// Send the file to the browser.
header( 'Content-Type: application/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.
@unlink( $tmpfile );
exit;
}

$allowed_files = array();
$style_files = array();

Expand Down Expand Up @@ -415,6 +495,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