diff --git a/CyberCP/secMiddleware.py b/CyberCP/secMiddleware.py
index 3f30e0941..8d58422f6 100644
--- a/CyberCP/secMiddleware.py
+++ b/CyberCP/secMiddleware.py
@@ -38,8 +38,13 @@ class secMiddleware:
import re
webhook_pattern = re.compile(r'^/websites/[^/]+/(webhook|gitNotify)/?$')
- if pathActual == "/backup/localInitiate" or pathActual == '/' or pathActual == '/verifyLogin' or pathActual == '/logout' or pathActual.startswith('/api')\
- or webhook_pattern.match(pathActual) or pathActual.startswith('/cloudAPI') or pathActual.startswith('/static/'):
+ # Public one-time phpMyAdmin launch links (limitedPhpmyAdmin plugin); must work when admin is logged out.
+ _lpma_public_launch = pathActual.startswith('/plugins/limitedPhpmyAdmin/launch/')
+ _lpma_pma_signon = pathActual == '/phpmyadmin/phpmyadminsignin.php'
+
+ if pathActual == "/backup/localInitiate" or pathActual == '/' or pathActual == '/verifyLogin' or pathActual == '/logout' or pathActual.startswith('/api')\
+ or webhook_pattern.match(pathActual) or pathActual.startswith('/cloudAPI') or pathActual.startswith('/static/')\
+ or _lpma_public_launch or _lpma_pma_signon:
pass
else:
# Session check logging removed
@@ -108,6 +113,11 @@ class secMiddleware:
response = self.get_response(request)
return response
+ # phpMyAdmin sign-on POST carries MySQL password; skip character filter (may contain $ ( ) etc.).
+ if pathActual == '/phpmyadmin/phpmyadminsignin.php':
+ response = self.get_response(request)
+ return response
+
# logging.writeToFile(request.body)
try:
data = json.loads(request.body)
diff --git a/plogical/lpma_policy_read.inc.php b/plogical/lpma_policy_read.inc.php
new file mode 100644
index 000000000..2281ab389
--- /dev/null
+++ b/plogical/lpma_policy_read.inc.php
@@ -0,0 +1,134 @@
+ true,
+ 'two_factor' => true,
+ 'features' => true,
+ 'sql' => true,
+ 'navigation' => true,
+ 'main_panel' => true,
+ 'export' => true,
+ 'import' => true,
+ ];
+ $policy = [
+ 'strict_mode' => true,
+ 'blocked_tabs' => $defaultBlocked,
+ ];
+ $paths = [
+ '/usr/local/CyberCP/pluginState/limited_phpmyadmin_policy.json',
+ '/var/lib/cyberpanel-panelstate/limited_phpmyadmin_policy.json',
+ '/etc/cyberpanel/limited_phpmyadmin_policy.json',
+ ];
+ foreach ($paths as $policyPath) {
+ if (! @is_readable($policyPath)) {
+ continue;
+ }
+ $raw = @file_get_contents($policyPath);
+ if ($raw === false) {
+ continue;
+ }
+ $decoded = @json_decode($raw, true);
+ if (! is_array($decoded)) {
+ continue;
+ }
+ $policy['strict_mode'] = isset($decoded['strict_mode']) ? (bool) $decoded['strict_mode'] : true;
+ if (isset($decoded['blocked_tabs']) && is_array($decoded['blocked_tabs'])) {
+ foreach ($defaultBlocked as $k => $_v) {
+ $policy['blocked_tabs'][$k] = isset($decoded['blocked_tabs'][$k])
+ ? (bool) $decoded['blocked_tabs'][$k]
+ : true;
+ }
+ }
+ break;
+ }
+
+ return $policy;
+}
+
+/**
+ * True if a cpma_* request to this application route must be turned away (Settings prefs + main menu targets).
+ * Does not block table browse at route "/sql" (that is Browse, not the SQL runner).
+ */
+function lpma_cpma_route_blocked(string $requestedRoute, array $policy): bool
+{
+ if ($requestedRoute === '') {
+ return false;
+ }
+ $bt = $policy['blocked_tabs'] ?? [];
+ $blocked = static function (string $k) use ($bt): bool {
+ return (($bt[$k] ?? true) === true);
+ };
+
+ if (strpos($requestedRoute, '/preferences') === 0) {
+ $routeToTab = [
+ '/preferences/manage' => 'manage',
+ '/preferences/two-factor' => 'two_factor',
+ '/preferences/features' => 'features',
+ '/preferences/sql' => 'sql',
+ '/preferences/navigation' => 'navigation',
+ '/preferences/main-panel' => 'main_panel',
+ '/preferences/export' => 'export',
+ '/preferences/import' => 'import',
+ ];
+ if (isset($routeToTab[$requestedRoute])) {
+ return $blocked($routeToTab[$requestedRoute]);
+ }
+
+ return (($policy['strict_mode'] ?? true) === true);
+ }
+
+ if ($blocked('sql')) {
+ if (preg_match('#^/(server|database|table)/sql$#', $requestedRoute) === 1) {
+ return true;
+ }
+ if ($requestedRoute === '/database/multi-table-query' || $requestedRoute === '/database/qbe') {
+ return true;
+ }
+ }
+
+ if ($blocked('export') && preg_match('#^/(server|database|table)/export$#', $requestedRoute) === 1) {
+ return true;
+ }
+
+ if ($blocked('import') && preg_match('#^/(server|database|table)/import$#', $requestedRoute) === 1) {
+ return true;
+ }
+
+ if ($blocked('main_panel')) {
+ if (
+ $requestedRoute === '/server/databases'
+ || $requestedRoute === '/server/variables'
+ || $requestedRoute === '/server/collations'
+ ) {
+ return true;
+ }
+ if (strpos($requestedRoute, '/server/status') === 0) {
+ return true;
+ }
+ }
+
+ if ($blocked('features')) {
+ if (
+ $requestedRoute === '/server/engines'
+ || $requestedRoute === '/server/plugins'
+ || $requestedRoute === '/server/binlog'
+ ) {
+ return true;
+ }
+ if (
+ $requestedRoute === '/database/designer'
+ || $requestedRoute === '/database/central-columns'
+ || $requestedRoute === '/database/tracking'
+ || $requestedRoute === '/table/tracking'
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/plogical/phpmyadminsignin.php b/plogical/phpmyadminsignin.php
index 1ac2461e9..051bfd595 100644
--- a/plogical/phpmyadminsignin.php
+++ b/plogical/phpmyadminsignin.php
@@ -3,10 +3,28 @@
define("PMA_SIGNON_INDEX", 1);
+require_once __DIR__ . '/lpma_policy_read.inc.php';
+
try {
define('PMA_SIGNON_SESSIONNAME', 'SignonSession');
define('PMA_DISABLE_SSL_PEER_VALIDATION', TRUE);
+ function lpma_set_strict_cookie($enabled) {
+ $opts = array(
+ 'expires' => $enabled ? (time() + 86400) : (time() - 86400),
+ 'path' => '/phpmyadmin/',
+ 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
+ 'httponly' => true,
+ 'samesite' => 'Lax',
+ );
+ setcookie('PMA_LPMA_STRICT', $enabled ? '1' : '', $opts);
+ }
+
+ function lpma_global_strict_mode_enabled() {
+ $p = lpma_read_limited_policy();
+ return ! empty($p['strict_mode']);
+ }
+
// Handle both GET and POST parameters for token and username
$token = isset($_POST['token']) ? $_POST['token'] : (isset($_GET['token']) ? $_GET['token'] : null);
$username = isset($_POST['username']) ? $_POST['username'] : (isset($_GET['username']) ? $_GET['username'] : null);
@@ -32,6 +50,7 @@ try {
echo '';
} else if (isset($_POST['logout']) || isset($_GET['logout'])) {
+ lpma_set_strict_cookie(false);
session_name(PMA_SIGNON_SESSIONNAME);
@session_start();
$_SESSION = array();
@@ -47,9 +66,14 @@ try {
$username = htmlspecialchars($_POST['username'], ENT_QUOTES, 'UTF-8');
$password = $_POST['password'];
+ $strictMode = (isset($_POST['lpma_strict']) && $_POST['lpma_strict'] === '1');
+ $isLimitedUser = (strpos($username, 'cpma_') === 0);
$host = isset($_POST['host']) ? trim($_POST['host']) : '127.0.0.1';
if ($host === 'localhost') { $host = '127.0.0.1'; }
+ $effectiveStrictMode = ($strictMode || lpma_global_strict_mode_enabled()) && $isLimitedUser;
+ lpma_set_strict_cookie($effectiveStrictMode);
+
$_SESSION['PMA_single_signon_user'] = $username;
$_SESSION['PMA_single_signon_password'] = $password;
$_SESSION['PMA_single_signon_host'] = $host;
diff --git a/public/phpmyadmin/phpmyadminsignin.php b/public/phpmyadmin/phpmyadminsignin.php
index 1ac2461e9..4a2a04b2f 100644
--- a/public/phpmyadmin/phpmyadminsignin.php
+++ b/public/phpmyadmin/phpmyadminsignin.php
@@ -3,10 +3,39 @@
define("PMA_SIGNON_INDEX", 1);
+// Policy helper ships in plogical/ (same layout as phpmyadmin index.php)
+$_lpma_policy = dirname(dirname(__DIR__)) . '/plogical/lpma_policy_read.inc.php';
+if (is_readable($_lpma_policy)) {
+ require_once $_lpma_policy;
+} elseif (is_readable(__DIR__ . '/lpma_policy_read.inc.php')) {
+ require_once __DIR__ . '/lpma_policy_read.inc.php';
+} else {
+ http_response_code(500);
+ header('Content-Type: text/plain; charset=utf-8');
+ echo 'phpMyAdmin sign-on is misconfigured: lpma_policy_read.inc.php is missing.';
+ exit;
+}
+
try {
define('PMA_SIGNON_SESSIONNAME', 'SignonSession');
define('PMA_DISABLE_SSL_PEER_VALIDATION', TRUE);
+ function lpma_set_strict_cookie($enabled) {
+ $opts = array(
+ 'expires' => $enabled ? (time() + 86400) : (time() - 86400),
+ 'path' => '/phpmyadmin/',
+ 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
+ 'httponly' => true,
+ 'samesite' => 'Lax',
+ );
+ setcookie('PMA_LPMA_STRICT', $enabled ? '1' : '', $opts);
+ }
+
+ function lpma_global_strict_mode_enabled() {
+ $p = lpma_read_limited_policy();
+ return ! empty($p['strict_mode']);
+ }
+
// Handle both GET and POST parameters for token and username
$token = isset($_POST['token']) ? $_POST['token'] : (isset($_GET['token']) ? $_GET['token'] : null);
$username = isset($_POST['username']) ? $_POST['username'] : (isset($_GET['username']) ? $_GET['username'] : null);
@@ -32,6 +61,7 @@ try {
echo '';
} else if (isset($_POST['logout']) || isset($_GET['logout'])) {
+ lpma_set_strict_cookie(false);
session_name(PMA_SIGNON_SESSIONNAME);
@session_start();
$_SESSION = array();
@@ -47,9 +77,14 @@ try {
$username = htmlspecialchars($_POST['username'], ENT_QUOTES, 'UTF-8');
$password = $_POST['password'];
+ $strictMode = (isset($_POST['lpma_strict']) && $_POST['lpma_strict'] === '1');
+ $isLimitedUser = (strpos($username, 'cpma_') === 0);
$host = isset($_POST['host']) ? trim($_POST['host']) : '127.0.0.1';
if ($host === 'localhost') { $host = '127.0.0.1'; }
+ $effectiveStrictMode = ($strictMode || lpma_global_strict_mode_enabled()) && $isLimitedUser;
+ lpma_set_strict_cookie($effectiveStrictMode);
+
$_SESSION['PMA_single_signon_user'] = $username;
$_SESSION['PMA_single_signon_password'] = $password;
$_SESSION['PMA_single_signon_host'] = $host;
diff --git a/webmail/services/imap_client.py b/webmail/services/imap_client.py
index 5bd36e043..66c1fdfea 100644
--- a/webmail/services/imap_client.py
+++ b/webmail/services/imap_client.py
@@ -98,7 +98,9 @@ class IMAPClient:
def _folder_type(self, folder_name):
"""Identify special folder type for UI (icons, sidebar grouping).
- CyberPanel/Dovecot uses INBOX.* names; accounts may also use Spam, Trash, etc.
+ CyberPanel/Dovecot uses INBOX.* names, but some accounts also have
+ INBOX.spam, Trash, Archive, etc. Classify those so they are not treated
+ as generic user folders.
"""
fn = (folder_name or '').strip()
if not fn:
diff --git a/webmail/views.py b/webmail/views.py
index 4629fee1d..638534da2 100644
--- a/webmail/views.py
+++ b/webmail/views.py
@@ -5,7 +5,8 @@ from loginSystem.views import loadLoginPage
from .webmailManager import WebmailManager
-# --- Page Views ---
+# ── Page Views ────────────────────────────────────────────────
+
def loadWebmail(request):
try:
wm = WebmailManager(request)
@@ -23,7 +24,8 @@ def loadLogin(request):
return wm.loadLogin()
-# --- Auth APIs ---
+# ── Auth APIs ─────────────────────────────────────────────────
+
def apiLogin(request):
try:
wm = WebmailManager(request)
@@ -70,7 +72,8 @@ def apiSwitchAccount(request):
return _error_response(e)
-# --- Folder APIs ---
+# ── Folder APIs ───────────────────────────────────────────────
+
def apiListFolders(request):
try:
wm = WebmailManager(request)
@@ -111,7 +114,8 @@ def apiDeleteFolder(request):
return _error_response(e)
-# --- Message APIs ---
+# ── Message APIs ──────────────────────────────────────────────
+
def apiListMessages(request):
try:
wm = WebmailManager(request)
@@ -152,7 +156,8 @@ def apiGetAttachment(request):
return _error_response(e)
-# --- Action APIs ---
+# ── Action APIs ───────────────────────────────────────────────
+
def apiSendMessage(request):
try:
wm = WebmailManager(request)
@@ -223,7 +228,8 @@ def apiMarkFlagged(request):
return _error_response(e)
-# --- Contact APIs ---
+# ── Contact APIs ──────────────────────────────────────────────
+
def apiListContacts(request):
try:
wm = WebmailManager(request)
@@ -323,7 +329,8 @@ def apiImportRulesFromSnappymail(request):
return _error_response(e)
-# --- Sieve Rule APIs ---
+# ── Sieve Rule APIs ──────────────────────────────────────────
+
def apiListRules(request):
try:
wm = WebmailManager(request)
@@ -374,7 +381,8 @@ def apiActivateRules(request):
return _error_response(e)
-# --- Settings APIs ---
+# ── Settings APIs ─────────────────────────────────────────────
+
def apiGetSettings(request):
try:
wm = WebmailManager(request)
@@ -395,7 +403,8 @@ def apiSaveSettings(request):
return _error_response(e)
-# --- Image Proxy ---
+# ── Image Proxy ───────────────────────────────────────────────
+
def apiProxyImage(request):
try:
wm = WebmailManager(request)
@@ -404,7 +413,8 @@ def apiProxyImage(request):
return _error_response(e)
-# --- Helpers ---
+# ── Helpers ───────────────────────────────────────────────────
+
def _error_response(e):
data = {'status': 0, 'error_message': str(e)}
return HttpResponse(json.dumps(data), content_type='application/json')