Merge pull request #1747 from master3395/v2.5.5-dev

V2.5.5 dev
This commit is contained in:
Master3395
2026-03-27 23:10:37 +01:00
committed by GitHub
17 changed files with 896 additions and 138 deletions

View File

@@ -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)

View File

@@ -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',
],
},
},

View File

@@ -102,4 +102,84 @@ def dns_static_context(request):
version = int(time.time())
return {
'DNS_STATIC_VERSION': version
}
}
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

View File

@@ -2396,8 +2396,8 @@
{% endif %}
</div>
{% endif %}
{% if admin or managePlugins %}
{% if managePlugins and not admin %}
{% if show_plugins_menu %}
{% if grant_only_lpma_sidebar or managePlugins and not admin %}
<div class="section-header">{% trans "PLUGINS" %}</div>
{% endif %}
<a href="#" class="menu-item" onclick="toggleSubmenu('plugins-submenu', this); return false;">
@@ -2408,12 +2408,19 @@
<i class="fas fa-chevron-right chevron"></i>
</a>
<div id="plugins-submenu" class="submenu">
{% if show_plugin_management_links %}
<a href="{% url 'installed' %}" class="menu-item">
<span>Installed</span>
</a>
<a href="{% url 'installed' %}?view=store" class="menu-item">
<span>{% trans "Plugin Store" %}</span>
</a>
{% endif %}
{% if show_lpma_sidebar_link %}
<a href="{{ lpma_sidebar_url }}" class="menu-item">
<span>{% trans "Limited phpMyAdmin" %}</span>
</a>
{% endif %}
</div>
{% endif %}
</div>

View File

@@ -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 %}
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<span>{% trans "Note: Latest commit does not match, please upgrade CyberPanel." %} <a href="https://cyberpanel.net/KnowledgeBase/home/upgrading-cyberpanel/">Learn how to upgrade CyberPanel.</a></span>
<span>
{% blocktrans with cmp=notecheck_compare_remote %}Note: The current commit does not match the latest commit on {{ cmp }}. Please upgrade CyberPanel.{% endblocktrans %}
<a href="https://cyberpanel.net/KnowledgeBase/home/upgrading-cyberpanel/" target="_blank" rel="noopener noreferrer">{% trans "Learn how to upgrade CyberPanel." %}</a>
</span>
</div>
{% endif %}
{% if show_fork_block and fork_drift_upstream %}
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<span>{% 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." %}</span>
</div>
{% elif local_behind_official and not Notecheck %}
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<span>{% trans "Your commit differs from the latest on the official CyberPanel repository (usmannasir/cyberpanel). This is informational if you intentionally track a fork." %}</span>
</div>
{% endif %}
@@ -294,8 +374,9 @@
</div>
<div class="content-card">
<h2 class="card-title">Version Information</h2>
<h2 class="card-title">{% trans "Version Information" %}</h2>
<p class="version-section">{% trans "This installation" %}</p>
<div class="info-grid">
<div class="info-item">
<span class="info-label">{% trans "Current Version" %}</span>
@@ -306,22 +387,61 @@
<span class="info-value">{{ build }}</span>
</div>
<div class="info-item">
<span class="info-label">{% trans "Current Commit" %}</span>
<span class="info-value">{{ Currentcomt }}</span>
<span class="info-label">{% trans "Current commit" %}</span>
<span class="info-value" title="{{ Currentcomt }}">
{% if Currentcomt_short %}<code class="version-sha">{{ Currentcomt_short }}</code>{% else %}—{% endif %}
</span>
</div>
<div class="info-item">
<span class="info-label">{% trans "Latest Version" %}</span>
<span class="info-label">{% trans "Published latest version" %}</span>
<span class="info-value">{{ latestVersion }}</span>
</div>
<div class="info-item">
<span class="info-label">{% trans "Latest Build" %}</span>
<span class="info-label">{% trans "Published latest build" %}</span>
<span class="info-value">{{ latestBuild }}</span>
</div>
<div class="info-item">
<span class="info-label">{% trans "Latest Commit" %}</span>
<span class="info-value">{{ latestcomit }}</span>
</div>
</div>
<p class="version-section">{% trans "Git remotes and branch tips" %}</p>
<div class="version-meta-row">
<span class="version-meta-row-label">{% trans "Origin (git remote)" %}</span>
<span class="version-meta-row-value">{{ remote_display|default:"—" }}</span>
</div>
<div class="version-meta-row">
<span class="version-meta-row-label">{% trans "Tracking branch" %}</span>
<span class="version-meta-row-value"><code class="version-sha">{{ branch_ref }}</code></span>
</div>
{% if fork_remote_commit %}
<div class="version-meta-row">
<span class="version-meta-row-label">{% trans "Your fork — latest on GitHub" %}</span>
<span class="version-meta-row-value">
<code class="version-sha" title="{{ fork_remote_commit }}">{{ fork_remote_commit_short }}</code>
{% if fork_commit_url %}
<a href="{{ fork_commit_url }}" target="_blank" rel="noopener noreferrer">{% trans "View commit" %}</a>
{% endif %}
</span>
</div>
{% endif %}
{% if upstream_commit %}
<div class="version-meta-row">
<span class="version-meta-row-label">{% trans "Official upstream (usmannasir/cyberpanel) — latest on GitHub" %}</span>
<span class="version-meta-row-value">
<code class="version-sha" title="{{ upstream_commit }}">{{ upstream_commit_short }}</code>
{% if upstream_commit_url %}
<a href="{{ upstream_commit_url }}" target="_blank" rel="noopener noreferrer">{% trans "View commit" %}</a>
{% endif %}
</span>
</div>
{% elif latestcomit and not fork_remote_commit and not on_dev_branch %}
<div class="version-meta-row">
<span class="version-meta-row-label">{% trans "Latest commit on origin (comparison)" %}</span>
<span class="version-meta-row-value">
<code class="version-sha" title="{{ latestcomit }}">{{ latestcomit_short }}</code>
</span>
</div>
{% endif %}
</div>
<div class="content-card" ng-hide="upgradelogBox">

View File

@@ -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()

View File

@@ -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

View File

@@ -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:

View File

@@ -0,0 +1,134 @@
<?php
/**
* Load Limited phpMyAdmin UI policy (strict mode + blocked preference tabs).
* Primary: pluginState (writable by cyberpanel). Fallbacks for older installs.
*/
function lpma_read_limited_policy(): array
{
$defaultBlocked = [
'manage' => 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;
}

View File

@@ -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 '<script>document.getElementById("redirectForm").submit();</script>';
} 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;

View File

@@ -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()

View File

@@ -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 '<script>document.getElementById("redirectForm").submit();</script>';
} 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;

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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:

View File

@@ -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')