Make WordPress Core

Ticket #64439: theme-admin-download.patch

File theme-admin-download.patch, 16.7 KB (added by solankisoftware, 3 months ago)
  • wp-admin/includes/theme.php

     
    138138}
    139139
    140140/**
     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 */
     150function 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/**
    141292 * Gets the page templates available in this theme.
    142293 *
    143294 * @since 1.5.0
     
    791942                                'activate'   => current_user_can( 'switch_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=activate&stylesheet=' . $encoded_slug ), 'switch-theme_' . $slug ) : null,
    792943                                'customize'  => $customize_action,
    793944                                'delete'     => ( ! is_multisite() && current_user_can( 'delete_themes' ) ) ? wp_nonce_url( admin_url( 'themes.php?action=delete&stylesheet=' . $encoded_slug ), 'delete-theme_' . $slug ) : null,
     945                                'download'   => current_user_can( 'switch_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=download&stylesheet=' . $encoded_slug ), 'download-theme_' . $slug ) : null,
    794946                                'autoupdate' => wp_is_auto_update_enabled_for_type( 'theme' ) && ! is_multisite() && current_user_can( 'update_themes' )
    795947                                        ? wp_nonce_url( admin_url( 'themes.php?action=' . $auto_update_action . '&stylesheet=' . $encoded_slug ), 'updates' )
    796948                                        : null,
  • wp-admin/themes.php

     
    120120                wp_redirect( admin_url( 'themes.php?disabled-auto-update=true' ) );
    121121
    122122                exit;
     123        } elseif ( 'download' === $_GET['action'] ) {
     124                check_admin_referer( 'download-theme_' . $_GET['stylesheet'] );
     125                $theme = wp_get_theme( $_GET['stylesheet'] );
     126
     127                if ( ! $theme->exists() || ! $theme->is_allowed() ) {
     128                        wp_die(
     129                                '<h1>' . __( 'An error occurred.' ) . '</h1>' .
     130                                '<p>' . __( 'The requested theme does not exist.' ) . '</p>',
     131                                403
     132                        );
     133                }
     134
     135                if ( ! current_user_can( 'switch_themes' ) ) {
     136                        wp_die(
     137                                '<h1>' . __( 'You need a higher level of permission.' ) . '</h1>' .
     138                                '<p>' . __( 'Sorry, you are not allowed to download themes.' ) . '</p>',
     139                                403
     140                        );
     141                }
     142
     143                $result = download_theme( $_GET['stylesheet'] );
     144
     145                if ( is_wp_error( $result ) ) {
     146                        wp_die( $result );
     147                }
     148
     149                // download_theme() handles the file output and exit.
    123150        }
    124151}
    125152
     
    655682                        <?php } ?>
    656683                <?php } ?>
    657684
     685                <?php if ( ! empty( $theme['actions']['download'] ) ) { ?>
     686                        <?php
     687                        /* translators: %s: Theme name. */
     688                        $download_aria_label = sprintf( _x( 'Download %s', 'theme' ), $theme['name'] );
     689                        ?>
     690                        <a class="button download-theme"
     691                                href="<?php echo esc_url( $theme['actions']['download'] ); ?>"
     692                                aria-label="<?php echo esc_attr( $download_aria_label ); ?>"
     693                        ><?php _e( 'Download' ); ?></a>
     694                <?php } ?>
     695
    658696                </div>
    659697        </div>
    660698</div>
     
    10431081                                        ><?php _e( 'Live Preview' ); ?></a>
    10441082                                <# } #>
    10451083                        <# } #>
     1084
     1085                        <# if ( data.actions.download ) { #>
     1086                                <?php
     1087                                /* translators: %s: Theme name. */
     1088                                $download_aria_label = sprintf( _x( 'Download %s', 'theme' ), '{{ data.name }}' );
     1089                                ?>
     1090                                <a class="button download-theme"
     1091                                        href="{{{ data.actions.download }}}"
     1092                                        aria-label="<?php echo esc_attr( $download_aria_label ); ?>"
     1093                                ><?php _e( 'Download' ); ?></a>
     1094                        <# } #>
    10461095                </div>
    10471096        </div>
    10481097</script>
     
    13091358                                        aria-label="<?php echo esc_attr( $aria_label ); ?>"
    13101359                                ><?php _e( 'Delete' ); ?></a>
    13111360                        <# } #>
     1361
     1362                        <# if ( data.actions.download ) { #>
     1363                                <?php
     1364                                /* translators: %s: Theme name. */
     1365                                $download_aria_label = sprintf( _x( 'Download %s', 'theme' ), '{{ data.name }}' );
     1366                                ?>
     1367                                <a class="button download-theme"
     1368                                        href="{{{ data.actions.download }}}"
     1369                                        aria-label="<?php echo esc_attr( $download_aria_label ); ?>"
     1370                                ><?php _e( 'Download' ); ?></a>
     1371                        <# } #>
    13121372                </div>
    13131373        </div>
    13141374</script>