Adds option for dot notification in menus and cleans up empty lines
parent
2aaf051677
commit
5771fb0fe1
|
|
@ -215,12 +215,12 @@ class PluginManager
|
||||||
|
|
||||||
// Use App API to get database connection
|
// Use App API to get database connection
|
||||||
$db = \App\App::db();
|
$db = \App\App::db();
|
||||||
|
|
||||||
// If database unavailable, fallback to manifest
|
// If database unavailable, fallback to manifest
|
||||||
if (!$db) {
|
if (!$db) {
|
||||||
return self::$catalog[$plugin]['meta']['enabled'] ?? false;
|
return self::$catalog[$plugin]['meta']['enabled'] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo = ($db instanceof \PDO) ? $db : $db->getConnection();
|
$pdo = ($db instanceof \PDO) ? $db : $db->getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -305,7 +305,7 @@ class PluginManager
|
||||||
|
|
||||||
// Disable foreign key checks temporarily to allow table drops
|
// Disable foreign key checks temporarily to allow table drops
|
||||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
||||||
|
|
||||||
foreach ($tables as $table) {
|
foreach ($tables as $table) {
|
||||||
// Check if this table belongs to the plugin by checking its migration file
|
// Check if this table belongs to the plugin by checking its migration file
|
||||||
$migrationFile = self::$catalog[$plugin]['path'] . '/migrations/create_' . $plugin . '_tables.sql';
|
$migrationFile = self::$catalog[$plugin]['path'] . '/migrations/create_' . $plugin . '_tables.sql';
|
||||||
|
|
@ -317,7 +317,7 @@ class PluginManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-enable foreign key checks
|
// Re-enable foreign key checks
|
||||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -613,7 +613,7 @@ if ($queryAction === 'plugin_check_page' && isset($_GET['plugin'])) {
|
||||||
if ($hasMigration) {
|
if ($hasMigration) {
|
||||||
foreach ($migrationFiles as $migrationFile) {
|
foreach ($migrationFiles as $migrationFile) {
|
||||||
$migrationContent = file_get_contents($migrationFile);
|
$migrationContent = file_get_contents($migrationFile);
|
||||||
|
|
||||||
// Extract tables created by this migration (plugin-owned)
|
// Extract tables created by this migration (plugin-owned)
|
||||||
if (preg_match_all('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?([a-zA-Z0-9_]+)`?/i', $migrationContent, $matches)) {
|
if (preg_match_all('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?([a-zA-Z0-9_]+)`?/i', $migrationContent, $matches)) {
|
||||||
foreach ($matches[1] as $tableName) {
|
foreach ($matches[1] as $tableName) {
|
||||||
|
|
@ -622,7 +622,7 @@ if ($queryAction === 'plugin_check_page' && isset($_GET['plugin'])) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all referenced tables (dependencies)
|
// Find all referenced tables (dependencies)
|
||||||
foreach ($allTables as $table) {
|
foreach ($allTables as $table) {
|
||||||
if (strpos($migrationContent, $table) !== false && !in_array($table, $pluginOwnedTables)) {
|
if (strpos($migrationContent, $table) !== false && !in_array($table, $pluginOwnedTables)) {
|
||||||
|
|
@ -709,6 +709,18 @@ if (is_array($overviewStatusesPayload)) {
|
||||||
$adminOverviewStatuses = $overviewStatusesPayload['statuses'] ?? (is_array($overviewStatusesPayload) ? $overviewStatusesPayload : []);
|
$adminOverviewStatuses = $overviewStatusesPayload['statuses'] ?? (is_array($overviewStatusesPayload) ? $overviewStatusesPayload : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$adminTabDotsPayload = \App\Core\HookDispatcher::applyFilters('admin.tabs.dot_indicators', [
|
||||||
|
'dots' => [],
|
||||||
|
'sections' => $sectionRegistry,
|
||||||
|
'section_state' => $sectionState,
|
||||||
|
'app_root' => $app_root,
|
||||||
|
'user_id' => $userId,
|
||||||
|
]);
|
||||||
|
$adminTabDots = [];
|
||||||
|
if (is_array($adminTabDotsPayload)) {
|
||||||
|
$adminTabDots = $adminTabDotsPayload['dots'] ?? (is_array($adminTabDotsPayload) ? $adminTabDotsPayload : []);
|
||||||
|
}
|
||||||
|
|
||||||
// Get any new feedback messages
|
// Get any new feedback messages
|
||||||
include_once '../app/helpers/feedback.php';
|
include_once '../app/helpers/feedback.php';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,17 @@ if (!empty($modal_to_open)) {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
.tm-admin-tab-label,
|
||||||
|
.tm-admin-subnav-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
.tm-admin-tab-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
@ -145,7 +156,12 @@ if (!empty($adminOverviewStatuses) && is_array($adminOverviewStatuses)) {
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected="<?= $isActive ? 'true' : 'false' ?>"
|
aria-selected="<?= $isActive ? 'true' : 'false' ?>"
|
||||||
aria-controls="tm-admin-tab-<?= htmlspecialchars($sectionKey) ?>">
|
aria-controls="tm-admin-tab-<?= htmlspecialchars($sectionKey) ?>">
|
||||||
<?= htmlspecialchars($tabMeta['label'] ?? ucfirst($sectionKey)) ?>
|
<span class="tm-admin-tab-label">
|
||||||
|
<?= htmlspecialchars($tabMeta['label'] ?? ucfirst($sectionKey)) ?>
|
||||||
|
<?php if (!empty($adminTabDots[$sectionKey])): ?>
|
||||||
|
<span class="tm-admin-tab-dot" aria-hidden="true"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -637,7 +653,7 @@ endif; ?>
|
||||||
if ($hasMigration) {
|
if ($hasMigration) {
|
||||||
foreach ($migrationFiles as $migrationFile) {
|
foreach ($migrationFiles as $migrationFile) {
|
||||||
$migrationContent = file_get_contents($migrationFile);
|
$migrationContent = file_get_contents($migrationFile);
|
||||||
|
|
||||||
// Extract tables created by this migration (plugin-owned)
|
// Extract tables created by this migration (plugin-owned)
|
||||||
if (preg_match_all('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?([a-zA-Z0-9_]+)`?/i', $migrationContent, $matches)) {
|
if (preg_match_all('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?([a-zA-Z0-9_]+)`?/i', $migrationContent, $matches)) {
|
||||||
foreach ($matches[1] as $tableName) {
|
foreach ($matches[1] as $tableName) {
|
||||||
|
|
@ -646,7 +662,7 @@ endif; ?>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all referenced tables (dependencies)
|
// Find all referenced tables (dependencies)
|
||||||
foreach ($allTables as $table) {
|
foreach ($allTables as $table) {
|
||||||
if (strpos($migrationContent, $table) !== false && !in_array($table, $pluginOwnedTables)) {
|
if (strpos($migrationContent, $table) !== false && !in_array($table, $pluginOwnedTables)) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,33 @@
|
||||||
|
<?php
|
||||||
|
$navMainDotsPayload = \App\Core\HookDispatcher::applyFilters('nav.main.dot_indicators', [
|
||||||
|
'dots' => [],
|
||||||
|
'app_root' => $app_root,
|
||||||
|
'user_id' => $userId ?? 0,
|
||||||
|
'db' => $db ?? null,
|
||||||
|
]);
|
||||||
|
$navMainDots = [];
|
||||||
|
if (is_array($navMainDotsPayload)) {
|
||||||
|
$navMainDots = $navMainDotsPayload['dots'] ?? (is_array($navMainDotsPayload) ? $navMainDotsPayload : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
$navMainHasDot = false;
|
||||||
|
if (!empty($navMainDots) && is_array($navMainDots)) {
|
||||||
|
$navMainHasDot = (bool)array_filter($navMainDots, static function($value) {
|
||||||
|
return (bool)$value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$navSettingsDotsPayload = \App\Core\HookDispatcher::applyFilters('nav.settings.dot_indicators', [
|
||||||
|
'dots' => [],
|
||||||
|
'app_root' => $app_root,
|
||||||
|
'user_id' => $userId ?? 0,
|
||||||
|
'db' => $db ?? null,
|
||||||
|
]);
|
||||||
|
$navSettingsDots = [];
|
||||||
|
if (is_array($navSettingsDotsPayload)) {
|
||||||
|
$navSettingsDots = $navSettingsDotsPayload['dots'] ?? (is_array($navSettingsDotsPayload) ? $navSettingsDotsPayload : []);
|
||||||
|
}
|
||||||
|
?>
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
|
|
||||||
<!-- Modern Menu -->
|
<!-- Modern Menu -->
|
||||||
|
|
@ -65,14 +94,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn modern-header-btn dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
<button class="btn modern-header-btn dropdown-toggle position-relative" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-cog"></i>
|
||||||
|
<?php if ($navMainHasDot): ?>
|
||||||
|
<span class="modern-notification-dot" aria-hidden="true"></span>
|
||||||
|
<?php endif; ?>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right modern-dropdown">
|
<div class="dropdown-menu dropdown-menu-right modern-dropdown">
|
||||||
<h6 class="dropdown-header modern-dropdown-header">settings</h6>
|
<h6 class="dropdown-header modern-dropdown-header">settings</h6>
|
||||||
<?php if ($userObject->hasRight($userId, 'superuser')) {?>
|
<?php if ($userObject->hasRight($userId, 'superuser')) {?>
|
||||||
<a class="dropdown-item modern-dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=admin">
|
<a class="dropdown-item modern-dropdown-item" href="<?= htmlspecialchars($app_root) ?>?page=admin">
|
||||||
<i class="fas fa-toolbox"></i>Admin
|
<span class="tm-nav-link-label">
|
||||||
|
<i class="fas fa-toolbox"></i>Admin
|
||||||
|
<?php if (!empty($navSettingsDots['admin'])): ?>
|
||||||
|
<span class="tm-nav-dot" aria-hidden="true"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
<?php if ($userObject->hasRight($userId, 'superuser') ||
|
<?php if ($userObject->hasRight($userId, 'superuser') ||
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ if (!function_exists('logs_ensure_tables')) {
|
||||||
register_hook('logger.system_init', function(array $context) {
|
register_hook('logger.system_init', function(array $context) {
|
||||||
// Ensure tables exist
|
// Ensure tables exist
|
||||||
logs_ensure_tables();
|
logs_ensure_tables();
|
||||||
|
|
||||||
// Load plugin-specific LoggerFactory class
|
// Load plugin-specific LoggerFactory class
|
||||||
require_once __DIR__ . '/models/LoggerFactory.php';
|
require_once __DIR__ . '/models/LoggerFactory.php';
|
||||||
[$logger, $userIP] = LoggerFactory::create($context['db']);
|
[$logger, $userIP] = LoggerFactory::create($context['db']);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ function logs_plugin_handle(string $action, array $context = []): bool {
|
||||||
function logs_plugin_render_list($logObject, $db, int $userId, bool $validSession, string $app_root): void {
|
function logs_plugin_render_list($logObject, $db, int $userId, bool $validSession, string $app_root): void {
|
||||||
// Load User class for permissions check
|
// Load User class for permissions check
|
||||||
$userObject = new \User($db);
|
$userObject = new \User($db);
|
||||||
|
|
||||||
// Check for rights; user or system
|
// Check for rights; user or system
|
||||||
$has_system_access = ($userObject->hasRight($userId, 'superuser') ||
|
$has_system_access = ($userObject->hasRight($userId, 'superuser') ||
|
||||||
$userObject->hasRight($userId, 'view app logs'));
|
$userObject->hasRight($userId, 'view app logs'));
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,29 @@
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* dot notifications in menus */
|
||||||
|
.tm-nav-icon,
|
||||||
|
.tm-nav-link-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.tm-nav-icon .tm-nav-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.15rem;
|
||||||
|
right: -0.35rem;
|
||||||
|
}
|
||||||
|
.tm-nav-dot {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
background: #e53935;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 0 0 2px #fff;
|
||||||
|
}
|
||||||
|
/* dot notifications in menus */
|
||||||
|
|
||||||
.tm-profile-view {
|
.tm-profile-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -215,24 +238,24 @@
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-card-header,
|
.action-card-header,
|
||||||
.action-card-body {
|
.action-card-body {
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-title {
|
.action-title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-form-group {
|
.action-form-group {
|
||||||
margin-bottom: 0.875rem;
|
margin-bottom: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-actions {
|
.action-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-actions .btn {
|
.action-actions .btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -3601,6 +3624,40 @@ body {
|
||||||
color: #4361ee;
|
color: #4361ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* admin dot notifications */
|
||||||
|
.tm-admin-tab-dot {
|
||||||
|
width: 0.45rem;
|
||||||
|
height: 0.45rem;
|
||||||
|
background: #e53935;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 0 0 2px #fff;
|
||||||
|
}
|
||||||
|
.tm-admin-subnav-dot {
|
||||||
|
width: 0.4rem;
|
||||||
|
height: 0.4rem;
|
||||||
|
background: #e53935;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
/* admin dot notifications */
|
||||||
|
|
||||||
|
.modern-notification-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -4px;
|
||||||
|
background: #ef476f;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.modern-notification-badge {
|
.modern-notification-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -4px;
|
top: -4px;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class RateLimitMiddlewareTest extends TestCase
|
||||||
|
|
||||||
// Set up App::db() for RateLimiter
|
// Set up App::db() for RateLimiter
|
||||||
App::set('db', $this->db->getConnection());
|
App::set('db', $this->db->getConnection());
|
||||||
|
|
||||||
// Create rate limiter instance
|
// Create rate limiter instance
|
||||||
$this->rateLimiter = new RateLimiter();
|
$this->rateLimiter = new RateLimiter();
|
||||||
|
|
||||||
|
|
@ -124,10 +124,10 @@ class RateLimitMiddlewareTest extends TestCase
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_whitelist");
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_whitelist");
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_auth");
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_auth");
|
||||||
$this->db->getConnection()->exec("TRUNCATE TABLE log");
|
$this->db->getConnection()->exec("TRUNCATE TABLE log");
|
||||||
|
|
||||||
// Clean up App state
|
// Clean up App state
|
||||||
App::reset('db');
|
App::reset('db');
|
||||||
|
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class RateLimiterTest extends TestCase
|
||||||
|
|
||||||
// Set up App::db() for RateLimiter
|
// Set up App::db() for RateLimiter
|
||||||
App::set('db', $this->db->getConnection());
|
App::set('db', $this->db->getConnection());
|
||||||
|
|
||||||
// The RateLimiter constructor will create all necessary tables
|
// The RateLimiter constructor will create all necessary tables
|
||||||
$this->rateLimiter = new RateLimiter();
|
$this->rateLimiter = new RateLimiter();
|
||||||
}
|
}
|
||||||
|
|
@ -45,10 +45,10 @@ class RateLimiterTest extends TestCase
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->pagesRatelimitTable}");
|
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->pagesRatelimitTable}");
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->blacklistTable}");
|
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->blacklistTable}");
|
||||||
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->whitelistTable}");
|
$this->db->getConnection()->exec("DROP TABLE IF EXISTS {$this->rateLimiter->whitelistTable}");
|
||||||
|
|
||||||
// Clean up App state
|
// Clean up App state
|
||||||
App::reset('db');
|
App::reset('db');
|
||||||
|
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue