| | 141 | * Downloads a theme as a ZIP archive. |
| | 142 | * |
| | 143 | * @since 6.0.0 |
| | 144 | * |
| | 145 | * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. |
| | 146 | * |
| | 147 | * @param string $stylesheet Stylesheet of the theme to download. |
| | 148 | * @return bool|WP_Error True on success, WP_Error on failure. |
| | 149 | */ |
| | 150 | function download_theme( $stylesheet ) { |
| | 151 | global $wp_filesystem; |
| | 152 | |
| | 153 | if ( empty( $stylesheet ) ) { |
| | 154 | return new WP_Error( 'empty_stylesheet', __( 'Theme stylesheet is empty.' ) ); |
| | 155 | } |
| | 156 | |
| | 157 | $theme = wp_get_theme( $stylesheet ); |
| | 158 | |
| | 159 | if ( ! $theme->exists() ) { |
| | 160 | return new WP_Error( 'theme_not_found', __( 'Theme not found.' ) ); |
| | 161 | } |
| | 162 | |
| | 163 | // Initialize filesystem. |
| | 164 | if ( empty( $wp_filesystem ) ) { |
| | 165 | require_once ABSPATH . '/wp-admin/includes/file.php'; |
| | 166 | WP_Filesystem(); |
| | 167 | } |
| | 168 | |
| | 169 | $theme_root = get_theme_root( $stylesheet ); |
| | 170 | $theme_dir = $theme_root . '/' . $stylesheet; |
| | 171 | |
| | 172 | if ( ! $wp_filesystem->is_dir( $theme_dir ) ) { |
| | 173 | return new WP_Error( 'theme_dir_not_found', __( 'Theme directory not found.' ) ); |
| | 174 | } |
| | 175 | |
| | 176 | // Create temporary directory for ZIP file. |
| | 177 | $upload_dir = wp_upload_dir(); |
| | 178 | $temp_dir = $upload_dir['basedir'] . '/theme-downloads'; |
| | 179 | |
| | 180 | if ( ! $wp_filesystem->is_dir( $temp_dir ) ) { |
| | 181 | $wp_filesystem->mkdir( $temp_dir, FS_CHMOD_DIR ); |
| | 182 | } |
| | 183 | |
| | 184 | $zip_filename = sanitize_file_name( $stylesheet . '.zip' ); |
| | 185 | $zip_path = $temp_dir . '/' . $zip_filename; |
| | 186 | |
| | 187 | // Remove existing ZIP file if it exists. |
| | 188 | if ( $wp_filesystem->exists( $zip_path ) ) { |
| | 189 | $wp_filesystem->delete( $zip_path ); |
| | 190 | } |
| | 191 | |
| | 192 | // Get all theme files recursively. |
| | 193 | $files = list_files( $theme_dir ); |
| | 194 | |
| | 195 | if ( empty( $files ) ) { |
| | 196 | return new WP_Error( 'no_files', __( 'No files found in theme directory.' ) ); |
| | 197 | } |
| | 198 | |
| | 199 | // Create ZIP archive. |
| | 200 | if ( class_exists( 'ZipArchive' ) ) { |
| | 201 | $zip = new ZipArchive(); |
| | 202 | if ( $zip->open( $zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) !== true ) { |
| | 203 | return new WP_Error( 'zip_create_failed', __( 'Could not create ZIP archive.' ) ); |
| | 204 | } |
| | 205 | |
| | 206 | foreach ( $files as $file ) { |
| | 207 | if ( ! is_dir( $file ) ) { |
| | 208 | $relative_path = str_replace( $theme_dir . DIRECTORY_SEPARATOR, '', $file ); |
| | 209 | $relative_path = str_replace( '\\', '/', $relative_path ); // Normalize path separators. |
| | 210 | |
| | 211 | // Skip hidden files and directories. |
| | 212 | $path_parts = explode( '/', $relative_path ); |
| | 213 | $skip_file = false; |
| | 214 | foreach ( $path_parts as $part ) { |
| | 215 | if ( strpos( $part, '.' ) === 0 && $part !== '.' && $part !== '..' ) { |
| | 216 | $skip_file = true; |
| | 217 | break; |
| | 218 | } |
| | 219 | } |
| | 220 | |
| | 221 | if ( ! $skip_file ) { |
| | 222 | $zip->addFile( $file, $relative_path ); |
| | 223 | } |
| | 224 | } |
| | 225 | } |
| | 226 | |
| | 227 | $zip->close(); |
| | 228 | } else { |
| | 229 | // Fallback to PclZip if ZipArchive is not available. |
| | 230 | require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; |
| | 231 | $archive = new PclZip( $zip_path ); |
| | 232 | |
| | 233 | $file_list = array(); |
| | 234 | foreach ( $files as $file ) { |
| | 235 | if ( ! is_dir( $file ) ) { |
| | 236 | $relative_path = str_replace( $theme_dir . DIRECTORY_SEPARATOR, '', $file ); |
| | 237 | $relative_path = str_replace( '\\', '/', $relative_path ); // Normalize path separators. |
| | 238 | |
| | 239 | // Skip hidden files and directories. |
| | 240 | $path_parts = explode( '/', $relative_path ); |
| | 241 | $skip_file = false; |
| | 242 | foreach ( $path_parts as $part ) { |
| | 243 | if ( strpos( $part, '.' ) === 0 && $part !== '.' && $part !== '..' ) { |
| | 244 | $skip_file = true; |
| | 245 | break; |
| | 246 | } |
| | 247 | } |
| | 248 | |
| | 249 | if ( ! $skip_file ) { |
| | 250 | $file_list[] = array( |
| | 251 | PCLZIP_ATT_FILE_NAME => $file, |
| | 252 | PCLZIP_ATT_FILE_NEW_FULL_NAME => $relative_path, |
| | 253 | ); |
| | 254 | } |
| | 255 | } |
| | 256 | } |
| | 257 | |
| | 258 | if ( empty( $file_list ) || $archive->create( $file_list ) === 0 ) { |
| | 259 | return new WP_Error( 'zip_create_failed', __( 'Could not create ZIP archive.' ) ); |
| | 260 | } |
| | 261 | } |
| | 262 | |
| | 263 | // Send ZIP file to browser. |
| | 264 | if ( ! file_exists( $zip_path ) ) { |
| | 265 | return new WP_Error( 'zip_not_created', __( 'ZIP file was not created.' ) ); |
| | 266 | } |
| | 267 | |
| | 268 | // Clean output buffer. |
| | 269 | if ( ob_get_level() ) { |
| | 270 | ob_end_clean(); |
| | 271 | } |
| | 272 | |
| | 273 | // Set headers for download. |
| | 274 | header( 'Content-Type: application/zip' ); |
| | 275 | header( 'Content-Disposition: attachment; filename="' . $zip_filename . '"' ); |
| | 276 | header( 'Content-Length: ' . filesize( $zip_path ) ); |
| | 277 | header( 'Cache-Control: no-cache, must-revalidate' ); |
| | 278 | header( 'Expires: Sat, 26 Jul 1997 05:00:00 GMT' ); |
| | 279 | |
| | 280 | // Read and output file. |
| | 281 | readfile( $zip_path ); |
| | 282 | |
| | 283 | // Clean up temporary file. |
| | 284 | if ( file_exists( $zip_path ) ) { |
| | 285 | @unlink( $zip_path ); |
| | 286 | } |
| | 287 | |
| | 288 | exit; |
| | 289 | } |
| | 290 | |
| | 291 | /** |