Home / Admin / WPCode Snippet: Download All Snippets Button
Duplicate Snippet

Embed Snippet on Your Site

WPCode Snippet: Download All Snippets Button

* Description: Adds bulk download button to WPCode Snippets Sync admin page
* Location: Admin Only
* Priority: 20
*
* INSTALL ORDER: #5 - Install after sync system is fully set up
*
* REQUIRES:
* - WPCode Snippets Sync CPT Registration (Priority 5)
* - WPCode Snippets Sync Engine (Priority 10)
* - PHP ZipArchive extension (standard on most hosts)
*
* SECURITY (WPCS Compliant):
* - Capability check FIRST (current_user_can) - per Patchstack/WPCS best practice
* - Nonce verification via check_ajax_referer() - standard for AJAX handlers
* - Uses wp_nonce_url() for URL nonce generation (standard _wpnonce param)
* - Uses get_temp_dir() for WordPress-standard temp location
* - Directory traversal prevention via realpath() validation
* - PHP 8.2+ compatible (ZipArchive::OVERWRITE flag)
* - Auto-cleanup of temp files after download (per WP VIP guidelines)
*
* @version 1.1.0
* @since 2025-01-02
*/

ismail daugherty PRO
<10
Code Preview
php
<?php
/**
 * WPCode Snippet: Download All Snippets Button
 * Description: Adds bulk download button to WPCode Snippets Sync admin page
 * Location: Admin Only
 * Priority: 20
 * 
 * INSTALL ORDER: #5 - Install after sync system is fully set up
 * 
 * REQUIRES: 
 *   - WPCode Snippets Sync CPT Registration (Priority 5)
 *   - WPCode Snippets Sync Engine (Priority 10)
 *   - PHP ZipArchive extension (standard on most hosts)
 * 
 * SECURITY (WPCS Compliant):
 *   - Capability check FIRST (current_user_can) - per Patchstack/WPCS best practice
 *   - Nonce verification via check_ajax_referer() - standard for AJAX handlers
 *   - Uses wp_nonce_url() for URL nonce generation (standard _wpnonce param)
 *   - Uses get_temp_dir() for WordPress-standard temp location
 *   - Directory traversal prevention via realpath() validation
 *   - PHP 8.2+ compatible (ZipArchive::OVERWRITE flag)
 *   - Auto-cleanup of temp files after download (per WP VIP guidelines)
 * 
 * v1.5.1 BUGFIX:
 *   - Added 'folder_name' to files_data array in kic_scan_mu_subfolders()
 *   - Fixes "Undefined array key folder_name" error in MU-Plugins Sync Engine
 *   - Both snippets now have consistent data structures
 * 
 * v1.5.0 UPDATES:
 *   - Now scans ALL mu-plugins subfolders (not just "shortcode" folders)
 *   - Renamed function: kic_scan_shortcode_folders() -> kic_scan_mu_subfolders()
 *   - UI label changed from "shortcode files" to "subfolder files"
 * 
 * v1.4.0 UPDATES:
 *   - Scans mu-plugins subfolders containing "shortcode" in name
 *   - Includes all .php files from shortcode folders (skips subdirs like OLD/)
 *   - Preserves folder hierarchy: mu-plugins-subfolders/folder-name/file.php
 *   - Shows shortcode folder counts in admin notice
 * 
 * v1.3.0 UPDATES:
 *   - Filenames now include -active or -inactive suffix
 *   - Shows active/inactive counts in button notice
 *   - MU-plugins always marked as active (they run automatically)
 * 
 * v1.2.0 UPDATES:
 *   - Now includes MU-plugins (source_type = 'mu-plugin')
 *   - Creates files from snippet_code field when no file attached
 *   - Organizes into wpcode/ and mu-plugins/ folders in ZIP
 *   - Shows separate counts for WPCode vs MU-plugins
 * 
 * @version 1.5.1
 * @since 2025-01-02
 * @updated 2025-01-13 - Now includes ALL mu-plugins subfolders
 */
defined( 'ABSPATH' ) || exit;
/**
 * Scan mu-plugins directory for ALL subfolders
 * Returns array of folder info with their PHP files
 * Skips nested subdirectories (like OLD/) - only gets root-level .php files in each folder
 * 
 * @return array Array of subfolder data
 */
