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..aedd3213688f3 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -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( '

' . __( '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'; + + // 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, 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( '

' . __( '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="' . 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(); @@ -415,6 +495,13 @@ + +
+ + + + +