-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Trac 64439: Add theme download capability to Theme Editor #10731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Conversation
|
Hi @noruzzamans! 👋 Thank you for your contribution to WordPress! 💖 It looks like this is your first pull request to No one monitors this repository for new pull requests. Pull requests must be attached to a Trac ticket to be considered for inclusion in WordPress Core. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description. Pull requests are never merged on GitHub. The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making. More information about how GitHub pull requests can be used to contribute to WordPress can be found in the Core Handbook. Please include automated tests. Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the Automated Testing page in the handbook. If you have not had a chance, please review the Contribute with Code page in the WordPress Core Handbook. The Developer Hub also documents the various coding standards that are followed:
Thank you, |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
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.
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.
Co-authored-by: Weston Ruter <[email protected]>
Co-authored-by: Weston Ruter <[email protected]>
Co-authored-by: Weston Ruter <[email protected]>
Co-authored-by: Weston Ruter <[email protected]>
- Replace inline style on download form with CSS class. - Restore newline at end of file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request introduces a theme download feature to the WordPress Theme File Editor, allowing users to download the currently selected theme as a ZIP archive. The implementation provides both a UI button and a secure backend handler.
Changes:
- Added a "Download Theme" button below the theme editor form
- Implemented secure backend logic to create ZIP archives using ZipArchive (with PclZip fallback)
- Added minimal styling for the download form
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| src/wp-admin/theme-editor.php | Added download handler with permission checks, nonce verification, and ZIP generation logic; added download form UI |
| src/wp-admin/css/common.css | Added minimal styling for the download theme form |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
|
@westonruter Thanks for the review. I have addressed the feedback:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $file_path = $file->getRealPath(); | ||
| $relative_path = substr( $file_path, strlen( $theme_dir ) + 1 ); | ||
| $zip->addFile( $file_path, $stylesheet . '/' . $relative_path ); |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of getRealPath() without checking for failure could allow symlinks to be followed outside the theme directory, creating a security risk. If getRealPath() returns false (which can happen for broken symlinks), this will cause issues. Additionally, symlinks pointing outside the theme directory could be included in the archive. Consider checking the return value of getRealPath() and validating that the resolved path is still within the theme directory using a path containment check (e.g., checking if the resolved path starts with the theme directory path).
| $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 ); | ||
| } | ||
|
|
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the ZipArchive creation or file addition fails silently (e.g., due to permissions issues or disk space), the code continues to line 146 without checking if files were actually added to the archive. This could result in downloading an empty or incomplete zip file. Consider checking the number of files added to the archive or validating the zip file size before sending it to the user.
| $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 ); | |
| } | |
| $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(); | |
| $relative_path = substr( $file_path, strlen( $theme_dir ) + 1 ); | |
| if ( ! $zip->addFile( $file_path, $stylesheet . '/' . $relative_path ) ) { | |
| $zip->close(); | |
| wp_die( '<p>' . __( 'Could not create zip archive.' ) . '</p>' ); | |
| } | |
| $files_added++; | |
| } | |
| if ( 0 === $files_added ) { | |
| $zip->close(); | |
| wp_die( '<p>' . __( 'Could not create zip archive.' ) . '</p>' ); | |
| } |
| } | ||
|
|
||
| $tmpfile = get_temp_dir() . DIRECTORY_SEPARATOR . $zipname . '-' . time() . '.zip'; | ||
|
|
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For themes with a large number of files or large file sizes, the zip creation process could exceed PHP's default execution time limit, causing the download to fail. Other WordPress operations that handle potentially long-running tasks (like updates and exports) use set_time_limit() to extend the execution time. Consider adding a time limit extension before starting the zip creation process to ensure large themes can be downloaded successfully.
| // Attempt to extend execution time to allow large theme archives to be created. | |
| if ( function_exists( 'set_time_limit' ) ) { | |
| @set_time_limit( 300 ); | |
| } |
| 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>' ); | ||
| } |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an error occurs during zip creation (lines 108-143), the temporary file may be left on the server because there's no cleanup in the error paths. The wp_die() calls at lines 109 and 142 exit without deleting the partially created zip file. Consider adding cleanup code before wp_die() calls or using a try-finally pattern to ensure temporary files are always removed, even on error.
| ); | ||
|
|
||
| foreach ( $files as $file ) { | ||
| $filelist[] = $file->getRealPath(); |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of getRealPath() without checking for failure could cause issues if it returns false (which can happen for broken symlinks). Additionally, symlinks pointing outside the theme directory could be included in the archive, creating a security risk. Consider checking the return value of getRealPath() and validating that the resolved path is still within the theme directory using a path containment check.
|
|
||
| // Send the file to the browser. | ||
| header( 'Content-Type: application/zip' ); | ||
| header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $zipname ) . '.zip"' ); |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Content-Disposition header is missing proper quotes escaping. While sanitize_file_name() removes most problematic characters, the filename is already wrapped in double quotes in the header. If the sanitized filename contains a quote character or other special characters that survive sanitization, it could break the header format. Consider using RFC 6266 compliant filename encoding or additional escaping to ensure the header is properly formatted.
| header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $zipname ) . '.zip"' ); | |
| $download_filename = sanitize_file_name( $zipname ) . '.zip'; | |
| $header_filename = str_replace( array( '\\\\', '"' ), array( '\\\\\\\\', '\\"' ), $download_filename ); | |
| header( 'Content-Disposition: attachment; filename="' . $header_filename . '"' ); |
| // 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 ); |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ZipArchive::addFile() method can fail silently if a file is not readable or if there are permission issues, but there's no error checking after each file is added. This means some files could be skipped without any indication to the user. Consider checking the return value of addFile() and either logging errors or tracking the count of successfully added files to ensure the archive is complete.
| $zip->addFile( $file_path, $stylesheet . '/' . $relative_path ); | |
| if ( true !== $zip->addFile( $file_path, $stylesheet . '/' . $relative_path ) ) { | |
| $zip->close(); | |
| // Remove partially created archive. | |
| @unlink( $tmpfile ); | |
| wp_die( | |
| '<p>' . sprintf( | |
| /* translators: %s: File path. */ | |
| __( 'Could not add file to zip archive: %s' ), | |
| esc_html( $relative_path ) | |
| ) . '</p>' | |
| ); | |
| } |
Description
This Pull Request introduces the ability to download the currently selected theme as a ZIP file directly from the Appearance > Theme File Editor screen. This feature allows users to easily create a backup or copy of the theme they are editing without needing FTP or file manager access.
Ticket
https://core.trac.wordpress.org/ticket/64439
Acknowledgements
Props to @solankisoftware for the initial patch.
Thanks for the contribution! I noticed the file path in the original patch was slightly incorrect for the development environment (it was missing
src/), so I have corrected the path to ensure it applies cleanly to thewordpress-developrepository.Changes
download_themeaction.ZipArchive(with a fallback toPclZipfor compatibility).edit_themes) and nonce verification before processing the download.Testing Instructions
.zipfile containing the full theme directory is downloaded successfully.