if ( ! function_exists( 'kic_scan_mu_subfolders' ) ) {
function kic_scan_mu_subfolders() {
    $mu_plugins_dir = WPMU_PLUGIN_DIR;
    $subfolders = array();
    
    if ( ! is_dir( $mu_plugins_dir ) ) {
        return $subfolders;
    }
    
    // Scan for subdirectories
    $directories = glob( trailingslashit( $mu_plugins_dir ) . '*', GLOB_ONLYDIR );
    
    if ( empty( $directories ) ) {
        return $subfolders;
    }
    
    foreach ( $directories as $dir_path ) {
        $folder_name = basename( $dir_path );
        
        // Get all PHP files in this folder (not recursive into subdirs like OLD/)
        $php_files = glob( trailingslashit( $dir_path ) . '*.php' );
        
        if ( empty( $php_files ) ) {
            continue;
        }
        
        $files_data = array();
        foreach ( $php_files as $file_path ) {
            $files_data[] = array(
                'path'        => $file_path,
                'filename'    => basename( $file_path ),
                'folder_name' => $folder_name,
            );
        }
        
        $subfolders[] = array(
            'folder_name' => $folder_name,
            'folder_path' => $dir_path,
            'files'       => $files_data,
            'file_count'  => count( $files_data ),
        );
    }
    
    return $subfolders;
}
}
/**
 * Add "Download All Snippets" button to admin list page
 */
add_action( 'admin_notices', function() {
    $screen = get_current_screen();
    
    if ( ! $screen || 'edit-wpcode_snippets_sync' !== $screen->id ) {
        return;
    }
    
    // Get count of all snippets
    $count = wp_count_posts( 'wpcode_snippets_sync' );
    $total = isset( $count->publish ) ? $count->publish : 0;
    
    if ( $total < 1 ) {
        return;
    }
    
    // Count by source type
    $wpcode_count = count( get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'posts_per_page' => -1,
        'post_status'    => 'publish',
        'fields'         => 'ids',
        'no_found_rows'  => true,
        'meta_query'     => array(
            'relation' => 'OR',
            array(
                'key'   => 'source_type',
                'value' => 'wpcode',
            ),
            array(
                'key'     => 'source_type',
                'compare' => 'NOT EXISTS',
            ),
        ),
    ) ) );
    
    $mu_plugin_count = count( get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'posts_per_page' => -1,
        'post_status'    => 'publish',
        'fields'         => 'ids',
        'no_found_rows'  => true,
        'meta_query'     => array(
            array(
                'key'   => 'source_type',
                'value' => 'mu-plugin',
            ),
        ),
    ) ) );
    
    // Count active vs inactive (WPCode only - MU-plugins are always active)
    $active_count = count( get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'posts_per_page' => -1,
        'post_status'    => 'publish',
        'fields'         => 'ids',
        'no_found_rows'  => true,
        'meta_query'     => array(
            'relation' => 'AND',
            array(
                'key'   => 'snippet_active',
                'value' => '1',
            ),
            array(
                'relation' => 'OR',
                array(
                    'key'   => 'source_type',
                    'value' => 'wpcode',
                ),
                array(
                    'key'     => 'source_type',
                    'compare' => 'NOT EXISTS',
                ),
            ),
        ),
    ) ) );
    
    $inactive_count = $wpcode_count - $active_count;
    
    // Count subfolders and files
    $mu_subfolders = kic_scan_mu_subfolders();
    $subfolder_count = count( $mu_subfolders );
    $subfolder_file_count = 0;
    foreach ( $mu_subfolders as $folder ) {
        $subfolder_file_count += $folder['file_count'];
    }
    
    // Use wp_nonce_url for proper URL nonce generation
    $download_url = wp_nonce_url( 
        admin_url( 'admin-ajax.php?action=download_all_snippets' ), 
        'download_all_snippets_action', 
        '_wpnonce' 
    );
    
    ?>
    <div class="notice notice-info" style="display: flex; align-items: center; justify-content: space-between; padding: 12px 15px;">
        <p style="margin: 0;">
            <strong>📦 Bulk Export:</strong> 
            Download all synced snippets as a ZIP archive.
            <span style="color: #666; font-size: 12px;">
                (<?php echo esc_html( $wpcode_count ); ?> WPCode: <span style="color: #00a32a;"><?php echo esc_html( $active_count ); ?> active</span>, <span style="color: #d63638;"><?php echo esc_html( $inactive_count ); ?> inactive</span> | <?php echo esc_html( $mu_plugin_count ); ?> MU-plugins<?php if ( $subfolder_file_count > 0 ) : ?> | <span style="color: #2271b1;"><?php echo esc_html( $subfolder_file_count ); ?> subfolder files</span><?php endif; ?>)
            </span>
        </p>
        <a href="<?php echo esc_url( $download_url ); ?>" 
           class="button button-primary" 
           style="margin-left: 15px;"
           id="download-all-snippets-btn">
            <span class="dashicons dashicons-download" style="margin-top: 3px; margin-right: 3px;"></span>
            Download All Snippets
        </a>
    </div>
    <?php
}, 10 ); // Priority 10 to show after MU-plugins control panel
/**
 * AJAX handler for downloading all snippets as ZIP
 * 
 * Security: Uses check_ajax_referer() which automatically dies on failure
 * and properly handles nonce verification per WordPress Coding Standards.
 * 
 * v1.4.0: Adds shortcode subfolders from mu-plugins
 * v1.3.0: Adds -active/-inactive suffix to filenames
 * v1.2.0: Now includes MU-plugins by generating files from snippet_code field
 */
