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/CyberCP/settings.py b/CyberCP/settings.py
index c5ce2e71f..8d59217a6 100644
--- a/CyberCP/settings.py
+++ b/CyberCP/settings.py
@@ -159,6 +159,7 @@ TEMPLATES = [
'baseTemplate.context_processors.notification_preferences_context',
'baseTemplate.context_processors.firewall_static_context',
'baseTemplate.context_processors.dns_static_context',
+ 'baseTemplate.context_processors.plugin_sidebar_context',
],
},
},
diff --git a/baseTemplate/context_processors.py b/baseTemplate/context_processors.py
index 83215a38a..911f698f0 100644
--- a/baseTemplate/context_processors.py
+++ b/baseTemplate/context_processors.py
@@ -102,4 +102,84 @@ def dns_static_context(request):
version = int(time.time())
return {
'DNS_STATIC_VERSION': version
- }
\ No newline at end of file
+ }
+
+
+def plugin_sidebar_context(request):
+ """
+ Sidebar Plugins menu: Installed / Plugin Store (ACL), Limited phpMyAdmin link
+ for admins, plugin managers, or cpuser grantees; grant-only users see LPMA only.
+ """
+ defaults = {
+ 'lpma_plugin_installed': False,
+ 'lpma_has_cpuser_grant': False,
+ 'show_plugin_management_links': False,
+ 'show_lpma_sidebar_link': False,
+ 'show_plugins_menu': False,
+ 'grant_only_lpma_sidebar': False,
+ 'plugin_sidebar_extra_links': [],
+ 'lpma_sidebar_url': '/plugins/limitedPhpmyAdmin/',
+ }
+ try:
+ if 'userID' not in request.session:
+ return defaults
+ uid = request.session['userID']
+ from plogical.acl import ACLManager
+
+ acl = ACLManager.loadedACL(uid)
+ admin = bool(acl.get('admin'))
+ manage_plugins = bool(acl.get('managePlugins'))
+ show_plugin_management_links = admin or manage_plugins
+
+ lpma_plugin_installed = os.path.isdir('/usr/local/CyberCP/limitedPhpmyAdmin')
+
+ lpma_has_cpuser_grant = False
+ if lpma_plugin_installed:
+ try:
+ from django.apps import apps
+
+ if apps.is_installed('limitedPhpmyAdmin'):
+ from limitedPhpmyAdmin.models import LimitedPhpmyAdminGrant
+
+ lpma_has_cpuser_grant = LimitedPhpmyAdminGrant.objects.filter(
+ enabled=True,
+ subject_type='cpuser',
+ administrator_id=uid,
+ ).exists()
+ except Exception:
+ lpma_has_cpuser_grant = False
+
+ show_lpma_sidebar_link = lpma_plugin_installed and (
+ show_plugin_management_links or lpma_has_cpuser_grant
+ )
+ show_plugins_menu = show_plugin_management_links or (
+ lpma_has_cpuser_grant and lpma_plugin_installed
+ )
+ grant_only_lpma_sidebar = (
+ lpma_has_cpuser_grant and lpma_plugin_installed and not show_plugin_management_links
+ )
+
+ extra = []
+ if show_lpma_sidebar_link:
+ extra.append(
+ {
+ 'url': '/plugins/limitedPhpmyAdmin/',
+ 'label_key': 'limited_phpmyadmin_sidebar',
+ }
+ )
+
+ defaults.update(
+ {
+ 'lpma_plugin_installed': lpma_plugin_installed,
+ 'lpma_has_cpuser_grant': lpma_has_cpuser_grant,
+ 'show_plugin_management_links': show_plugin_management_links,
+ 'show_lpma_sidebar_link': show_lpma_sidebar_link,
+ 'show_plugins_menu': show_plugins_menu,
+ 'grant_only_lpma_sidebar': grant_only_lpma_sidebar,
+ 'plugin_sidebar_extra_links': extra,
+ 'lpma_sidebar_url': '/plugins/limitedPhpmyAdmin/',
+ }
+ )
+ return defaults
+ except Exception:
+ return defaults
\ No newline at end of file
diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html
index bcd21031b..a090cfd21 100644
--- a/baseTemplate/templates/baseTemplate/index.html
+++ b/baseTemplate/templates/baseTemplate/index.html
@@ -2396,8 +2396,8 @@
{% endif %}
{% endif %}
- {% if admin or managePlugins %}
- {% if managePlugins and not admin %}
+ {% if show_plugins_menu %}
+ {% if grant_only_lpma_sidebar or managePlugins and not admin %}
{% endif %}
{% endif %}
diff --git a/baseTemplate/templates/baseTemplate/versionManagment.html b/baseTemplate/templates/baseTemplate/versionManagment.html
index 202db35ec..3d1d745bd 100644
--- a/baseTemplate/templates/baseTemplate/versionManagment.html
+++ b/baseTemplate/templates/baseTemplate/versionManagment.html
@@ -165,6 +165,72 @@
font-weight: 600;
}
+ .alert-info {
+ background: var(--bg-hover, #eef0ff);
+ border: 1px solid var(--border-color, #c7c9f0);
+ color: var(--text-primary, #2f3640);
+ }
+
+ .alert-info a {
+ color: var(--accent-color, #5b5fcf);
+ font-weight: 600;
+ }
+
+ .version-section {
+ margin-top: 20px;
+ margin-bottom: 10px;
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--text-primary, #2f3640);
+ }
+
+ .version-section:first-of-type {
+ margin-top: 0;
+ }
+
+ .version-meta-row {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 100%;
+ margin-bottom: 14px;
+ padding: 12px 14px;
+ border-radius: 8px;
+ border: 1px solid var(--border-color, #e8e9ff);
+ background: var(--bg-hover, #f8f9ff);
+ }
+
+ .version-meta-row-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-secondary, #8893a7);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .version-meta-row-value {
+ font-size: 14px;
+ color: var(--text-primary, #2f3640);
+ word-break: break-word;
+ line-height: 1.5;
+ }
+
+ .version-meta-row-value code,
+ .version-sha {
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: 13px;
+ background: var(--bg-secondary, white);
+ padding: 2px 8px;
+ border-radius: 4px;
+ border: 1px solid var(--border-color, #e8e9ff);
+ }
+
+ .version-meta-row-value a {
+ font-weight: 600;
+ color: var(--accent-color, #5b5fcf);
+ margin-left: 4px;
+ }
+
/* Info grid */
.info-grid {
display: grid;
@@ -269,7 +335,21 @@
{% if Notecheck %}
+ {% endif %}
+ {% if show_fork_block and fork_drift_upstream %}
+
+
+ {% trans "Info: Your fork branch tip on GitHub differs from the official upstream tip on the same branch. That is normal if you have not merged upstream yet." %}
+
+ {% elif local_behind_official and not Notecheck %}
+
+
+ {% trans "Your commit differs from the latest on the official CyberPanel repository (usmannasir/cyberpanel). This is informational if you intentionally track a fork." %}
{% endif %}
@@ -294,8 +374,9 @@
-
Version Information
+
{% trans "Version Information" %}
+
{% trans "This installation" %}
{% trans "Current Version" %}
@@ -306,22 +387,61 @@
{{ build }}
- {% trans "Current Commit" %}
- {{ Currentcomt }}
+ {% trans "Current commit" %}
+
+ {% if Currentcomt_short %}{{ Currentcomt_short }}{% else %}—{% endif %}
+
- {% trans "Latest Version" %}
+ {% trans "Published latest version" %}
{{ latestVersion }}
- {% trans "Latest Build" %}
+ {% trans "Published latest build" %}
{{ latestBuild }}
-
- {% trans "Latest Commit" %}
- {{ latestcomit }}
-
+
+
{% trans "Git remotes and branch tips" %}
+
+ {% trans "Origin (git remote)" %}
+ {{ remote_display|default:"—" }}
+
+
+ {% trans "Tracking branch" %}
+ {{ branch_ref }}
+
+
+ {% if fork_remote_commit %}
+
+ {% endif %}
+
+ {% if upstream_commit %}
+
+ {% elif latestcomit and not fork_remote_commit and not on_dev_branch %}
+
+ {% trans "Latest commit on origin (comparison)" %}
+
+ {{ latestcomit_short }}
+
+
+ {% endif %}
diff --git a/baseTemplate/views.py b/baseTemplate/views.py
index 510311506..871a8551e 100644
--- a/baseTemplate/views.py
+++ b/baseTemplate/views.py
@@ -55,6 +55,34 @@ def _version_compare(a, b):
return 0
+def _parse_github_origin(remote_out):
+ """Return (owner, repo) or (None, None) if unparseable."""
+ if not remote_out:
+ return None, None
+ m = re.search(r'github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$', remote_out.strip())
+ if not m:
+ return None, None
+ owner, repo = m.group(1), m.group(2).rstrip('.git')
+ return owner, repo
+
+
+def _github_branch_tip_sha(owner, repo, branch_ref):
+ """First commit SHA on branch via GitHub API, or empty string on failure."""
+ try:
+ u = 'https://api.github.com/repos/%s/%s/commits?sha=%s' % (owner, repo, branch_ref)
+ r = requests.get(u, timeout=10)
+ r.raise_for_status()
+ data = r.json()
+ if not data:
+ return ''
+ sha = data[0].get('sha', '') or ''
+ return sha
+ except (requests.RequestException, IndexError, KeyError, TypeError) as e:
+ logging.CyberCPLogFileWriter.writeToFile(
+ '[versionManagment] GitHub API %s/%s @%s failed: %s' % (owner, repo, branch_ref, str(e)))
+ return ''
+
+
@ensure_csrf_cookie
def renderBase(request):
template = 'baseTemplate/homePage.html'
@@ -67,49 +95,8 @@ def renderBase(request):
@ensure_csrf_cookie
def versionManagement(request):
- currentVersion = VERSION
- currentBuild = str(BUILD)
-
- getVersion = requests.get('https://cyberpanel.net/version.txt')
- latest = getVersion.json()
- latestVersion = latest['version']
- latestBuild = latest['build']
- branch_ref = 'v%s.%s' % (latestVersion, latestBuild)
-
- notechk = True
- Currentcomt = ''
- latestcomit = ''
-
- if _version_compare(currentVersion, latestVersion) > 0:
- notechk = False
- else:
- remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true'
- remote_out = ProcessUtilities.outputExecutioner(remote_cmd)
- is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '')
-
- if is_usmannasir:
- u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % branch_ref
- logging.CyberCPLogFileWriter.writeToFile(u)
- try:
- r = requests.get(u, timeout=10)
- r.raise_for_status()
- latestcomit = r.json()[0]['sha']
- except (requests.RequestException, IndexError, KeyError) as e:
- logging.CyberCPLogFileWriter.writeToFile('[versionManagement] GitHub API failed: %s' % str(e))
- head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true'
- Currentcomt = ProcessUtilities.outputExecutioner(head_cmd).rstrip('\n')
- if latestcomit and Currentcomt == latestcomit:
- notechk = False
- else:
- notechk = False
-
- template = 'baseTemplate/versionManagment.html'
- finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion,
- 'latestBuild': latestBuild, 'latestcomit': latestcomit, "Currentcomt": Currentcomt,
- "Notecheck": notechk}
-
- proc = httpProc(request, template, finalData, 'versionManagement')
- return proc.render()
+ """Legacy entrypoint; same UI as versionManagment (URLs use versionManagment)."""
+ return versionManagment(request)
@ensure_csrf_cookie
@@ -297,6 +284,13 @@ def versionManagment(request):
latestcomit = ''
latestVersion = '0'
latestBuild = '0'
+ upstream_latest_sha = ''
+ fork_latest_sha = ''
+ show_fork_block = False
+ fork_display = ''
+ fork_commit_url = ''
+ upstream_commit_url = ''
+ fork_drift_upstream = False
on_dev_branch = (currentVersion == '2.5.5' and currentBuild == 'dev')
@@ -311,58 +305,109 @@ def versionManagment(request):
if on_dev_branch:
latestVersion, latestBuild = '2.5.5', 'dev'
- # Dev branch: compare against v2.5.5-dev, show dev version info
if on_dev_branch:
branch_ref = 'v2.5.5-dev'
latestVersion, latestBuild = '2.5.5', 'dev'
else:
branch_ref = 'v%s.%s' % (latestVersion, latestBuild)
- # Always fetch local HEAD for display
head_cmd = 'git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || true'
Currentcomt = (ProcessUtilities.outputExecutioner(head_cmd) or '').rstrip('\n')
remote_cmd = 'git -C /usr/local/CyberCP remote get-url origin 2>/dev/null || true'
- remote_out = ProcessUtilities.outputExecutioner(remote_cmd)
- is_usmannasir = 'usmannasir/cyberpanel' in (remote_out or '')
+ remote_out = (ProcessUtilities.outputExecutioner(remote_cmd) or '')
+ origin_owner, origin_repo = _parse_github_origin(remote_out)
+ if origin_owner and origin_repo:
+ remote_display = '%s/%s' % (origin_owner, origin_repo)
+ is_official = (origin_owner.lower() == 'usmannasir' and origin_repo.lower() == 'cyberpanel')
+ show_fork_block = not is_official
+ if show_fork_block:
+ fork_display = remote_display
+ else:
+ remote_display = (remote_out.strip() or '—')
+ logging.CyberCPLogFileWriter.writeToFile(
+ '[versionManagment] Unparseable git origin, upstream-only UI: %s'
+ % (remote_out[:200] if remote_out else '(empty)'))
+ show_fork_block = False
+
+ upstream_latest_sha = _github_branch_tip_sha('usmannasir', 'cyberpanel', branch_ref)
+ latestcomit = upstream_latest_sha
+ if upstream_latest_sha:
+ upstream_commit_url = 'https://github.com/usmannasir/cyberpanel/commit/%s' % upstream_latest_sha
+
+ if show_fork_block and origin_owner and origin_repo:
+ fork_latest_sha = _github_branch_tip_sha(origin_owner, origin_repo, branch_ref)
+ if fork_latest_sha:
+ fork_commit_url = 'https://github.com/%s/%s/commit/%s' % (
+ origin_owner, origin_repo, fork_latest_sha)
+
+ if (fork_latest_sha and upstream_latest_sha and fork_latest_sha != upstream_latest_sha):
+ fork_drift_upstream = True
- # Stable: newer than cyberpanel.net = up to date; dev: compare commits
if not on_dev_branch and notechk and _version_compare(currentVersion, latestVersion) > 0:
notechk = False
elif notechk:
- # Dev branch: always use usmannasir v2.5.5-dev as canonical "latest"
- # Forks: use usmannasir for Latest Commit so all dev users compare to same upstream
- fetch_branch = branch_ref if (is_usmannasir or on_dev_branch) else None
- if fetch_branch:
- u = "https://api.github.com/repos/usmannasir/cyberpanel/commits?sha=%s" % fetch_branch
- logging.CyberCPLogFileWriter.writeToFile(u)
- try:
- r = requests.get(u, timeout=10)
- r.raise_for_status()
- latestcomit = r.json()[0]['sha']
- if Currentcomt and latestcomit and Currentcomt == latestcomit:
- notechk = False
- except (requests.RequestException, IndexError, KeyError) as e:
- logging.CyberCPLogFileWriter.writeToFile('[versionManagment] GitHub API failed: %s' % str(e))
- elif not on_dev_branch:
- # Stable fork: fetch from fork's branch
- m = re.search(r'github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$', (remote_out or '').strip())
- if m:
- owner, repo = m.group(1), m.group(2).rstrip('.git')
- try:
- u = "https://api.github.com/repos/%s/%s/commits?sha=%s" % (owner, repo, branch_ref)
- r = requests.get(u, timeout=10)
- r.raise_for_status()
- latestcomit = r.json()[0]['sha']
- if Currentcomt and latestcomit and Currentcomt == latestcomit:
- notechk = False
- except (requests.RequestException, IndexError, KeyError):
- pass
+ cur = (Currentcomt or '').strip().lower()
+ if show_fork_block:
+ fk = (fork_latest_sha or '').strip().lower()
+ up = (upstream_latest_sha or '').strip().lower()
+ if fk:
+ notechk = not (bool(cur) and cur == fk)
+ elif up:
+ notechk = not (bool(cur) and cur == up)
+ else:
+ notechk = False
+ else:
+ up = (upstream_latest_sha or '').strip().lower()
+ if up:
+ notechk = not (bool(cur) and cur == up)
+ else:
+ notechk = False
+
+ is_usmannasir = not show_fork_block
+ fork_remote_commit = fork_latest_sha if show_fork_block else ''
+ upstream_commit = upstream_latest_sha
+ notecheck_compare_remote = fork_display if show_fork_block else 'usmannasir/cyberpanel'
+ local_behind_official = bool(
+ on_dev_branch and Currentcomt and upstream_commit and Currentcomt != upstream_commit)
+
+ def _short_sha(commit_hash):
+ if not commit_hash or len(commit_hash) < 7:
+ return commit_hash or ''
+ return commit_hash[:7]
template = 'baseTemplate/versionManagment.html'
- finalData = {'build': currentBuild, 'currentVersion': currentVersion, 'latestVersion': latestVersion,
- 'latestBuild': latestBuild, 'latestcomit': latestcomit, "Currentcomt": Currentcomt,
- "Notecheck": notechk}
+ finalData = {
+ 'build': currentBuild,
+ 'currentVersion': currentVersion,
+ 'latestVersion': latestVersion,
+ 'latestBuild': latestBuild,
+ 'latestcomit': latestcomit,
+ 'Currentcomt': Currentcomt,
+ 'Notecheck': notechk,
+ 'show_fork_block': show_fork_block,
+ 'tracking_branch': branch_ref,
+ 'branch_ref': branch_ref,
+ 'fork_display': fork_display,
+ 'fork_latest_sha': fork_latest_sha,
+ 'upstream_latest_sha': upstream_latest_sha,
+ 'fork_commit_url': fork_commit_url,
+ 'upstream_commit_url': upstream_commit_url,
+ 'fork_drift_upstream': fork_drift_upstream,
+ 'remote_display': remote_display,
+ 'is_usmannasir': is_usmannasir,
+ 'fork_remote_commit': fork_remote_commit,
+ 'upstream_commit': upstream_commit,
+ 'notecheck_compare_remote': notecheck_compare_remote,
+ 'local_behind_official': local_behind_official,
+ 'on_dev_branch': on_dev_branch,
+ 'Currentcomt_short': _short_sha(Currentcomt),
+ 'latestcomit_short': _short_sha(latestcomit),
+ 'fork_remote_commit_short': _short_sha(fork_remote_commit),
+ 'upstream_commit_short': _short_sha(upstream_commit),
+ 'fork_latest_sha_short': _short_sha(fork_latest_sha),
+ 'upstream_latest_sha_short': _short_sha(upstream_latest_sha),
+ }
proc = httpProc(request, template, finalData, 'versionManagement')
return proc.render()
diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh
index 5310008d9..212cfb199 100644
--- a/cyberpanel_upgrade.sh
+++ b/cyberpanel_upgrade.sh
@@ -102,6 +102,15 @@ Pre_Upgrade_Setup_Repository
Pre_Upgrade_Setup_Git_URL
Pre_Upgrade_Required_Components
Main_Upgrade
-Sync_CyberCP_To_Latest
+CYBERPANEL_GIT_SYNC_OK=0
+if Sync_CyberCP_To_Latest; then
+ CYBERPANEL_GIT_SYNC_OK=1
+else
+ echo -e "\e[31m[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: Git sync of /usr/local/CyberCP to origin/$Branch_Name failed. Panel code may be outdated. See /var/log/cyberpanel_upgrade_debug.log and /etc/cyberpanel/last_git_sync_failed\e[0m" | tee -a /var/log/cyberpanel_upgrade_debug.log
+fi
+export CYBERPANEL_GIT_SYNC_OK
Post_Upgrade_System_Tweak
Post_Install_Display_Final_Info
+if [[ "$CYBERPANEL_GIT_SYNC_OK" -ne 1 ]]; then
+ exit 1
+fi
diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py
index 10a0fc913..126a9c35e 100644
--- a/firewall/firewallManager.py
+++ b/firewall/firewallManager.py
@@ -39,6 +39,174 @@ class FirewallManager:
def __init__(self, request = None):
self.request = request
+ @staticmethod
+ def _normalize_banned_ip_key(ip):
+ return (ip or '').strip().lower()
+
+ def _autoban_duration_seconds(self, duration):
+ duration_map = {'1h': 3600, '24h': 86400, '7d': 604800, '30d': 2592000}
+ d = (duration or '').strip().lower()
+ if d == 'permanent' or not d:
+ return None
+ return duration_map.get(d, 86400)
+
+ def _autoban_log_expired(self, log, current_time):
+ """True if timed ban from AutoBanLog is past expiry (permanent never expires here)."""
+ d = (log.ban_duration or '').strip().lower()
+ if d == 'permanent' or not d:
+ return False
+ sec = self._autoban_duration_seconds(log.ban_duration)
+ if sec is None:
+ return False
+ if not log.banned_at:
+ return False
+ try:
+ ts = int(log.banned_at.timestamp())
+ except Exception:
+ return False
+ return (ts + sec) <= int(current_time)
+
+ def _merge_autoban_security_alerts_logs(self, active_banned_ips, current_time):
+ """
+ List Auto Ban Security Alerts log rows whose IP is not already in the firewall
+ merged list (DB + JSON), so /firewall/#banned-ips matches the plugin settings view.
+ """
+ try:
+ from django.apps import apps
+
+ if not apps.is_installed('autoBanSecurityAlerts'):
+ return
+ from autoBanSecurityAlerts.models import AutoBanLog
+ except Exception:
+ return
+
+ seen = set()
+ for row in active_banned_ips:
+ k = self._normalize_banned_ip_key(row.get('ip') or row.get('ip_address'))
+ if k:
+ seen.add(k)
+
+ try:
+ autoban_by_ip = {}
+ for log in AutoBanLog.objects.order_by('-banned_at'):
+ k = self._normalize_banned_ip_key(str(log.ip_address))
+ if not k:
+ continue
+ if k not in autoban_by_ip:
+ autoban_by_ip[k] = log
+
+ for _k, log in autoban_by_ip.items():
+ ip_key = self._normalize_banned_ip_key(str(log.ip_address))
+ if ip_key in seen:
+ continue
+ if self._autoban_log_expired(log, current_time):
+ continue
+ banned_on_str = 'N/A'
+ if log.banned_at:
+ try:
+ banned_on_str = log.banned_at.strftime('%Y-%m-%d %H:%M:%S')
+ except Exception:
+ banned_on_str = str(log.banned_at)
+ expires_display = 'Never'
+ dlow = (log.ban_duration or '').strip().lower()
+ if dlow != 'permanent' and dlow:
+ sec = self._autoban_duration_seconds(log.ban_duration)
+ if sec is not None and log.banned_at:
+ try:
+ from datetime import datetime
+
+ exp_ts = int(log.banned_at.timestamp()) + sec
+ expires_display = datetime.fromtimestamp(exp_ts).strftime(
+ '%Y-%m-%d %H:%M:%S'
+ )
+ except Exception:
+ expires_display = 'Never'
+ active_banned_ips.append(
+ {
+ 'id': 'ablog-%s' % log.pk,
+ 'ip': str(log.ip_address),
+ 'reason': log.ban_reason or '',
+ 'duration': log.ban_duration or 'permanent',
+ 'banned_on': banned_on_str,
+ 'expires': expires_display,
+ 'active': True,
+ 'ban_source': 'autoBanSecurityAlerts',
+ }
+ )
+ except Exception as e:
+ logging.CyberCPLogFileWriter.writeToFile(
+ 'getBannedIPs: AutoBanSecurityAlerts merge failed: %s' % str(e)
+ )
+
+ def _remove_autoban_log_by_suffix_id(self, userID, ablog_id_str):
+ """Handle unban when UI sends id like ablog-123 (orphan log-only row)."""
+ try:
+ pk = int(str(ablog_id_str).split('-', 1)[1])
+ except Exception:
+ final_dic = {'status': 0, 'error_message': 'Invalid auto-ban log id'}
+ return HttpResponse(json.dumps(final_dic), content_type='application/json')
+ try:
+ admin = Administrator.objects.get(pk=userID)
+ if admin.acl.adminStatus != 1:
+ return ACLManager.loadError()
+ from django.apps import apps
+
+ if not apps.is_installed('autoBanSecurityAlerts'):
+ final_dic = {'status': 0, 'error_message': 'Auto Ban plugin is not installed'}
+ return HttpResponse(json.dumps(final_dic), content_type='application/json')
+ from autoBanSecurityAlerts.models import AutoBanLog
+
+ log = AutoBanLog.objects.filter(pk=pk).first()
+ if not log:
+ final_dic = {'status': 0, 'error_message': 'Auto-ban log entry not found'}
+ return HttpResponse(json.dumps(final_dic), content_type='application/json')
+ ip = str(log.ip_address).strip()
+ admin0 = Administrator.objects.filter(acl__adminStatus=1).first()
+ if not admin0:
+ final_dic = {'status': 0, 'error_message': 'No admin user found'}
+ return HttpResponse(json.dumps(final_dic), content_type='application/json')
+
+ result = self.removeBannedIP(admin0.pk, {'ip': ip})
+ raw = getattr(result, 'content', None)
+ parsed = {}
+ if raw is not None:
+ try:
+ if isinstance(raw, bytes):
+ raw = raw.decode('utf-8', errors='replace')
+ parsed = json.loads(raw)
+ except Exception:
+ parsed = {}
+ err_raw = (parsed.get('error_message') or parsed.get('error') or '').strip()
+ err_msg = err_raw.lower()
+
+ if parsed.get('status') == 1:
+ log.delete()
+ return HttpResponse(
+ json.dumps(
+ {
+ 'status': 1,
+ 'message': parsed.get('message', 'IP unbanned successfully'),
+ }
+ ),
+ content_type='application/json',
+ )
+ if 'not found' in err_msg:
+ log.delete()
+ return HttpResponse(
+ json.dumps(
+ {
+ 'status': 1,
+ 'message': 'Log cleared (ban was already removed or not found in firewall)',
+ }
+ ),
+ content_type='application/json',
+ )
+ final_dic = {'status': 0, 'error_message': err_raw or 'Failed to remove ban'}
+ return HttpResponse(json.dumps(final_dic), content_type='application/json')
+ except BaseException as msg:
+ final_dic = {'status': 0, 'error_message': str(msg)}
+ return HttpResponse(json.dumps(final_dic), content_type='application/json')
+
def _load_banned_ips_store(self):
"""
Load banned IPs from the primary store, falling back to legacy path.
@@ -2094,6 +2262,9 @@ class FirewallManager:
'active': True
})
+ # Auto Ban Security Alerts: show log-only / plugin-visible bans on same list as firewall UI
+ self._merge_autoban_security_alerts_logs(active_banned_ips, current_time)
+
# Optional server-side search: filter by IP or reason (case-insensitive substring)
# Normalize: strip query and compare against stripped IP/reason so "1.2.3.4" matches stored " 1.2.3.4 "
search_q = (data.get('search') or data.get('q') or '').strip() if data else ''
@@ -2296,6 +2467,9 @@ class FirewallManager:
return ACLManager.loadError()
banned_ip_id = data.get('id')
+ if isinstance(banned_ip_id, str) and banned_ip_id.startswith('ablog-'):
+ return self._remove_autoban_log_by_suffix_id(userID, banned_ip_id)
+
requested_ip = (data.get('ip') or '').strip()
ip_to_unban = None
@@ -2388,6 +2562,9 @@ class FirewallManager:
return ACLManager.loadError()
banned_ip_id = data.get('id')
+ if isinstance(banned_ip_id, str) and banned_ip_id.startswith('ablog-'):
+ return self._remove_autoban_log_by_suffix_id(userID, banned_ip_id)
+
requested_ip = (data.get('ip') or '').strip()
try:
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/plogical/upgrade.py b/plogical/upgrade.py
index f728108ef..8241e1c1f 100644
--- a/plogical/upgrade.py
+++ b/plogical/upgrade.py
@@ -4577,17 +4577,22 @@ class Migration(migrations.Migration):
# Change to parent directory
os.chdir('/usr/local')
- # Remove old CyberCP directory
+ # Remove old CyberCP directory (quarantine if rmtree fails — e.g. busy files)
if os.path.exists('CyberCP'):
Upgrade.stdOut("Removing old CyberCP directory...")
try:
shutil.rmtree('CyberCP')
Upgrade.stdOut("Old CyberCP directory removed successfully.")
- except Exception as e:
- Upgrade.stdOut(f"Error removing CyberCP directory: {str(e)}")
- # Try to restore backup if removal fails
- Upgrade.restoreCriticalFiles(backup_dir, backed_up_files)
- return 0, 'Failed to remove old CyberCP directory'
+ except OSError as e:
+ Upgrade.stdOut("rmtree failed (%s); quarantining old CyberCP..." % str(e))
+ quarantine = '/usr/local/CyberCP.legacy.%s' % int(time.time())
+ try:
+ shutil.move('CyberCP', quarantine)
+ Upgrade.stdOut("Moved old tree to %s" % quarantine)
+ except OSError as e2:
+ Upgrade.stdOut(f"Error removing or moving CyberCP directory: {str(e2)}")
+ Upgrade.restoreCriticalFiles(backup_dir, backed_up_files)
+ return 0, 'Failed to remove or quarantine old CyberCP directory'
# Clone the new repository (use CYBERPANEL_GIT_USER for fork, e.g. master3395)
git_user = os.environ.get('CYBERPANEL_GIT_USER', 'master3395')
@@ -4612,10 +4617,15 @@ class Migration(migrations.Migration):
if os.path.exists('CyberCP'):
try:
shutil.rmtree('CyberCP')
- except Exception as e:
- Upgrade.stdOut("Error removing CyberCP: %s" % str(e))
- Upgrade.restoreCriticalFiles(backup_dir, backed_up_files)
- return 0, 'Failed to remove CyberCP for upstream clone'
+ except OSError as e:
+ Upgrade.stdOut("rmtree failed (%s); quarantining..." % str(e))
+ quarantine = '/usr/local/CyberCP.legacy.%s' % int(time.time())
+ try:
+ shutil.move('CyberCP', quarantine)
+ except OSError as e2:
+ Upgrade.stdOut("Error removing CyberCP: %s" % str(e2))
+ Upgrade.restoreCriticalFiles(backup_dir, backed_up_files)
+ return 0, 'Failed to remove CyberCP for upstream clone'
command = 'git clone https://github.com/%s/cyberpanel CyberCP' % upstream_user
if not Upgrade.executioner(command, command, 1):
Upgrade.restoreCriticalFiles(backup_dir, backed_up_files)
@@ -6581,7 +6591,31 @@ slowlog = /var/log/php{version}-fpm-slow.log
# execPath = execPath + " removeCSF"
# Upgrade.executioner(execPath, 'fix csf if there', 0)
- Upgrade.downloadAndUpgrade(versionNumbring, branch)
+ clone_attempts = int(os.environ.get('CYBERPANEL_UPGRADE_CLONE_ATTEMPTS', '2'))
+ if clone_attempts < 1:
+ clone_attempts = 1
+ download_ok = False
+ download_err = None
+ for attempt in range(1, clone_attempts + 1):
+ ok, download_err = Upgrade.downloadAndUpgrade(versionNumbring, branch)
+ if ok:
+ download_ok = True
+ break
+ Upgrade.stdOut(
+ 'downloadAndUpgrade failed (attempt %d/%d): %s'
+ % (attempt, clone_attempts, download_err or 'unknown'), 0)
+ if attempt < clone_attempts:
+ Upgrade.stdOut('Retrying full CyberPanel tree replacement in 5 seconds...', 0)
+ time.sleep(5)
+ if not download_ok:
+ Upgrade.stdOut(
+ 'CRITICAL: CyberPanel code update failed after %d attempt(s): %s. '
+ 'The panel was not replaced with the new branch. Check logs and disk permissions on /usr/local.'
+ % (clone_attempts, download_err or 'unknown'), 0)
+ if Upgrade.SoftUpgrade == 0:
+ Upgrade.executioner('systemctl start lscpd', 'Start LSCPD after failed code update', 0)
+ sys.exit(1)
+
versionNumbring = Upgrade.downloadLink()
Upgrade.download_install_phpmyadmin()
Upgrade.downoad_and_install_raindloop()
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/upgrade_modules/08_main_upgrade.sh b/upgrade_modules/08_main_upgrade.sh
index 101cf6b31..a0638cb0f 100644
--- a/upgrade_modules/08_main_upgrade.sh
+++ b/upgrade_modules/08_main_upgrade.sh
@@ -30,6 +30,8 @@ echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Running: $CP_PYTHON upgrade.py $Branch_N
# Export Git user so upgrade.py clones from the same repo (master3395 or --repo override)
export CYBERPANEL_GIT_USER="${Git_User:-master3395}"
+# Retry full /usr/local/CyberCP re-clone this many times if download/checkout fails (default 2)
+export CYBERPANEL_UPGRADE_CLONE_ATTEMPTS="${CYBERPANEL_UPGRADE_CLONE_ATTEMPTS:-2}"
# Run from directory that contains upgrade.py (downloaded by Pre_Upgrade_Required_Components)
for d in /root/cyberpanel_upgrade_tmp /usr/local/CyberCP; do
diff --git a/upgrade_modules/09_sync.sh b/upgrade_modules/09_sync.sh
index 3d385e818..4b9e9f038 100644
--- a/upgrade_modules/09_sync.sh
+++ b/upgrade_modules/09_sync.sh
@@ -1,30 +1,97 @@
#!/usr/bin/env bash
# CyberPanel upgrade – sync CyberCP to latest commit. Sourced by cyberpanel_upgrade.sh.
+# If local edits or untracked files block checkout, stash/quarantine and reset --hard to match origin.
Sync_CyberCP_To_Latest() {
+ local SYNC_STATE_FILE="/etc/cyberpanel/last_git_sync_failed"
+ mkdir -p /etc/cyberpanel
+ rm -f "$SYNC_STATE_FILE" 2>/dev/null || true
+
if [[ ! -d /usr/local/CyberCP/.git ]]; then
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] No .git in /usr/local/CyberCP, skipping sync" | tee -a /var/log/cyberpanel_upgrade_debug.log
return 0
fi
+
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Syncing /usr/local/CyberCP to latest commit for branch: $Branch_Name" | tee -a /var/log/cyberpanel_upgrade_debug.log
- # Backup production settings so sync does not overwrite DB credentials / local config
+
if [[ -f /usr/local/CyberCP/CyberCP/settings.py ]]; then
cp /usr/local/CyberCP/CyberCP/settings.py /tmp/cyberpanel_settings_backup.py
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Backed up settings.py for restore after sync" | tee -a /var/log/cyberpanel_upgrade_debug.log
fi
- (
- cd /usr/local/CyberCP
- git fetch origin 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log
- if git show-ref -q "refs/remotes/origin/$Branch_Name"; then
- git checkout -B "$Branch_Name" "origin/$Branch_Name" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log
- else
- git checkout "$Branch_Name" 2>/dev/null || true
- git pull --ff-only origin "$Branch_Name" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log || true
+
+ if ! cd /usr/local/CyberCP; then
+ echo "1" > "$SYNC_STATE_FILE"
+ return 1
+ fi
+
+ local remote_ref="origin/$Branch_Name"
+ local fetch_rc sync_ok=0
+
+ git fetch origin "$Branch_Name" 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log
+ fetch_rc="${PIPESTATUS[0]}"
+ if [[ "$fetch_rc" -ne 0 ]]; then
+ echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: git fetch origin $Branch_Name failed (exit $fetch_rc)" | tee -a /var/log/cyberpanel_upgrade_debug.log
+ echo "1" > "$SYNC_STATE_FILE"
+ return 1
+ fi
+
+ if ! git show-ref -q "refs/remotes/$remote_ref"; then
+ echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: remote ref $remote_ref not found after fetch" | tee -a /var/log/cyberpanel_upgrade_debug.log
+ echo "1" > "$SYNC_STATE_FILE"
+ return 1
+ fi
+
+ _cp_force_sync_to_remote_tip() {
+ local ref="$1" quarantine dest rel
+ git checkout -B "$Branch_Name" "$ref" >> /var/log/cyberpanel_upgrade_debug.log 2>&1
+ if [[ $? -eq 0 ]]; then
+ return 0
fi
- )
- local sync_code=$?
- # Merge production DATABASES into branch settings.py so DB creds survive without stripping
- # new INSTALLED_APPS (webmail, emailDelivery, etc.). Blind full restore broke integrated webmail.
+ echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Checkout blocked; stashing tracked+untracked changes and resetting --hard to $ref" | tee -a /var/log/cyberpanel_upgrade_debug.log
+ git stash push -u -m "cyberpanel-upgrade-auto-$(date +%s)" >> /var/log/cyberpanel_upgrade_debug.log 2>&1 || true
+ git reset --hard "$ref" >> /var/log/cyberpanel_upgrade_debug.log 2>&1
+ if [[ $? -eq 0 ]]; then
+ return 0
+ fi
+ quarantine="/root/cyberpanel_git_untracked_quarantine_$(date +%s)"
+ mkdir -p "$quarantine"
+ echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] reset --hard still blocked; moving untracked paths to $quarantine" | tee -a /var/log/cyberpanel_upgrade_debug.log
+ # shellcheck disable=SC2162
+ while IFS= read -r line; do
+ case "$line" in
+ \?\?*)
+ rel="${line#??}"
+ rel="${rel#"${rel%%[![:space:]]*}"}"
+ if [[ -n "$rel" ]] && [[ -e "$rel" ]]; then
+ dest="$quarantine/$rel"
+ mkdir -p "$(dirname "$dest")"
+ mv "$rel" "$dest" 2>/dev/null || mv "$rel" "$quarantine/" 2>/dev/null || true
+ fi
+ ;;
+ esac
+ done < <(git status --porcelain)
+ git reset --hard "$ref" >> /var/log/cyberpanel_upgrade_debug.log 2>&1
+ return $?
+ }
+
+ if _cp_force_sync_to_remote_tip "$remote_ref"; then
+ local local_h remote_h
+ local_h=$(git rev-parse HEAD 2>/dev/null)
+ remote_h=$(git rev-parse "$remote_ref" 2>/dev/null)
+ if [[ -n "$local_h" ]] && [[ -n "$remote_h" ]] && [[ "$local_h" == "$remote_h" ]]; then
+ sync_ok=1
+ fi
+ fi
+
+ if [[ "$sync_ok" -ne 1 ]]; then
+ echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: /usr/local/CyberCP HEAD does not match $remote_ref after forced sync (local=$(git rev-parse HEAD 2>/dev/null), remote=$(git rev-parse "$remote_ref" 2>/dev/null))" | tee -a /var/log/cyberpanel_upgrade_debug.log
+ echo "1" > "$SYNC_STATE_FILE"
+ if [[ -f /tmp/cyberpanel_settings_backup.py ]]; then
+ cp /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py
+ fi
+ return 1
+ fi
+
if [[ -f /tmp/cyberpanel_settings_backup.py ]] && [[ -f /usr/local/CyberCP/upgrade_modules/merge_production_settings.py ]]; then
python3 /usr/local/CyberCP/upgrade_modules/merge_production_settings.py /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py 2>&1 | tee -a /var/log/cyberpanel_upgrade_debug.log
if [[ "${PIPESTATUS[0]}" -eq 0 ]]; then
@@ -36,22 +103,18 @@ Sync_CyberCP_To_Latest() {
cp /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restored settings.py after sync (merge script missing)" | tee -a /var/log/cyberpanel_upgrade_debug.log
fi
- # LiteSpeed serves /static/ from public/static/; ensure it has latest baseTemplate static files (e.g. dashboard JS)
+
if [[ -d /usr/local/CyberCP/public/static ]] && [[ -d /usr/local/CyberCP/baseTemplate/static/baseTemplate ]]; then
rsync -a /usr/local/CyberCP/baseTemplate/static/baseTemplate/ /usr/local/CyberCP/public/static/baseTemplate/ 2>/dev/null || \
cp -r /usr/local/CyberCP/baseTemplate/static/baseTemplate/* /usr/local/CyberCP/public/static/baseTemplate/ 2>/dev/null || true
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Synced baseTemplate static to public/static" | tee -a /var/log/cyberpanel_upgrade_debug.log
fi
- # Firewall UI (firewall.js) – so Firewall Rules and Banned IPs load correct layout and Modify buttons
if [[ -d /usr/local/CyberCP/public/static ]] && [[ -f /usr/local/CyberCP/firewall/static/firewall/firewall.js ]]; then
mkdir -p /usr/local/CyberCP/public/static/firewall
cp -f /usr/local/CyberCP/firewall/static/firewall/firewall.js /usr/local/CyberCP/public/static/firewall/ 2>/dev/null && \
echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Synced firewall static to public/static" | tee -a /var/log/cyberpanel_upgrade_debug.log || true
fi
- if [[ $sync_code -eq 0 ]]; then
- echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Sync completed. Current HEAD: $(git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || echo 'unknown')" | tee -a /var/log/cyberpanel_upgrade_debug.log
- else
- echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Sync returned code $sync_code (non-fatal)" | tee -a /var/log/cyberpanel_upgrade_debug.log
- fi
+
+ echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Sync completed. Current HEAD: $(git -C /usr/local/CyberCP rev-parse HEAD 2>/dev/null || echo 'unknown')" | tee -a /var/log/cyberpanel_upgrade_debug.log
return 0
}
diff --git a/upgrade_modules/11_display_final.sh b/upgrade_modules/11_display_final.sh
index 81b884c43..a1dcedcd6 100644
--- a/upgrade_modules/11_display_final.sh
+++ b/upgrade_modules/11_display_final.sh
@@ -22,7 +22,12 @@ _b " ▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒███ ▒▒▒▒▒▒
_b " ███ ▒███"
_b " ▒▒██████"
_b " ▒▒▒▒▒▒"
-_b " *** UPGRADE COMPLETED SUCCESSFULLY! ***"
+if [[ "${CYBERPANEL_GIT_SYNC_OK:-1}" -eq 1 ]]; then
+ _b " *** UPGRADE COMPLETED SUCCESSFULLY! ***"
+else
+ _b " *** UPGRADE FINISHED BUT GIT SYNC FAILED - /usr/local/CyberCP MAY BE OUTDATED ***"
+ _b " See /var/log/cyberpanel_upgrade_debug.log (exit code will be non-zero)"
+fi
_b ""
_bl
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')