From 0113d2e9c9ff34a6ffb10707cc76d34b67a0c367 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 19 Jan 2026 16:29:41 +0000
Subject: [PATCH] =Refactored window.getTemplate into a full templating class window.jvbTemplates. Refactored CRUD.js, UploadManager.js, FormController.js, PopulateForm.js with that in mind
---
inc/managers/AdminPages.php | 755 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 714 insertions(+), 41 deletions(-)
diff --git a/inc/managers/AdminPages.php b/inc/managers/AdminPages.php
index c9f9e25..a2147a0 100644
--- a/inc/managers/AdminPages.php
+++ b/inc/managers/AdminPages.php
@@ -1,6 +1,9 @@
<?php
namespace JVBase\managers;
+use JVBase\utility\Features;
+use WP_REST_Response;
+
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
@@ -31,7 +34,7 @@
'icon' => jvbCSSIcon('settings'),
'position' => 0
];
- $this->subpages = apply_filters('jvbAdminSubpages', []);
+ $this->subpages = get_option(BASE.'adminSubpage', []);
// delete_option(BASE.'admin_actions');
// delete_option(BASE.'admin_subpages');
// $this->getSubpages();
@@ -41,8 +44,167 @@
// 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']);
+ // Handle form submissions
+ add_action('admin_init', [$this, 'handleAdminPageSubmission']);
+ add_action('admin_notices', [$this, 'displayAdminNotices']);
}
+ /**
+ * 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'));
+ $source = sanitize_text_field($request->get_param('source') ?? 'icons'); // Add source param
+ $icons = \JVBase\managers\IconsManager::for($source);
+
+ switch ($action) {
+ case 'refresh-icons':
+ $icons->forceRefresh();
+ return new \WP_REST_Response([
+ 'success' => true,
+ 'message' => "Icon CSS regenerated successfully for '{$source}'"
+ ]);
+
+ 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
*
@@ -177,6 +339,14 @@
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
@@ -259,6 +429,7 @@
</div>
</div>
<?php
+
}
/**
@@ -412,9 +583,9 @@
if (current_user_can($action['capability'])) {
?>
<a data-action="<?=$action['slug']?>" class="jvb-action">
- <?= jvbIcon($action['icon']); ?>
+ <?= jvbDashIcon($action['icon']); ?>
<span class="jvb-link-title"><?= esc_html($action['label'])?></span>
- <span class="loader"><?=jvbIcon('refresh')?><?=jvbIcon('check')?></span>
+ <span class="loader"><?=jvbDashIcon('arrows-clockwise')?><?=jvbDashIcon('check')?></span>
</a>
<?php
}
@@ -469,7 +640,7 @@
*/
protected function getIcon(string $icon = 'logo', bool $css = false): string
{
- $svg = jvbIcon($icon, ['wrap' => false]);
+ $svg = jvbDashIcon($icon, ['wrap' => false]);
if ($css) {
// For CSS, replace currentColor with brand color
$svg = str_replace('currentColor', '#FF0080', $svg);
@@ -486,42 +657,544 @@
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">
+ <?= jvbDashIcon('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); ?>">
+ <?= jvbDashIcon('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); ?>">
+ <?= jvbDashIcon('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
+ {
+ // Get current source from query param or default to 'icons'
+ $current_source = $_GET['icon_source'] ?? 'icons';
+ $current_source = sanitize_text_field($current_source);
+
+ // Get all registered icon sources
+ $all_sources = ['icons', 'forms', 'dash']; // You could get this dynamically if needed
+
+ $icons = \JVBase\managers\IconsManager::for($current_source);
+ $versions = $icons->getVersionHistory();
+ $nonce = wp_create_nonce('wp_rest');
+
+ ?>
+ <div class="wrap jvb-admin-wrap">
+ <h1>Icon Management</h1>
+
+ <!-- Source Selector -->
+ <div class="jvb-icon-source-selector">
+ <label for="icon-source-select">Icon Source:</label>
+ <select id="icon-source-select" onchange="window.location.href='<?= admin_url('admin.php?page=' . BASE . 'icons&icon_source='); ?>' + this.value">
+ <?php foreach ($all_sources as $source): ?>
+ <option value="<?= esc_attr($source); ?>" <?= selected($current_source, $source, false); ?>>
+ <?= esc_html(ucfirst($source)); ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+
+ <div class="jvb-icon-actions">
+ <button type="button" class="button button-primary" data-action="refresh-icons" data-source="<?= esc_attr($current_source); ?>">
+ <?= jvbDashIcon('arrows-clockwise'); ?>
+ Force Refresh CSS
+ </button>
+ <button type="button" class="button" data-action="merge-icon-versions" data-source="<?= esc_attr($current_source); ?>" id="merge-versions-btn" disabled>
+ <?= jvbDashIcon('git-merge'); ?>
+ Merge Selected Versions
+ </button>
+ </div>
+
+ <h2>Version History for <?= esc_html(ucfirst($current_source)); ?></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 view-icon-list-btn"
+ data-timestamp="<?= esc_attr($version['timestamp']); ?>">
+ (view)
+ </button>
+ </td>
+ <td><?= esc_html($version['size_formatted']); ?></td>
+ <td>
+ <button type="button" class="button restore-version-btn"
+ data-action="restore-icon-version"
+ data-source="<?= esc_attr($current_source); ?>"
+ data-timestamp="<?= esc_attr($version['timestamp']); ?>">
+ <?= jvbDashIcon('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); ?>';
+ const currentSource = '<?= esc_js($current_source); ?>';
+
+ // Helper function for API calls
+ function callIconAction(action, data = {}) {
+ const body = { action, source: currentSource, ...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
+ document.querySelector('[data-action="refresh-icons"]')?.addEventListener('click', function() {
+ if (confirm('Force regenerate icon CSS? This will reload the page.')) {
+ this.disabled = true;
+ callIconAction('refresh-icons');
+ }
+ });
+
+ // Merge versions button
+ document.getElementById('merge-versions-btn')?.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
+ }
+
+ public static function addSubpage(string $key, array $value):void
+ {
+ $option = get_option(BASE.'adminSubpage', []);
+ if (empty($option) || !array_key_exists($key, $option)) {
+ $option[$key] = $value;
+ update_option(BASE.'adminSubpage', $option);
+ }
+ }
+
+ /**
+ * Handle admin page form submissions
+ * Fires after WordPress admin_init
+ */
+ public function handleAdminPageSubmission(): void
+ {
+ // Only process on our admin pages
+ if (!isset($_GET['page']) || strpos($_GET['page'], BASE) !== 0) {
+ return;
+ }
+
+ // Check for form submission
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['submit'])) {
+ return;
+ }
+
+ // Verify nonce
+ $nonce_field = BASE . 'admin_page_nonce';
+ if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], $nonce_field)) {
+ add_action('admin_notices', function() {
+ echo '<div class="notice notice-error"><p>Security check failed. Please try again.</p></div>';
+ });
+ return;
+ }
+
+ $page_slug = sanitize_text_field($_GET['page']);
+
+ // Allow other classes to handle their page submissions
+ $result = apply_filters('jvb_admin_page_submission', null, $page_slug, $_POST);
+
+ // Store result in transient for after redirect
+ if (is_array($result)) {
+ set_transient(BASE . 'admin_notice_' . get_current_user_id(), $result, 30);
+ }
+
+ // Redirect to prevent form resubmission (POST-Redirect-GET pattern)
+ wp_safe_redirect(add_query_arg(['page' => $page_slug], admin_url('admin.php')));
+ exit;
+ }
+
+ /**
+ * Display admin notices from form submissions
+ */
+ public function displayAdminNotices(): void
+ {
+ $notice = get_transient(BASE . 'admin_notice_' . get_current_user_id());
+
+ if ($notice && is_array($notice)) {
+ delete_transient(BASE . 'admin_notice_' . get_current_user_id());
+
+ $type = $notice['success'] ? 'success' : 'error';
+ $message = $notice['message'] ?? 'Settings saved.';
+
+ echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
+ }
+ }
}
--
Gitblit v1.10.0