add_action( 'wp_ajax_download_all_snippets', function() {
    
    // Capability check FIRST (before nonce to prevent timing attacks)
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_send_json_error( array( 'message' => 'Unauthorized access.' ), 403 );
    }
    
    // Verify nonce - check_ajax_referer auto-dies on failure
    check_ajax_referer( 'download_all_snippets_action', '_wpnonce' );
    
    // Check ZipArchive exists
    if ( ! class_exists( 'ZipArchive' ) ) {
        wp_send_json_error( array( 'message' => 'ZipArchive PHP extension is required but not available.' ), 500 );
    }
    
    // Get all sync records
    $snippets = get_posts( array(
        'post_type'      => 'wpcode_snippets_sync',
        'posts_per_page' => -1,
        'post_status'    => 'publish',
        'no_found_rows'  => true,
    ) );
    
    // Get ALL subfolders
    $mu_subfolders = kic_scan_mu_subfolders();
    
    if ( empty( $snippets ) && empty( $mu_subfolders ) ) {
        wp_send_json_error( array( 'message' => 'No snippets or subfolders found to download.' ), 404 );
    }
    
    // Use WordPress standard temp directory
    $temp_dir = get_temp_dir();
    $zip_filename = 'wpcode-snippets-export-' . gmdate( 'Y-m-d-His' ) . '.zip';
    $zip_path = trailingslashit( $temp_dir ) . $zip_filename;
    
    // Initialize ZIP with OVERWRITE for PHP 8.2+ compatibility
    $zip = new ZipArchive();
    $zip_result = $zip->open( $zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE );
    
    if ( true !== $zip_result ) {
        wp_send_json_error( array( 
            'message' => 'Could not create ZIP file.', 
            'error_code' => $zip_result 
        ), 500 );
    }
    
    // Get upload directory for file path resolution
    $upload_dir = wp_upload_dir();
    
    // Track what we're adding
    $manifest = array(
        'exported_at'         => gmdate( 'Y-m-d H:i:s' ) . ' UTC',
        'exported_by'         => wp_get_current_user()->user_login,
        'site_url'            => home_url(),
        'wordpress_ver'       => get_bloginfo( 'version' ),
        'total_files'         => 0,
        'wpcode_count'        => 0,
        'wpcode_active'       => 0,
        'wpcode_inactive'     => 0,
        'mu_plugin_count'     => 0,
        'subfolder_count'     => 0,
        'subfolder_file_count'=> 0,
        'snippets'            => array(),
        'subfolders'          => array(),
    );
    
    $files_added = 0;
    $wpcode_added = 0;
    $wpcode_active = 0;
    $wpcode_inactive = 0;
    $mu_plugin_added = 0;
    $subfolder_files_added = 0;
    $errors = array();
    
    // ========================================
    // PART 1: Process synced snippets (WPCode + MU-plugins)
    // ========================================
    foreach ( $snippets as $snippet ) {
        $post_id = $snippet->ID;
        
        // Get ACF fields
        $kic_id = get_field( 'kic_snippet_id', $post_id );
        $snippet_type = get_field( 'snippet_type', $post_id );
        $snippet_title = get_field( 'snippet_title', $post_id );
        $snippet_file = get_field( 'snippet_code_file', $post_id );
        $snippet_code = get_field( 'snippet_code', $post_id );
        $original_wpcode_id = get_field( 'original_wpcode_id', $post_id );
        $snippet_active = get_field( 'snippet_active', $post_id );
        $snippet_priority = get_field( 'snippet_priority', $post_id );
        $snippet_location = get_field( 'snippet_location', $post_id );
        $source_type = get_field( 'source_type', $post_id );
        $file_path_meta = get_field( 'file_path', $post_id );
        
        // Determine if this is MU-plugin or WPCode
        $is_mu_plugin = ( 'mu-plugin' === $source_type );
        
        // Determine active status suffix
        if ( $is_mu_plugin ) {
            $is_active = true;
            $status_suffix = '-active';
        } else {
            $is_active = ! empty( $snippet_active );
            $status_suffix = $is_active ? '-active' : '-inactive';
        }
        
        // Determine ZIP entry path based on source type
        if ( $is_mu_plugin ) {
            if ( ! empty( $file_path_meta ) ) {
                $original_filename = basename( $file_path_meta );
                $original_filename = preg_replace( '/\.php$/i', $status_suffix . '.php', $original_filename );
            } else {
                $original_filename = sanitize_file_name( $snippet_title );
                if ( empty( $original_filename ) ) {
                    $original_filename = 'mu-plugin-' . $post_id;
                }
                $original_filename .= $status_suffix . '.php';
            }
            $zip_entry_name = 'mu-plugins/' . $original_filename;
        } else {
            $safe_name = sanitize_file_name( $snippet_title );
            if ( empty( $safe_name ) ) {
                $safe_name = 'snippet-' . $post_id;
            }
            $safe_name .= $status_suffix;
            
            $extension = '.txt';
            if ( ! empty( $snippet_type ) ) {
                switch ( strtolower( $snippet_type ) ) {
                    case 'php':
                        $extension = '.php';
                        break;
                    case 'css':
                        $extension = '.css';
                        break;
                    case 'js':
                    case 'javascript':
                        $extension = '.js';
                        break;
                    case 'html':
                        $extension = '.html';
                        break;
                }
            }
            
            $type_folder = ! empty( $snippet_type ) ? strtolower( $snippet_type ) : 'misc';
            $zip_entry_name = 'wpcode/' . $type_folder . '/' . $safe_name . $extension;
        }
        
        $file_content = null;
        $content_source = '';
        
        // Try to get content from file first
        if ( ! empty( $snippet_file ) ) {
            $file_url = is_array( $snippet_file ) ? $snippet_file['url'] : $snippet_file;
            
            if ( ! empty( $file_url ) ) {
                $local_file_path = str_replace( 
                    $upload_dir['baseurl'], 
                    $upload_dir['basedir'], 
                    esc_url_raw( $file_url )
                );
                $local_file_path = preg_replace( '/\?.*$/', '', $local_file_path );
                $local_file_path = realpath( $local_file_path );
                
                if ( $local_file_path && strpos( $local_file_path, realpath( $upload_dir['basedir'] ) ) === 0 ) {
                    if ( file_exists( $local_file_path ) ) {
                        $file_content = file_get_contents( $local_file_path );
                        $content_source = 'file';
                    }
                }
            }
        }
        
        // If no file content, try snippet_code field
        if ( null === $file_content && ! empty( $snippet_code ) ) {
            $file_content = $snippet_code;
            $content_source = 'code_field';
        }
        
        if ( empty( $file_content ) ) {
            $errors[] = "Skipped '{$snippet_title}' - no file or code content";
            continue;
        }
        
        if ( $zip->addFromString( $zip_entry_name, $file_content ) ) {
            $files_added++;
            
            if ( $is_mu_plugin ) {
                $mu_plugin_added++;
            } else {
                $wpcode_added++;
                if ( $is_active ) {
                    $wpcode_active++;
                } else {
                    $wpcode_inactive++;
                }
            }
            
            $manifest['snippets'][] = array(
                'file'               => $zip_entry_name,
                'kic_id'             => $kic_id,
                'title'              => $snippet_title,
                'type'               => $snippet_type,
                'source_type'        => $is_mu_plugin ? 'mu-plugin' : 'wpcode',
                'original_wpcode_id' => $original_wpcode_id,
                'original_file_path' => $file_path_meta,
                'active'             => $is_active,
                'priority'           => $snippet_priority,
                'location'           => $snippet_location,
                'content_source'     => $content_source,
            );
        } else {
            $errors[] = "Failed to add '{$snippet_title}' to ZIP";
        }
    }
    
    // ========================================
    // PART 2: Process ALL mu-plugins subfolders
    // ========================================
    $mu_plugins_dir = WPMU_PLUGIN_DIR;
    
    foreach ( $mu_subfolders as $folder ) {
        $folder_name = $folder['folder_name'];
        $folder_manifest = array(
            'folder_name' => $folder_name,
            'files'       => array(),
        );
        
        foreach ( $folder['files'] as $file_info ) {
            $file_path = $file_info['path'];
            $filename = $file_info['filename'];
            
            // Security: Verify file is within mu-plugins directory
            $real_path = realpath( $file_path );
            $real_mu_dir = realpath( $mu_plugins_dir );
            
            if ( ! $real_path || strpos( $real_path, $real_mu_dir ) !== 0 ) {
                $errors[] = "Skipped '{$filename}' - outside mu-plugins directory";
                continue;
            }
            
            if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
                $errors[] = "Skipped '{$filename}' - file not readable";
                continue;
            }
            
            $file_content = file_get_contents( $file_path );
            
            if ( false === $file_content ) {
                $errors[] = "Skipped '{$filename}' - could not read file";
                continue;
            }
            
            // ZIP path: mu-plugins-subfolders/folder-name/file.php
            $zip_entry_name = 'mu-plugins-subfolders/' . $folder_name . '/' . $filename;
            
            if ( $zip->addFromString( $zip_entry_name, $file_content ) ) {
                $subfolder_files_added++;
                $files_added++;
                
                $folder_manifest['files'][] = array(
                    'file'          => $zip_entry_name,
                    'original_path' => $file_path,
                    'filename'      => $filename,
                );
            } else {
                $errors[] = "Failed to add '{$filename}' from {$folder_name} to ZIP";
            }
        }
        
        if ( ! empty( $folder_manifest['files'] ) ) {
            $manifest['subfolders'][] = $folder_manifest;
        }
    }
    
    // Update manifest counts
    $manifest['total_files'] = $files_added;
    $manifest['wpcode_count'] = $wpcode_added;
    $manifest['wpcode_active'] = $wpcode_active;
    $manifest['wpcode_inactive'] = $wpcode_inactive;
    $manifest['mu_plugin_count'] = $mu_plugin_added;
    $manifest['subfolder_count'] = count( $mu_subfolders );
    $manifest['subfolder_file_count'] = $subfolder_files_added;
    
    if ( ! empty( $errors ) ) {
        $manifest['warnings'] = $errors;
    }
    
    // Add manifest to ZIP
    $zip->addFromString( 'manifest.json', wp_json_encode( $manifest, JSON_PRETTY_PRINT ) );
    
    // Add README
    $readme = "# WPCode Snippets Export\n\n";
    $readme .= "Exported: " . $manifest['exported_at'] . "\n";
    $readme .= "From: " . $manifest['site_url'] . "\n";
    $readme .= "Total Files: " . $files_added . "\n\n";
    $readme .= "## Status Summary\n\n";
    $readme .= "| Type | Active | Inactive | Total |\n";
    $readme .= "|------|--------|----------|-------|\n";
    $readme .= "| WPCode Snippets | " . $wpcode_active . " | " . $wpcode_inactive . " | " . $wpcode_added . " |\n";
    $readme .= "| MU-Plugins (root) | " . $mu_plugin_added . " | 0 | " . $mu_plugin_added . " |\n";
    $readme .= "| MU-Plugins (subfolders) | " . $subfolder_files_added . " | 0 | " . $subfolder_files_added . " |\n";
    $readme .= "| **Total** | **" . ( $wpcode_active + $mu_plugin_added + $subfolder_files_added ) . "** | **" . $wpcode_inactive . "** | **" . $files_added . "** |\n\n";
    $readme .= "## Filename Convention\n\n";
    $readme .= "WPCode and root MU-plugin filenames include status:\n";
    $readme .= "- `snippet-name-active.php` - Currently running\n";
    $readme .= "- `snippet-name-inactive.php` - Disabled\n\n";
    $readme .= "**Note:** Subfolder files keep original names (always active).\n\n";
    $readme .= "## Folder Structure\n\n";
    $readme .= "```\n";
    $readme .= "export.zip\n";
    $readme .= "├── wpcode/                    # WPCode snippets by type\n";
    $readme .= "│   ├── php/\n";
    $readme .= "│   ├── css/\n";
    $readme .= "│   ├── js/\n";
    $readme .= "│   └── html/\n";
    $readme .= "├── mu-plugins/                # Root-level MU-Plugins\n";
    $readme .= "├── mu-plugins-subfolders/     # All MU-Plugin subfolders\n";
    foreach ( $mu_subfolders as $folder ) {
        $readme .= "│   └── " . $folder['folder_name'] . "/\n";
    }
    $readme .= "├── manifest.json\n";
    $readme .= "└── README.md\n";
    $readme .= "```\n\n";
    $readme .= "## Deployment\n\n";
    $readme .= "Copy `mu-plugins-subfolders/*` folders to `/wp-content/mu-plugins/` on target site.\n";
    
    $zip->addFromString( 'README.md', $readme );
    
    // Close ZIP
    $zip->close();
    
    if ( ! file_exists( $zip_path ) ) {
        wp_die( 'ZIP file was not created.', 'Error', array( 'response' => 500 ) );
    }
    
    // Send headers for download
    header( 'Content-Type: application/zip' );
    header( 'Content-Disposition: attachment; filename="' . $zip_filename . '"' );
    header( 'Content-Length: ' . filesize( $zip_path ) );
    header( 'Pragma: no-cache' );
    header( 'Expires: 0' );
    
    readfile( $zip_path );
    
    // Cleanup temp file (wp_delete_file is preferred over unlink per WP VIP)
    if ( file_exists( $zip_path ) ) {
        wp_delete_file( $zip_path );
    }
    
    exit;
} );
/**
 * Add download link to individual row actions
 */
