| | |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\utility\Features; |
| | | use WP_REST_Response; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | |
| | | // Hook into WordPress admin |
| | | add_action('admin_menu', [$this, 'registerAdminPages']); |
| | | add_action('admin_enqueue_scripts', [$this, 'enqueueAdminAssets']); |
| | | |
| | | add_filter(BASE.'admin_action_filter', [$this, 'handleCacheActions'], 10, 3); |
| | | |
| | | add_action('rest_api_init', [$this, 'registerRestRoutes']); |
| | | } |
| | | |
| | | /** |
| | | * Register REST API routes for admin actions |
| | | */ |
| | | public function registerRestRoutes(): void |
| | | { |
| | | register_rest_route('jvb/v1', '/admin-cache', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleCacheAction'], |
| | | 'permission_callback' => [$this, 'checkAdminPermission'] |
| | | ]); |
| | | |
| | | register_rest_route('jvb/v1', '/admin-icons', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleIconAction'], |
| | | 'permission_callback' => [$this, 'checkAdminPermission'] |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * Check if user has admin permissions |
| | | */ |
| | | public function checkAdminPermission(\WP_REST_Request $request): bool |
| | | { |
| | | if (!current_user_can('manage_options')) { |
| | | return false; |
| | | } |
| | | |
| | | // Verify nonce |
| | | $nonce = $request->get_header('X-WP-Nonce'); |
| | | if (!wp_verify_nonce($nonce, 'wp_rest')) { |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Handle cache-related actions |
| | | */ |
| | | public function handleCacheAction(\WP_REST_Request $request): \WP_REST_Response |
| | | { |
| | | $action = sanitize_text_field($request->get_param('action')); |
| | | |
| | | switch ($action) { |
| | | case 'flush-all': |
| | | wp_cache_flush(); |
| | | return new \WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'All caches flushed successfully' |
| | | ]); |
| | | |
| | | case 'flush-cache': |
| | | $group = sanitize_text_field($request->get_param('group')); |
| | | if (empty($group)) { |
| | | return new \WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'No cache group specified' |
| | | ], 400); |
| | | } |
| | | |
| | | \JVBase\managers\CacheManager::invalidateAll($group); |
| | | |
| | | return new \WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => "Cache group '{$group}' flushed successfully" |
| | | ]); |
| | | |
| | | default: |
| | | return new \WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Invalid action' |
| | | ], 400); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle icon-related actions |
| | | */ |
| | | public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response |
| | | { |
| | | $action = sanitize_text_field($request->get_param('action')); |
| | | $icons = \JVBase\managers\IconsManager::getInstance(); |
| | | |
| | | switch ($action) { |
| | | case 'refresh-icons': |
| | | $icons->forceRefresh(); |
| | | return new \WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Icon CSS regenerated successfully' |
| | | ]); |
| | | |
| | | case 'restore-icon-version': |
| | | $timestamp = (int)$request->get_param('timestamp'); |
| | | if (empty($timestamp)) { |
| | | return new \WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'No timestamp provided' |
| | | ], 400); |
| | | } |
| | | |
| | | if ($icons->restoreVersion($timestamp)) { |
| | | return new \WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Icon version restored successfully' |
| | | ]); |
| | | } |
| | | |
| | | return new \WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Failed to restore icon version' |
| | | ], 500); |
| | | |
| | | case 'merge-icon-versions': |
| | | $timestamps = $request->get_param('timestamps'); |
| | | |
| | | if (empty($timestamps) || !is_array($timestamps)) { |
| | | return new \WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'No versions selected for merging' |
| | | ], 400); |
| | | } |
| | | |
| | | $timestamps = array_map('intval', $timestamps); |
| | | |
| | | if (count($timestamps) < 2) { |
| | | return new \WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Please select at least 2 versions to merge' |
| | | ], 400); |
| | | } |
| | | |
| | | if ($icons->mergeVersions($timestamps)) { |
| | | return new \WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Icon versions merged successfully' |
| | | ]); |
| | | } |
| | | |
| | | return new \WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Failed to merge icon versions' |
| | | ], 500); |
| | | |
| | | default: |
| | | return new \WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Invalid action' |
| | | ], 400); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Register a subpage to appear under the main settings page |
| | | * |
| | |
| | | BASE.'cache', |
| | | [$this, 'renderCachePage'] |
| | | ); |
| | | add_submenu_page( |
| | | $this->main_page['menu_slug'], |
| | | 'Icon Management', |
| | | 'Icons', |
| | | 'manage_options', |
| | | BASE.'icons', |
| | | [$this, 'renderIconsPage'] |
| | | ); |
| | | |
| | | // $this->getSubpages(); |
| | | // Add registered subpages |
| | |
| | | return 'data:image/svg+xml;base64,' . base64_encode($svg); |
| | | } |
| | | |
| | | public function renderCachePage() |
| | | { |
| | | $groups = get_option(BASE.'all_cache_groups', []); |
| | | public function renderCachePage():void |
| | | { |
| | | $connections = CacheManager::getAllConnections(); |
| | | |
| | | ?> |
| | | <h1>Manage Cache</h1> |
| | | <?php |
| | | foreach ($groups as $group => $caches) { |
| | | ?> |
| | | <details> |
| | | <summary class="row btw"><h2><?=$group?></h2></summary> |
| | | <table> |
| | | <thead> |
| | | <tr> |
| | | <th scope="col"><input type="checkbox" name="select-all-<?=$group?>" id="select-all-<?=$group?>"> |
| | | <label for="select-all-<?=$group?>">All</label></th> |
| | | <th scope="col">Cache Key</th> |
| | | <th scope="col">Actions</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <?php |
| | | foreach ($caches as $key) { |
| | | ?> |
| | | <tr> |
| | | <td><input type="checkbox" name="select-<?=$group?>-<?=$key?>" id="select-<?=$group?>-<?=$key?>"><label for="select-<?=$group?>-<?=$key?>"></label></td> |
| | | <td><?= $key ?></td> |
| | | <td><button type="button" data-action="flush-<?=$group?>-<?=$key?>"><?= jvbIcon('trash')?></button></td> |
| | | </tr> |
| | | <?php |
| | | } |
| | | ?> |
| | | </tbody> |
| | | </table> |
| | | </details> |
| | | <?php |
| | | } |
| | | } |
| | | // Separate generic vs. specific caches |
| | | $generic_groups = []; |
| | | $content_specific = []; |
| | | $nonce = wp_create_nonce('wp_rest'); |
| | | |
| | | foreach ($connections as $group => $configs) { |
| | | $is_generic = !$this->isBoundToContentOrTaxonomy($group); |
| | | |
| | | if ($is_generic) { |
| | | $generic_groups[$group] = $configs; |
| | | } else { |
| | | $content_specific[$group] = $configs; |
| | | } |
| | | } |
| | | |
| | | ?> |
| | | <div class="wrap jvb-admin-wrap"> |
| | | <h1>Cache Management</h1> |
| | | |
| | | <div class="jvb-cache-actions"> |
| | | <button type="button" class="button button-primary" data-action="flush-all"> |
| | | <?= jvbIcon('arrows-clockwise'); ?> |
| | | Flush All Caches |
| | | </button> |
| | | </div> |
| | | |
| | | <div class="jvb-cache-section"> |
| | | <h2>Generic Caches & Connections</h2> |
| | | <table class="wp-list-table widefat fixed striped"> |
| | | <thead> |
| | | <tr> |
| | | <th class="manage-column">Cache Group</th> |
| | | <th class="manage-column">Connected To</th> |
| | | <th class="manage-column">Actions</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <?php if (empty($generic_groups)): ?> |
| | | <tr><td colspan="3">No generic caches registered</td></tr> |
| | | <?php else: ?> |
| | | <?php foreach ($generic_groups as $group => $configs): ?> |
| | | <tr> |
| | | <td><strong><?= esc_html($group); ?></strong></td> |
| | | <td><?= $this->formatConnections($configs); ?></td> |
| | | <td> |
| | | <button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>"> |
| | | <?= jvbIcon('trash'); ?> Flush |
| | | </button> |
| | | </td> |
| | | </tr> |
| | | <?php endforeach; ?> |
| | | <?php endif; ?> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | |
| | | <details class="jvb-cache-section"> |
| | | <summary><h2>Content-Specific Caches</h2></summary> |
| | | <table class="wp-list-table widefat fixed striped"> |
| | | <thead> |
| | | <tr> |
| | | <th>Cache Group</th> |
| | | <th>Connected To</th> |
| | | <th>Actions</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <?php foreach ($content_specific as $group => $configs): ?> |
| | | <tr> |
| | | <td><strong><?= esc_html($group); ?></strong></td> |
| | | <td><?= $this->formatConnections($configs); ?></td> |
| | | <td> |
| | | <button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>"> |
| | | <?= jvbIcon('trash'); ?> Flush |
| | | </button> |
| | | </td> |
| | | </tr> |
| | | <?php endforeach; ?> |
| | | </tbody> |
| | | </table> |
| | | </details> |
| | | </div> |
| | | <script> |
| | | (function() { |
| | | const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-cache')); ?>'; |
| | | const nonce = '<?= esc_js($nonce); ?>'; |
| | | |
| | | function callCacheAction(action, data = {}) { |
| | | const body = { action, ...data }; |
| | | |
| | | return fetch(apiUrl, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'X-WP-Nonce': nonce |
| | | }, |
| | | body: JSON.stringify(body) |
| | | }) |
| | | .then(response => response.json()) |
| | | .then(data => { |
| | | if (data.success) { |
| | | alert(data.message || 'Success!'); |
| | | location.reload(); |
| | | } else { |
| | | alert('Error: ' + (data.message || 'Unknown error')); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | alert('Network error: ' + error.message); |
| | | console.error('Error:', error); |
| | | }); |
| | | } |
| | | |
| | | // Flush all caches |
| | | document.querySelector('[data-action="flush-all"]')?.addEventListener('click', function() { |
| | | if (confirm('Flush all caches? This may temporarily slow down your site.')) { |
| | | this.disabled = true; |
| | | callCacheAction('flush-all'); |
| | | } |
| | | }); |
| | | |
| | | // Flush individual cache groups |
| | | document.querySelectorAll('[data-action="flush-cache"]').forEach(btn => { |
| | | btn.addEventListener('click', function() { |
| | | const group = this.getAttribute('data-group'); |
| | | if (confirm(`Flush cache group "${group}"?`)) { |
| | | this.disabled = true; |
| | | callCacheAction('flush-cache', { group: group }); |
| | | } |
| | | }); |
| | | }); |
| | | })(); |
| | | </script> |
| | | <?php |
| | | } |
| | | |
| | | protected function isBoundToContentOrTaxonomy(string $group): bool |
| | | { |
| | | $group = jvbNoBase($group); |
| | | |
| | | if (defined('JVB_CONTENT')) { |
| | | foreach (JVB_CONTENT as $key => $config) { |
| | | if (jvbNoBase($key) === $group) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (defined('JVB_TAXONOMY')) { |
| | | foreach (JVB_TAXONOMY as $key => $config) { |
| | | if (jvbNoBase($key) === $group) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | protected function formatConnections(array $configs): string |
| | | { |
| | | $connections = []; |
| | | foreach ($configs as $config) { |
| | | $parent = $config['parent'] ?? 'unknown'; |
| | | $scope = $config['scope'] ?? 'id'; |
| | | $connections[] = "{$parent} ({$scope})"; |
| | | } |
| | | return esc_html(implode(', ', $connections)); |
| | | } |
| | | |
| | | public function handleCacheActions($response, $request, $action):WP_REST_Response |
| | | { |
| | | if (!str_starts_with($action, 'flush-')) { |
| | | return $response; |
| | | } |
| | | |
| | | if ($action === 'flush-all') { |
| | | wp_cache_flush(); |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'All caches flushed successfully' |
| | | ]); |
| | | } |
| | | |
| | | if (str_starts_with($action, 'flush-cache')) { |
| | | $group = $request->get_param('group'); |
| | | if (empty($group)) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'No cache group specified' |
| | | ], 400); |
| | | } |
| | | |
| | | \JVBase\managers\CacheManager::invalidateAll($group); |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => "Cache group '{$group}' flushed successfully" |
| | | ]); |
| | | } |
| | | |
| | | if ($action === 'merge-icon-versions') { |
| | | $timestamps = $request->get_param('timestamps'); |
| | | |
| | | if (empty($timestamps) || !is_array($timestamps)) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'No versions selected for merging' |
| | | ], 400); |
| | | } |
| | | |
| | | // Convert to integers |
| | | $timestamps = array_map('intval', $timestamps); |
| | | |
| | | if (count($timestamps) < 2) { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Please select at least 2 versions to merge' |
| | | ], 400); |
| | | } |
| | | |
| | | $icons = \JVBase\managers\IconsManager::getInstance(); |
| | | |
| | | if ($icons->mergeVersions($timestamps)) { |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Icon versions merged successfully' |
| | | ]); |
| | | } |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Failed to merge icon versions' |
| | | ], 500); |
| | | } |
| | | |
| | | if ($action === 'refresh-icons') { |
| | | $icons = \JVBase\managers\IconsManager::getInstance(); |
| | | $icons->forceRefresh(); |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Icon CSS refresh triggered' |
| | | ]); |
| | | } |
| | | |
| | | if ($action === 'restore-icon-version') { |
| | | $timestamp = (int)$request->get_param('timestamp'); |
| | | $icons = \JVBase\managers\IconsManager::getInstance(); |
| | | |
| | | if ($icons->restoreVersion($timestamp)) { |
| | | return new WP_REST_Response([ |
| | | 'success' => true, |
| | | 'message' => 'Icon version restored successfully' |
| | | ]); |
| | | } |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Failed to restore icon version' |
| | | ], 500); |
| | | } |
| | | |
| | | return $response; |
| | | } |
| | | |
| | | public function renderIconsPage():void |
| | | { |
| | | $icons = \JVBase\managers\IconsManager::getInstance(); |
| | | $versions = $icons->getVersionHistory(); |
| | | $nonce = wp_create_nonce('wp_rest'); |
| | | |
| | | ?> |
| | | <div class="wrap jvb-admin-wrap"> |
| | | <h1>Icon Management</h1> |
| | | |
| | | <div class="jvb-icon-actions"> |
| | | <button type="button" class="button button-primary" data-action="refresh-icons"> |
| | | <?= jvbIcon('arrows-clockwise'); ?> |
| | | Force Refresh CSS |
| | | </button> |
| | | <button type="button" class="button" data-action="merge-icon-versions" id="merge-versions-btn" disabled> |
| | | <?= jvbIcon('git-merge'); ?> |
| | | Merge Selected Versions |
| | | </button> |
| | | </div> |
| | | |
| | | <h2>Version History</h2> |
| | | <table class="wp-list-table widefat fixed striped"> |
| | | <thead> |
| | | <tr> |
| | | <th class="check-column"> |
| | | <input type="checkbox" id="select-all-versions"> |
| | | <label for="select-all-versions" class="screen-reader-text">Select All</label> |
| | | </th> |
| | | <th>Date/Time</th> |
| | | <th>Icon Count</th> |
| | | <th>File Size</th> |
| | | <th>Actions</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <?php if (empty($versions)): ?> |
| | | <tr><td colspan="5">No version history available</td></tr> |
| | | <?php else: ?> |
| | | <?php foreach (array_reverse($versions) as $index => $version): ?> |
| | | <tr> |
| | | <th class="check-column"> |
| | | <input type="checkbox" |
| | | name="version-select" |
| | | class="version-checkbox" |
| | | value="<?= esc_attr($version['timestamp']); ?>"> |
| | | </th> |
| | | <td><?= esc_html(date('Y-m-d H:i:s', $version['timestamp'])); ?></td> |
| | | <td> |
| | | <?= esc_html($version['icon_count']); ?> icons |
| | | <button type="button" |
| | | class="button-link" |
| | | data-action="view-icon-list" |
| | | data-timestamp="<?= esc_attr($version['timestamp']); ?>"> |
| | | (view) |
| | | </button> |
| | | </td> |
| | | <td><?= esc_html($version['size_formatted']); ?></td> |
| | | <td> |
| | | <button type="button" class="button" |
| | | data-action="restore-icon-version" |
| | | data-timestamp="<?= esc_attr($version['timestamp']); ?>"> |
| | | <?= jvbIcon('arrow-counter-clockwise'); ?> Restore |
| | | </button> |
| | | </td> |
| | | </tr> |
| | | <tr id="icon-list-<?= esc_attr($version['timestamp']); ?>" class="icon-list-row" style="display: none;"> |
| | | <td colspan="5"> |
| | | <div class="icon-list-content"> |
| | | <?php foreach ($version['iconList'] as $style => $icons): ?> |
| | | <strong><?= esc_html(ucfirst($style)); ?>:</strong> |
| | | <?= esc_html(implode(', ', $icons)); ?><br> |
| | | <?php endforeach; ?> |
| | | </div> |
| | | </td> |
| | | </tr> |
| | | <?php endforeach; ?> |
| | | <?php endif; ?> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | |
| | | <script> |
| | | (function() { |
| | | const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-icons')); ?>'; |
| | | const nonce = '<?= esc_js($nonce); ?>'; |
| | | |
| | | // Helper function for API calls |
| | | function callIconAction(action, data = {}) { |
| | | const body = { action, ...data }; |
| | | |
| | | return fetch(apiUrl, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'X-WP-Nonce': nonce |
| | | }, |
| | | body: JSON.stringify(body) |
| | | }) |
| | | .then(response => response.json()) |
| | | .then(data => { |
| | | if (data.success) { |
| | | alert(data.message || 'Success!'); |
| | | location.reload(); |
| | | } else { |
| | | alert('Error: ' + (data.message || 'Unknown error')); |
| | | } |
| | | return data; |
| | | }) |
| | | .catch(error => { |
| | | alert('Network error: ' + error.message); |
| | | console.error('Error:', error); |
| | | }); |
| | | } |
| | | |
| | | // Enable/disable merge button based on selection |
| | | document.querySelectorAll('.version-checkbox').forEach(checkbox => { |
| | | checkbox.addEventListener('change', function() { |
| | | const checkedCount = document.querySelectorAll('.version-checkbox:checked').length; |
| | | document.getElementById('merge-versions-btn').disabled = checkedCount < 2; |
| | | }); |
| | | }); |
| | | |
| | | // Select all functionality |
| | | const selectAll = document.getElementById('select-all-versions'); |
| | | if (selectAll) { |
| | | selectAll.addEventListener('change', function() { |
| | | document.querySelectorAll('.version-checkbox').forEach(checkbox => { |
| | | checkbox.checked = this.checked; |
| | | checkbox.dispatchEvent(new Event('change')); |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | // Toggle icon list view |
| | | document.querySelectorAll('.view-icon-list-btn').forEach(btn => { |
| | | btn.addEventListener('click', function() { |
| | | const timestamp = this.getAttribute('data-timestamp'); |
| | | const row = document.getElementById('icon-list-' + timestamp); |
| | | if (row) { |
| | | row.style.display = row.style.display === 'none' ? '' : 'none'; |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | // Force refresh button |
| | | const refreshBtn = document.getElementById('refresh-icons-btn'); |
| | | if (refreshBtn) { |
| | | refreshBtn.addEventListener('click', function() { |
| | | if (confirm('Force regenerate icon CSS? This will reload the page.')) { |
| | | this.disabled = true; |
| | | callIconAction('refresh-icons'); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // Merge versions button |
| | | const mergeBtn = document.getElementById('merge-versions-btn'); |
| | | if (mergeBtn) { |
| | | mergeBtn.addEventListener('click', function() { |
| | | const checkboxes = document.querySelectorAll('.version-checkbox:checked'); |
| | | const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value)); |
| | | |
| | | if (timestamps.length < 2) { |
| | | alert('Please select at least 2 versions to merge'); |
| | | return; |
| | | } |
| | | |
| | | if (confirm(`Merge ${timestamps.length} versions? This will create a new CSS file with all unique icons.`)) { |
| | | this.disabled = true; |
| | | callIconAction('merge-icon-versions', { timestamps: timestamps }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // Restore version buttons |
| | | document.querySelectorAll('.restore-version-btn').forEach(btn => { |
| | | btn.addEventListener('click', function() { |
| | | const timestamp = parseInt(this.getAttribute('data-timestamp')); |
| | | |
| | | if (confirm('Restore this icon version? This will reload the page.')) { |
| | | this.disabled = true; |
| | | callIconAction('restore-icon-version', { timestamp: timestamp }); |
| | | } |
| | | }); |
| | | }); |
| | | })(); |
| | | </script> |
| | | <?php |
| | | } |
| | | } |