From 42fa8304ddb811b0f725f245130f70c0f5e86a6c Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 04 Nov 2025 06:12:02 +0000
Subject: [PATCH] =Refactored LoginManager to be more extensible and configurable, as well as an AjaxRateLimiter
---
inc/managers/DashboardManager.php | 1120 +++++++++++++++++++++++++++++++++++++++-------------------
1 files changed, 743 insertions(+), 377 deletions(-)
diff --git a/inc/managers/DashboardManager.php b/inc/managers/DashboardManager.php
index 0f75df3..276d3c1 100644
--- a/inc/managers/DashboardManager.php
+++ b/inc/managers/DashboardManager.php
@@ -1,7 +1,7 @@
<?php
namespace JVBase\managers;
-use JVBase\managers\CRUD;
+use JVBase\forms\TaxonomySelector;use JVBase\managers\CRUD;
use JVBase\meta\MetaManager;
use JVBase\utility\Features;
use WP_User;
@@ -22,8 +22,8 @@
public function __construct()
{
- $this->cache = new CacheManager('dashboard');
- $this->cache->invalidateGroup('dashboard');
+ $this->cache = CacheManager::for('dashboard', WEEK_IN_SECONDS);
+ $this->cache->invalidate();
add_action('init', [$this, 'registerDashboard']);
if (!$this->isRegistered()) {
add_action('init', [$this, 'buildDashboard']);
@@ -32,9 +32,12 @@
$this->role = jvbUserRole();
$this->userLink = (int)get_user_meta($this->user->ID, BASE.'link', true);
+
+ add_action('template_redirect', [$this, 'handleRedirects']);
add_action('template_include', [$this, 'dashboardTemplates']);
add_action('admin_init', [$this, 'redirectFromAdmin']);
add_action('wp_enqueue_scripts', [$this, 'dashboardScripts'], 50);
+ add_filter('jvbDashboardPage', [$this, 'renderIndex'], 10, 2);
}
/**
@@ -85,22 +88,121 @@
if (current_user_can('manage_options')) {
return;
}
-
// Redirect to custom dashboard
- wp_redirect(home_url('dash'));
- exit;
+ $this->redirectToDashboard();
}
+ protected function redirectToLogin():void
+ {
+ wp_redirect(wp_login_url(get_home_url(null, '/dash')));
+ exit;
+ }
+
+ protected function redirectToDashboard():void
+ {
+ wp_redirect(get_home_url(null, '/dash'));
+ exit;
+ }
+
+
+ protected function getConfig(string $page):array
+ {
+ $pages = $this->getAllDashboardPages();
+ $key = array_search($page, $pages);
+ if ($key === false || is_numeric($key)) {
+ return [];
+ }
+ return Features::getConfig($key);
+ }
+
+ /**
+ * Check if user can access page and redirect if not
+ * @param string $page
+ * @param int|null $userID
+ * @return void
+ */
+ protected function requirePageAccess(string $page, ?int $userID = null):void
+ {
+ $allowedPages = $this->getUserAllowedPages($userID);
+
+ if (!in_array($page, $allowedPages)) {
+ $this->redirectToDashboard();
+ }
+ }
+
+ protected function getTitle(string $slug):string
+ {
+ $config = $this->getConfig($slug);
+ if (!empty($config)) {
+ return $config['dash_title']??$config['plural'];
+ }
+ return ucwords(str_replace('-', ' ', str_replace('_', ' ', $slug)));
+ }
+
+ public function handleRedirects():void
+ {
+ // Only process dashboard-related pages and 404s
+ if (!is_singular(BASE.'dash') && !is_post_type_archive(BASE.'dash') && !is_404()) {
+ return;
+ }
+
+ // Check if user is logged in first
+ if (!is_404() && !is_user_logged_in()) {
+ error_log('Redirecting to login - user not logged in');
+ $this->redirectToLogin();
+ return;
+ }
+
+ // If logged in but doesn't have dashboard access, redirect to home
+ if (!is_404() && !isOurPeople() && !current_user_can('manage_options')) {
+ error_log('Redirecting to home - user lacks dashboard access');
+ wp_redirect(home_url());
+ exit;
+ }
+
+ // Handle 404s that are trying to access dashboard URLs
+ global $wp;
+ if (is_404() && (str_starts_with($wp->request, 'dash/') || $wp->request === 'dash')) {
+ error_log('404 on dashboard URL, redirecting to dashboard home');
+ $this->redirectToDashboard();
+ return;
+ }
+
+ // For valid dashboard pages, check access permissions
+ if (!is_404()) {
+ $page = $this->getCurrentPage();
+
+ // Dashboard home is always accessible (if authenticated)
+ if ($page === '' || $page === 'dash') {
+ return;
+ }
+
+ // Check if page exists in allowed pages
+ $allowedPages = $this->getUserAllowedPages();
+ if (!in_array($page, $allowedPages)) {
+ error_log("User not allowed to access page: {$page}");
+ $this->redirectToDashboard();
+ return;
+ }
+ }
+ }
+
/**
- * Ensures the necessary pages ar created
+ * Ensures the necessary pages are created
* @return void
*/
public function buildDashboard():void
{
- $manageableContent = jvbGetAllDashboardPages();
- foreach ($manageableContent as $slug) {
+ $manageableContent = $this->getAllDashboardPages();
+ foreach ($manageableContent as $key => $slug) {
+ if ($slug === 'dash') {
+ continue;
+ }
+ $existing = get_page_by_path($slug, OBJECT, BASE.'dash');
+ if ($existing) {
+ continue;
+ }
$title = $this->getTitle($slug);
- $slug = sanitize_title($title);
$ID = wp_insert_post(array(
'post_title' => $title,
@@ -110,174 +212,56 @@
));
if ($title === 'Integrations') {
- $integrations = ['BlueSky', 'Cloudflare', 'Facebook', 'Google Maps', 'Google My Business', 'Helcim', 'Instagram', 'Square', 'Umami'];
- foreach ($integrations as $integration) {
- $slug = sanitize_title($integration);
- wp_insert_post([
- 'post_title' => $integration,
- 'post_name' => $slug,
- 'post_type' => BASE.'dash',
- 'post_status' => 'publish',
- 'post_parent' => $ID
- ]);
- }
+ $this->buildIntegrationPages($ID);
}
}
update_option(BASE.'dashboard_registered', true);
remove_action('init', [$this, 'buildDashboard']);
}
- protected function getAllDashboardPages():array
+ /**
+ * Build integration sub-pages
+ * @param int $parentID
+ * @return void
+ */
+ protected function buildIntegrationPages(int $parentID):void
{
- $manageableContent = get_option(BASE.'all_dashboard_pages');
- if (JVB_TESTING) {
- $manageableContent = false;
+ $integrations = JVB()->getAvailableServices(false);
+
+ foreach ($integrations as $name => $integration) {
+ $title = $integration->getTitle();
+ $slug = sanitize_title($title);
+
+ // Check if integration page already exists
+ $existing = get_posts([
+ 'post_type' => BASE.'dash',
+ 'name' => $slug,
+ 'post_parent' => $parentID,
+ 'posts_per_page' => 1,
+ ]);
+
+ if (!empty($existing)) {
+ continue; // Skip if exists
+ }
+
+ wp_insert_post([
+ 'post_title' => $title,
+ 'post_name' => $slug,
+ 'post_type' => BASE.'dash',
+ 'post_status' => 'publish',
+ 'post_parent' => $parentID
+ ]);
}
- if ($manageableContent === false) {
-
- $manageableContent = [];
- $bios = [];
- foreach (JVB_USER as $role => $config) {
- $manageableContent = array_merge($manageableContent, jvbRolePages($role));
- }
-
- if (Features::forSite()->has('referrals')) {
- $manageableContent[] = 'referrals';
- }
- foreach (JVB_TAXONOMY as $tax => $config) {
- if (Features::forTaxonomy($tax)->has('is_content')) {
- $manageableContent[] = strtolower($config['plural']);
- }
- }
- if (Features::forMembership()->has('can_invite')) {
- $manageableContent[] = 'invites';
- }
-
- if (Features::forMembership()->has('term_approval')) {
- $manageableContent[] = 'approvals';
- }
-
- if (Features::forMembership()->has('forum')) {
- $manageableContent[] = 'news';
- }
-
- if (Features::forMembership()->has('member_content')) {
- $manageableContent[] = 'metrics';
- }
-
- if (!empty($bios)) {
- $manageableContent[] = 'bio';
- }
-
- if (Features::forSite()->has('favourites')) {
- $manageableContent[] = 'favourites';
- }
-
- if (Features::anyContentHas('karma') || Features::anyTaxonomyHas('karma') || Features::anyUserHas('karma')){
- $manageableContent[] = 'karmic-score';
- }
-
- if (Features::forSite()->has('notifications')) {
- $manageableContent[] = 'notifications';
- }
-
- if (Features::forSite()->has('support')){
- $manageableContent[] = 'support';
- }
-
- if (Features::hasAnyIntegration()) {
- $manageableContent[] = 'integrations';
- }
-
- $manageableContent[] = 'admin';
- $manageableContent = apply_filters('jvbDashboardPages', $manageableContent);
- $manageableContent = array_unique($manageableContent);
- sort($manageableContent);
- $manageableContent = array_map(function ($content) {
- return str_replace('_', '-', $content);
- }, $manageableContent);
- update_option(BASE.'all_dashboard_pages', $manageableContent);
- }
-
-
- return $manageableContent;
}
- protected function getRolePages(string $role):array
- {
- if (!array_key_exists(jvbNoBase($role), JVB_USER)) {
- return [];
- }
- $manageableContent = get_option(BASE.$role.'_pages');
- if (JVB_TESTING) {
- $manageableContent = false;
- }
- if ($manageableContent === false) {
- $manageableContent = [];
- $config = JVB_USER[$role];
- $content = $config['can_create'];
- $settings = $bio = false;
- if (array_key_exists('profile', $config)) {
- $manageableContent[] = $config['profile'];
- }
-
- foreach ($content as $c) {
- if (is_array($c)) {
- foreach ($c as $type => $contents) {
- $manageableContent = array_merge($manageableContent, $contents);
- }
- } else {
- $manageableContent = array_merge($manageableContent, [$c]);
- }
- }
-
- if (array_key_exists('has_dashboard', $config)) {
- $manageableContent[] = 'settings';
- }
-
- update_option(BASE.$role.'_pages', $manageableContent);
- }
-
- return $manageableContent;
- }
-
- protected function getTitle(string $page):string
- {
- $content = JVB_CONTENT;
- $contentTax = array_filter(JVB_TAXONOMY, function ($tax) {
- return jvbCheck('is_content', $tax);
- });
- $content = array_merge($content, $contentTax);
- $title = '';
-
-
- if (array_key_exists($page, $content)) {
- $config = $content[$page];
- $title = (array_key_exists('dash_title', $config)) ? $config['dash_title'] : $config['plural'];
- } else {
- switch ($page) {
- case 'admin':
- $title = 'Admin';
- break;
- default:
- $title = ucwords(str_replace('_', ' ', str_replace('-', ' ', $page)));
- }
- }
-
- return $title;
- }
protected function getDescription(string $page):string
{
- $content = JVB_CONTENT;
- $contentTax = array_filter(JVB_TAXONOMY, function ($tax) {
- return jvbCheck('is_content', $tax);
- });
- $content = array_merge($content, $contentTax);
- if (array_key_exists($page, $content)) {
- $config = $content[$page];
+ $config = $this->getConfig($page);
+ if (!empty($config)) {
$description = (array_key_exists('dash_description', $config)) ? $config['dash_description'] : '';
} else {
+ $description = apply_filters('jvbDashboardDescription', $page);
switch ($page) {
case 'approval':
$description = 'See your approval requests for term creation, joining shops, or joining edmonton.ink. You can also help shape the community by approving other\'s requests!';
@@ -320,147 +304,167 @@
if (!is_singular(BASE.'dash') && !is_post_type_archive(BASE.'dash')) {
return $template;
}
- if (!isOurPeople() && !current_user_can('manage_options')) {
- error_log('Redirecting because:');
- if (!isOurPeople()) {
- error_log('Not our people');
- }
- if (!current_user_can('manage_options')) {
- error_log('Cannot manage options');
- }
- wp_redirect(wp_login_url(get_home_url(null, '/dash')));
- exit;
- }
// Get current page/section
-
$page = $this->getCurrentPage();
+ $integrationSlugs = array_map(function($name) {
+ return sanitize_title(str_replace('_', '-', $name));
+ }, array_keys(JVB()->getAvailableServices(false)));
- switch ($page) {
- case 'integrations':
- if (!Features::hasAnyIntegration('user', $this->role)) {
- wp_redirect(get_home_url(null, '/dash'));
- exit;
- }
- break;
- case 'bluesky':
- case 'cloudflare':
- case 'facebook':
- case 'google-maps':
- case 'google-my-business':
- case 'helcim':
- case 'instagram':
- case 'square':
- case 'umami':
- if (!Features::hasIntegration($page,'user', $this->role)) {
- wp_redirect(get_home_url(null, '/dash'));
- exit;
- }
- break;
- case 'bio':
- $permission = JVB_USER[$this->role]['profile']??false;
- if (!$permission || (!current_user_can('manage_'.$permission) && !current_user_can('manage_options'))) {
- wp_redirect(get_home_url(null, '/dash'));
- exit;
- }
- break;
- case 'settings':
- if (!current_user_can('manage_settings') && !current_user_can('manage_options')) {
- wp_redirect(get_home_url(null, '/dash'));
- exit;
- }
- break;
- case 'approval':
- if (!current_user_can('skip_moderation')) {
- wp_redirect(get_home_url(null, '/dash'));
- exit;
- }
- break;
- case 'dash':
-
- break;
- default:
- $type = match($page) {
- 'menu-item' => 'menu_item',
- 'events' => 'event',
- default => $page
- };
-
- $permission = strtolower(str_replace(' ', '_',JVB_CONTENT[$type]['plural']??$type.'s'));
-
- if (!current_user_can('edit_'.$permission)) {
- error_log('User cannot edit: '.$permission);
- wp_redirect(get_home_url(null, '/dash'));
- exit;
- }
- break;
+ // Check if this is an integration subpage
+ if (in_array($page, $integrationSlugs)) {
+ // Pass along to the Integrations template handler which knows to check for subpages
+ $page = 'integrations';
}
- // Enqueue needed styles/scripts
+
+ echo $this->cache->remember(
+ $page,
+ function() use ($page) {
+ return $this->renderDashboard($page);
+ }
+ );
+
+ return '';
+ }
+
+ protected function getConstantSlug(string $page):string
+ {
+ $slug = array_search($page, $this->getAllDashboardPages());
+ return (is_numeric($slug)) ? '' : $slug;
+ }
+ protected function renderDashboard(string $page):string
+ {
+ ob_start();
jvbInlineStyles('nav');
jvbInlineStyles('dash');
jvbInlineStyles('forms');
- $this->cache->delete($page);
- echo $this->cache->remember(
+ $this->renderHeader();
+ // Pass to page handler
+ $constantSlug = $this->getConstantSlug($page);
+ echo apply_filters(
+ 'jvbDashboardPage',
+ $this->renderPage($page),
$page,
- function() use ($page) {
- ob_start();
- $this->renderHeader();
-
- switch ($page) {
- case 'dash':
- if (current_user_can('manage_options')) {
- $content = apply_filters('jvbAdminDashboard', '');
-
- if ($content !== '') {
- echo $content;
- }else {
- $this->renderAdmin();
- }
- } else {
- $this->renderIndex();
- }
-
- break;
- case 'admin':
- $this->renderAdmin();
- break;
- case 'bio':
- $this->renderForm(JVB_USER[$this->role]['profile']);
- break;
- case 'settings':
- $this->renderSettings();
- break;
- case 'integrations':
- case 'bluesky':
- case 'cloudflare':
- case 'facebook':
- case 'google-maps':
- case 'google-my-business':
- case 'helcim':
- case 'instagram':
- case 'square':
- case 'umami':
- $this->renderIntegrations($page);
- break;
- case 'approval':
- $this->renderApprovals();
- break;
- default:
- $this->renderCRUD($page);
- break;
- }
-
- echo jvbLoadingScreen();
- $this->renderFooter();
-
- // Get buffer contents and clean buffer
- return ob_get_clean();
- }
+ $constantSlug
);
- // Return empty string to prevent default template
- return '';
- }
+ $this->renderFooter();
+ return ob_get_clean();
+// $integrationSlugs = array_map(function($name) {
+// return sanitize_title(str_replace('_', '-', $name));
+// }, array_keys(JVB()->getAvailableServices(false)));
+//
+// if ($page === 'integrations' || in_array($page, $integrationSlugs)) {
+// // Check integration access
+// if ($page === 'integrations') {
+// if (!Features::hasAnyIntegration('user', $this->role)) {
+// $this->redirectToDashboard();
+// }
+// } else {
+// if (!Features::hasIntegration($page, 'user', $this->role)) {
+// $this->redirectToDashboard();
+// }
+// }
+// } elseif ($page === 'bio') {
+// // Bio page logic
+// $permission = JVB_USER[$this->role]['profile'] ?? false;
+// if (!$permission || (!current_user_can('manage_'.$permission) && !current_user_can('manage_options'))) {
+// $this->redirectToDashboard();
+// }
+// } elseif ($page === 'settings') {
+// // Settings page logic
+// if (!current_user_can('manage_settings') && !current_user_can('manage_options')) {
+// $this->redirectToDashboard();
+// }
+// } elseif ($page === 'approval') {
+// // Approval page logic
+// if (!current_user_can('skip_moderation')) {
+// $this->redirectToDashboard();
+// }
+// } elseif ($page !== 'dash') {
+// // Regular content type - check permission
+// $type = match($page) {
+// 'menu-item' => 'menu_item',
+// 'events' => 'event',
+// default => $page
+// };
+//
+// $permission = $this->getPermissionForType($type);
+// if (!current_user_can($permission)) {
+// $this->redirectToDashboard();
+// }
+// }
+// // Enqueue needed styles/scripts
+//
+// $this->cache->delete($page);
+// echo $this->cache->remember(
+// $page,
+// function() use ($page) {
+// ob_start();
+// $this->renderHeader();
+//
+// switch ($page) {
+// case 'dash':
+// if (current_user_can('manage_options')) {
+// $content = apply_filters('jvbAdminDashboard', '');
+//
+// if ($content !== '') {
+// echo $content;
+// }else {
+// $this->renderAdmin();
+// }
+// } else {
+// $this->renderIndex();
+// }
+//
+// break;
+// case 'admin':
+// $this->renderAdmin();
+// break;
+// case 'bio':
+// $this->renderForm(JVB_USER[$this->role]['profile']);
+// break;
+// case 'settings':
+// $this->renderSettings();
+// break;
+// case 'integrations':
+// case 'bluesky':
+// case 'cloudflare':
+// case 'facebook':
+// case 'google-maps':
+// case 'google-my-business':
+// case 'helcim':
+// case 'instagram':
+// case 'square':
+// case 'umami':
+// $this->renderIntegrations($page);
+// break;
+// case 'approval':
+// $this->renderApprovals();
+// break;
+// default:
+// $this->renderCRUD($page);
+// break;
+// }
+//
+// echo jvbLoadingScreen();
+// $this->renderFooter();
+//
+// // Get buffer contents and clean buffer
+// return ob_get_clean();
+// }
+// );
+//
+// // Return empty string to prevent default template
+// return '';
+ }
+
+ protected function renderPage(string $page):string
+ {
+ return '<h1>Whoops</h1>
+ <p>It seems this page isn\'t configured yet.</p>
+ <p>If this keeps happening, maybe contact the admin.</p>';
+ }
/**
* Enqueues necessary scripts
@@ -468,52 +472,48 @@
*/
public function dashboardScripts():void
{
- if (is_post_type_archive(BASE.'dash') || is_singular(BASE.'dash')) {
+ if (!is_singular(BASE.'dash') && !is_post_type_archive(BASE.'dash')) {
+ return;
+ }
+ wp_enqueue_script('jvb-loading');
+ wp_enqueue_script('jvb-form');
-// wp_enqueue_style('quill-css', 'https://cdn.quilljs.com/1.3.6/quill.snow.css');
+ // Consolidate all dashboard settings
+ wp_localize_script('jvb-loading', 'dashboardSettings', array(
+ 'loadingMessages' => array(
+ 'default' => 'Loading...',
+ 'error' => 'Failed to load page'
+ ),
+ 'strings' => array(
+ 'deleteConfirm' => 'Are you sure you want to delete this item?',
+ 'bulkDeleteConfirm' => 'Are you sure you want to delete these items?',
+ 'deleteSuccess' => 'Item(s) deleted successfully',
+ 'deleteError' => 'Error deleting item(s)',
+ 'saveSuccess' => 'Changes saved successfully',
+ 'saveError' => 'Error saving changes',
+ 'loadError' => 'Error loading content'
+ ),
+ 'currentUser' => array(
+ 'id' => $this->user->ID,
+ 'name' => $this->user->display_name,
+ 'role' => array_values($this->user->roles)[0] ?? '',
+ 'type' => str_replace(BASE, '', array_values($this->user->roles)[0]),
+ 'city' => '', // Add if needed,
+ 'artistID' => $this->userLink,
+ )
+ ));
+ wp_enqueue_script('jvb-selector');
+ wp_enqueue_script('jvb-uploader');
+ wp_enqueue_script('jvb-content');
+ wp_enqueue_script('jvb-crud');
- wp_enqueue_script('jvb-loading');
- wp_enqueue_script('jvb-form');
-
-
- // Consolidate all dashboard settings
- wp_localize_script('jvb-loading', 'dashboardSettings', array(
- 'loadingMessages' => array(
- 'default' => 'Loading...',
- 'error' => 'Failed to load page'
- ),
- 'strings' => array(
- 'deleteConfirm' => 'Are you sure you want to delete this item?',
- 'bulkDeleteConfirm' => 'Are you sure you want to delete these items?',
- 'deleteSuccess' => 'Item(s) deleted successfully',
- 'deleteError' => 'Error deleting item(s)',
- 'saveSuccess' => 'Changes saved successfully',
- 'saveError' => 'Error saving changes',
- 'loadError' => 'Error loading content'
- ),
- 'currentUser' => array(
- 'id' => $this->user->ID,
- 'name' => $this->user->display_name,
- 'role' => array_values($this->user->roles)[0] ?? '',
- 'type' => str_replace(BASE, '', array_values($this->user->roles)[0]),
- 'city' => '', // Add if needed,
- 'artistID' => $this->userLink,
- )
- ));
-
- wp_enqueue_script('jvb-selector');
- wp_enqueue_script('jvb-uploader');
- wp_enqueue_script('jvb-content');
- wp_enqueue_script('jvb-crud');
-// wp_enqueue_script('jvb-dashboard-navigator');
-
- $page = $this->getCurrentPage();
+ $page = $this->getCurrentPage();
switch ($page) {
case 'notifications':
- if (jvbSiteHasNotifications()) {
+ if (Features::forSite()->has('notifications')) {
wp_enqueue_script('jvb-notification-manager');
}
break;
@@ -547,7 +547,7 @@
}
break;
}
- if (jvbSiteHasFavourites()) {
+ if (Features::forSite()->has('favourites')) {
wp_enqueue_script('jvb-favourites');
wp_localize_script('jvb-favourites-manager', 'favouritesSettings', [
'strings' => [
@@ -563,22 +563,24 @@
wp_enqueue_script('jvb-creator');
- if (jvbSiteHasForum()) {
+ if (Features::forSite()->has('forum')) {
wp_enqueue_script('jvb-news');
}
do_action('jvbDashScripts', $page);
- }
}
- protected function getCurrentPage():string
+ protected function getCurrentPage():string
{
- global $wp;
- $dash = str_replace('dash/', '', $wp->request);
- if (str_starts_with($dash, 'integrations/')) {
- $dash = str_replace('integrations/', '', $dash);
+ if (is_post_type_archive(BASE.'dash')) {
+ return 'dash';
}
- return ($dash === '') ? 'dash' : $dash;
+ global $post;
+ if (!$post) {
+ return '';
+ }
+
+ return $post->post_name;
}
protected function renderHeader():void
@@ -591,7 +593,8 @@
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<?php
- $pages = jvbGetUserDashboardPages();
+ $pages = $this->getUserAllowedPages();
+
foreach($pages as $page) {
$page = str_replace('_', '-', $page);
$link = ($page === 'dash') ? '/'.$page : "/dash/$page";
@@ -610,13 +613,26 @@
$checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
$title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
- <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' role="switch" name="dark-mode"><span class="slider">'.
- jvbIcon('light').
- jvbIcon('dark').'</span></label>';
+ <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
+ jvbIcon('light', ['title'=> 'Light Mode']).
+ jvbIcon('dark', ['title'=>'Dark Mode']).
+ '</span></label>';
?>
<p class="title">
<a href="<?= get_home_url(); ?>" rel="home" title="Back to Site">
- <?= jvbIcon('logo-basic'); ?>
+ <?php
+ $icon = (int) get_option( 'site_icon' );
+ $out = '';
+ if ($icon > 0) {
+ $url = wp_get_attachment_image_url( $icon);
+ if ($url) {
+ $out = '<img src="'.$url.'">';
+ }
+ }
+ if ($out == '') {
+ $out =jvbIcon('home');
+ }
+ ?><?= $out ?>
</a>
</p>
@@ -637,24 +653,24 @@
?>
</section>
<footer class="col">
+ <?= jvbLoadingScreen() ?>
+ <?= TaxonomySelector::outputSelectorModal() ?>
<nav class="dashboard-nav">
<?php
$current_page = $this->getCurrentPage();
- $pages = jvbGetUserDashboardPages()?:[];
+ $pages = $this->getUserAllowedPages()?:[];
+ error_log('PageS: '.print_r($pages, true));
- global $jvb_everything;
echo '<ul>';
foreach ($pages as $page) {
// Add data-page attribute for the navigator
$active = ($current_page == $page) ? ' class="current"' : '';
$current = ($current_page == $page) ? ' aria-current="page"' : '';
- $icon = (array_key_exists($page, $jvb_everything)) ? $jvb_everything[$page]['icon'] ?? $page : $page;
-
- $title = $this->getTitle($page);
- $page = str_replace('_', '-', $page);
+ $config = $this->getConfig($page);
+ $icon = $config['icon']??$page;
+ $title = ucwords(str_replace('-', ' ', $page));
$link = ($page === 'dash') ? '/'.$page : "/dash/$page";
-
printf(
'<li%s><a href="%s"%s data-page="%s" data-dash title="%s">%s<span>%s</span></a></li>',
$active,
@@ -684,37 +700,47 @@
<?php
}
- protected function renderIndex():void
+ public function renderIndex(string $content, string $page):string
{
- $name = get_post_meta($this->userLink, BASE.'firstname', true);
- $name = ($name === '') ? $this->user->display_name : $name;
+ if ($page !== '' && $page !== 'dash') {
+ return $content;
+ }
+ ob_start();
+ $name = ($this->user->first_name !== '') ? $this->user->first_name : $this->user->display_name;
echo '<h1 style="text-transform:none;margin-top:2em!important;">Hey '.$name.'</h1>';
echo '<p>Welcome back!</p>';
- $pages = jvbGetUserDashboardPages();
+ $pages = $this->getUserAllowedPages();
echo '<h2>What would you like to do today?</h2>';
- global $jvb_everything;
echo '<ul>';
- foreach ($pages as $page) {
-
+ foreach ($pages as $slug => $page) {
+ if ($page === 'dash') {
+ continue;
+ }
$title = $this->getTitle($page);
$url = sanitize_title($title);
$description = $this->getDescription($page);
-
+ $icon = $page;
+ if (!is_numeric($slug)) {
+ $config = Features::getConfig($slug);
+ if (array_key_exists('icon', $config)) {
+ $icon = $config['icon'];
+ }
+ }
if ($title !== '') {
echo '<li><p><a href="'.get_home_url(null, '/dash/'.$url.'/').'"
- data-page="'.$url.'" data-dash>'.jvbIcon($page).ucfirst($title).'</a></p>'.$description.'</li>';
+ data-page="'.$url.'" data-dash>'.jvbIcon($icon).ucwords($title).'</a></p></li>';
}
}
echo '</ul>';
echo '<p>Everything saves auto-magically, so rest easy.</p>';
-
+ return ob_get_clean();
}
/**
* Similar to CRUD, except it only manages a single item, such as a user's profile or a shop
@@ -887,20 +913,13 @@
}
- protected function renderCRUD(string $type):void
- {
- $type = match($type) {
- 'menu-item' => 'menu_item',
- 'events' => 'event',
- default => $type
- };
- $crud = new CRUD($type);
- $crud->render();
- }
-
- protected function renderAdmin():void
+ public function renderAdmin(string $content, string $page):string
{
+ if ($page !== '' && $page !== 'dash') {
+ return $content;
+ }
+ ob_start();
?>
<nav class="tabs row start" role="tablist">
<?php
@@ -918,7 +937,7 @@
$active = ($i === 1) ? ' active' : '';
?>
<button type="button" class="tab<?=$active?>" data-tab="<?=$type?>" role="tab" aria-selected="<?= ($active !== '') ? 'true' : 'false'?>">
- <h2><?=jvbIcon($type)?> <?= $settings['plural'] ?></h2>
+ <h2><?=jvbIcon($settings['icon']??$key)?> <?= $settings['plural'] ?></h2>
</button>
<?php
$i++;
@@ -953,7 +972,8 @@
</div>
<?php
- global $jvb_everything;
+
+ $jvb_everything = array_merge(JVB_CONTENT, JVB_TAXONOMY);
foreach ($jvb_everything as $type => $settings) {
$meta = new MetaManager(null, 'form');
@@ -1044,5 +1064,351 @@
$meta->renderForm('admin', [], $fields)
);
}
+
+ return ob_get_clean();
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /**
+ * Get all possible dashboard pages regardless of user
+ * Used during dashboard build process
+ * @return array
+ */
+ protected function getAllDashboardPages():array
+ {
+ $cacheKey = 'all_pages';
+ $pages = $this->cache->get($cacheKey);
+ if ($pages === false || JVB_TESTING) {
+ $pages = [];
+
+ // Add feature-dependent pages (non-config)
+ if (Features::forSite()->has('referrals')) {
+ $pages[] = 'referrals';
+ }
+
+ if (Features::forMembership()->has('can_invite')) {
+ $pages[] = 'invites';
+ }
+
+ if (Features::forMembership()->has('term_approval')) {
+ $pages[] = 'approvals';
+ }
+
+ if (Features::forMembership()->has('forum')) {
+ $pages[] = 'news';
+ }
+
+ if (Features::forMembership()->has('member_content')) {
+ $pages[] = 'metrics';
+ }
+
+ if (Features::forSite()->has('favourites')) {
+ $pages[] = 'favourites';
+ }
+
+ if (Features::anyContentHas('karma') || Features::anyTaxonomyHas('karma') || Features::anyUserHas('karma')) {
+ $pages[] = 'karmic-score';
+ }
+
+ if (Features::forSite()->has('notifications')) {
+ $pages[] = 'notifications';
+ }
+
+ if (Features::forSite()->has('support')) {
+ $pages[] = 'support';
+ }
+
+ if (Features::hasAnyIntegration()) {
+ $pages[] = 'integrations';
+ }
+
+ // Add all content types (with config keys)
+ foreach (JVB_CONTENT as $slug => $config) {
+ $pages[$slug] = sanitize_title($config['plural']);
+ }
+
+ foreach (JVB_TAXONOMY as $slug=>$config) {
+ $pages[$slug] = sanitize_title($config['plural']);
+ }
+
+ // Allow filtering
+ $pages = apply_filters('jvbAllDashboardPages', $pages);
+
+ // Remove duplicates while preserving keys
+ $pages = array_unique($pages);
+
+ // Dash home always first
+ array_unshift($pages, 'dash');
+
+ $this->cache->set($cacheKey, $pages, WEEK_IN_SECONDS);
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Get pages available to a specific role
+ * @param string $role The role slug (with or without BASE prefix)
+ * @return array
+ */
+ protected function getRolePages(string $role):array
+ {
+ $role = jvbNoBase($role);
+
+ if (!array_key_exists($role, JVB_USER)) {
+ return [];
+ }
+
+ return Features::forUser($role)->getDashboardPages();
+ }
+
+ /**
+ * Get pages that a specific user is allowed to access
+ * Filters based on capabilities and features
+ * @param int|null $userID Optional user ID (defaults to current user)
+ * @return array
+ */
+ public function getUserAllowedPages(?int $userID = null):array
+ {
+ if ($userID === null) {
+ $user = $this->user;
+ $userID = $user->ID;
+ } else {
+ $user = get_userdata($userID);
+ }
+
+ if (!$user || !$this->userHasDashboardAccess($user)) {
+ return [];
+ }
+
+ $cacheKey = "user_pages_{$userID}";
+ $pages = $this->cache->get($cacheKey);
+
+ if ($pages === false || JVB_TESTING) {
+ if (user_can($userID, 'manage_options')) {
+ // Admin gets all pages as flat array
+ $pages = $this->getAllDashboardPages();
+ // Extract just the values (slugs)
+ $this->cache->set($cacheKey, $pages, WEEK_IN_SECONDS);
+ return $pages;
+ }
+ $roles = array_map('jvbNoBase', $user->roles);
+ $pages = $this->getAllDashboardPages();
+
+ $canSkip = user_can($userID, 'skip_moderation');
+ foreach($pages as $key => $slug) {
+ //Default to Remove pages
+ $remove = true;
+ if (!is_numeric($key)) {
+ $type = Features::getType($key);
+ if ($type) {
+ $permission = RoleManager::getPlural($key);
+ }
+ switch ($type) {
+ case 'content':
+ if (!user_can($userID, "edit_{$permission}")) {
+ $remove = false;
+ }
+ break;
+ case 'taxonomy':
+ $config = Features::getConfig($key, 'taxonomy');
+ if (array_key_exists('is_content', $config) && $config['is_content'] && (user_can($userID, "own_{$key}") || user_can($userID, "manage_{$key}"))) {
+ $remove = false;
+ }
+ break;
+ }
+ } else {
+ switch ($slug) {
+ case 'integrations':
+ foreach($roles as $role) {
+ if (Features::hasAnyIntegration('user', $role)) {
+ $remove = false;
+ }
+ }
+ break;
+ case 'invites':
+ $canInvite = JVB_MEMBERSHIP['can_invite']??[];
+ foreach ($roles as $role) {
+ if (array_key_exists($role, $canInvite)) {
+ $remove = false;
+ }
+ }
+ if ($remove) {
+ if ($canSkip || array_key_exists('invitable', $config)) {
+ $remove = false;
+ }
+ }
+ break;
+ case 'approvals':
+ $canApprove = false;
+ if (Features::forMembership()->has('term_approval')) {
+ if (array_key_exists('can_approve', JVB_MEMBERSHIP)) {
+ foreach ($roles as $role) {
+ if (in_array($role, JVB_MEMBERSHIP['can_approve'])) {
+ $canApprove = true;
+ }
+ }
+ } else {
+ //Anyone can approve
+ $canApprove = true;
+ }
+ }
+ if ($canSkip && $canApprove) {
+ $remove = false;
+ }
+ break;
+ case 'news':
+ $canAccess = false;
+ if (array_key_exists('member_only', JVB_MEMBERSHIP)){
+ foreach ($roles as $role) {
+ if (in_array($role, JVB_MEMBERSHIP['member_only'])) {
+ $canAccess = true;
+ }
+ }
+ }
+ if ($canAccess && $canSkip) {
+ $remove = false;
+ }
+ break;
+ case 'metrics':
+ foreach ($roles as $role) {
+ if (!empty(Features::forUser($role)->getCreatableContent())) {
+ $remove = false;
+ }
+ }
+ break;
+ case 'karmic-score':
+ foreach ($roles as $role) {
+ $contents = Features::forUser($role)->getCreatableContent();
+ if (!empty($contents)) {
+ foreach($contents as $content) {
+ if (Features::forContent($content)->has('karma')) {
+ $remove = false;
+ }
+ }
+ }
+ }
+ break;
+ case 'favourites':
+ case 'notifications':
+ case 'support':
+ $remove = false;
+ break;
+ default:
+ break;
+ }
+ if ($remove) {
+ unset($pages[$key]);
+ }
+ }
+ }
+
+ //Allow Filtering
+ $pages = apply_filters('jvbUserDashboardPages', $pages, $user->roles, $userID);
+ $pages = array_unique($pages);
+
+ $this->cache->set($cacheKey, $pages, WEEK_IN_SECONDS);
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Check if user can create content
+ * Replaces jvbUserCanCreate()
+ * @param int $userID
+ * @return bool
+ */
+ protected function userCanCreate(int $userID = 0):bool
+ {
+ $user = ($userID === 0) ? wp_get_current_user() : get_userdata($userID);
+ $roles = array_intersect(
+ $this->getRolesWithDashboard(),
+ array_map('jvbNoBase', $user->roles)
+ );
+
+ $creatable = [];
+ foreach ($roles as $role) {
+ $roleCreatable = Features::forUser($role)->getCreatableContent();
+ $creatable = array_merge($creatable, $roleCreatable);
+ }
+
+ return !empty($creatable);
+ }
+
+ /**
+ * Get user roles that have dashboard access
+ * Replaces jvbRolesWithDashboard()
+ * @return array
+ */
+ protected function getRolesWithDashboard():array
+ {
+ return array_keys(array_filter(JVB_USER, function ($role) {
+ return Features::forUser(array_search($role, JVB_USER))->has('has_dashboard');
+ }));
+ }
+
+ /**
+ * Check if user has dashboard access
+ * @param WP_User $user
+ * @return bool
+ */
+ protected function userHasDashboardAccess(WP_User $user):bool
+ {
+ if (user_can($user, 'manage_options')) {
+ return true;
+ }
+
+ $userRoles = array_map('jvbNoBase', $user->roles);
+ $dashboardRoles = $this->getRolesWithDashboard();
+
+ return count(array_intersect($dashboardRoles, $userRoles)) > 0;
+ }
+
+ /**
+ * Get the capability needed to access a content type
+ * @param string $type
+ * @return string
+ */
+ protected function getPermissionForType(string $type):string
+ {
+ // Check if it's a registered content type
+ if (array_key_exists($type, JVB_CONTENT)) {
+ $plural = JVB_CONTENT[$type]['plural'];
+ return 'edit_'.$plural;
+ }
+
+ // Default to edit_{type}s
+ return 'edit_'.$type.'s';
+ }
+
+ /**
+ * Invalidate dashboard page cache for a user or all users
+ * Call this when user roles or permissions change
+ * @param int|null $userID Specific user to invalidate, null for all
+ * @return void
+ */
+ public function invalidatePagesCache(?int $userID = null):void
+ {
+ if ($userID !== null) {
+ $this->cache->delete("user_pages_{$userID}");
+ } else {
+ // Invalidate all user caches by invalidating the group
+ $this->cache->invalidate();
+ }
+ }
}
--
Gitblit v1.10.0