add_filter( 'post_row_actions', function( $actions, $post ) {
    
    if ( 'wpcode_snippets_sync' !== $post->post_type ) {
        return $actions;
    }
    
    $snippet_file = get_field( 'snippet_code_file', $post->ID );
    
    if ( ! empty( $snippet_file ) ) {
        $file_url = is_array( $snippet_file ) ? $snippet_file['url'] : $snippet_file;
        
        if ( ! empty( $file_url ) ) {
            $download_url = add_query_arg( 'download', '1', esc_url_raw( $file_url ) );
            $actions['download'] = sprintf(
                '<a href="%s" title="%s">%s</a>',
                esc_url( $download_url ),
                esc_attr__( 'Download snippet file', 'wpcode-snippets-sync' ),
                esc_html__( 'Download', 'wpcode-snippets-sync' )
            );
        }
    }
    
    return $actions;
}, 10, 2 );
/**
 * Register admin scripts for button feedback
 */
add_action( 'admin_footer', function() {
    $screen = get_current_screen();
    
    if ( ! $screen || 'edit-wpcode_snippets_sync' !== $screen->id ) {
        return;
    }
    ?>
    <script>
    jQuery(document).ready(function($) {
        $('#download-all-snippets-btn').on('click', function() {
            var $btn = $(this);
            var originalText = $btn.html();
            
            $btn.html('<span class="dashicons dashicons-update" style="margin-top: 3px; margin-right: 3px; animation: rotation 1s infinite linear;"></span> Preparing ZIP...');
            
            setTimeout(function() {
                $btn.html(originalText);
            }, 3000);
        });
    });
    </script>
    <style>
    @keyframes rotation {
        from { transform: rotate(0deg); }
        to { transform: rotate(360deg); }
    }
    </style>
    <?php
} );

Comments

Add a Comment