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

V2.5.5 dev
This commit is contained in:
Master3395
2026-04-11 20:35:10 +02:00
committed by GitHub
17 changed files with 2352 additions and 210 deletions

View File

@@ -2559,7 +2559,7 @@
<script src="{% static 'managePHP/managePHP.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'serverLogs/serverLogs.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'serverStatus/serverStatus.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=6" data-cfasync="false"></script>
<script src="{% static 'firewall/firewall.js' %}?v={{ CP_VERSION }}&fw={{ FIREWALL_STATIC_VERSION|default:CP_VERSION }}&cb=11" data-cfasync="false"></script>
<script src="{% static 'emailPremium/emailPremium.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
<script src="{% static 'manageServices/manageServices.js' %}?v={{ CP_VERSION }}&msModal=20260402d" data-cfasync="false"></script>
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>

View File

@@ -210,6 +210,9 @@ class ContainerManager(multi.Thread):
data['memoryUsage'] = 0
data['cpuUsage'] = 0
# JSON for Angular ng-init (flat map container_port -> host_port from DB)
data['ports_json'] = json.dumps(data.get('ports', {}))
template = 'dockerManager/viewContainer.html'
proc = httpProc(request, template, data, 'admin')
return proc.render()
@@ -588,14 +591,21 @@ class ContainerManager(multi.Thread):
image = data['image']
tag = data['tag']
force_update = bool(data.get('force_update'))
try:
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
data_ret = {'installImageStatus': 0, 'error_message': "Image already installed"}
if not self._validate_image_name(image):
data_ret = {'installImageStatus': 0, 'error_message': 'Invalid image name format'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except docker.errors.ImageNotFound:
pass
if not force_update:
try:
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
data_ret = {'installImageStatus': 0, 'error_message': "Image already installed"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except docker.errors.ImageNotFound:
pass
try:
image = client.images.pull(image, tag=tag)
@@ -636,14 +646,17 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Check if image already exists
try:
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
data_ret = {'pullImageStatus': 0, 'error_message': "Image already exists locally"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except docker.errors.ImageNotFound:
pass
force_update = bool(data.get('force_update'))
# Check if image already exists (skip when refreshing from registry)
if not force_update:
try:
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
data_ret = {'pullImageStatus': 0, 'error_message': "Image already exists locally"}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except docker.errors.ImageNotFound:
pass
# Pull the image
try:
@@ -1306,6 +1319,36 @@ class ContainerManager(multi.Thread):
secure_log_error(e, 'container_operation')
return 'Operation failed'
@staticmethod
def _normalize_ports_for_save(ports_raw):
"""
Parse client-submitted port map for saveContainerSettings recreate.
Returns (True, dict) with container_port -> host_port (str),
(True, None) if ports_raw is None (caller should use DB),
or (False, error_message).
"""
if ports_raw is None:
return True, None
if not isinstance(ports_raw, dict):
return False, 'Invalid ports payload'
out = {}
for ck, cv in ports_raw.items():
if not ck:
continue
ckey = str(ck).strip()
if not ckey:
continue
if cv in (None, '', 'null'):
continue
try:
hp = int(cv)
except (ValueError, TypeError):
return False, 'Invalid host port for %s' % ckey
if hp < 1024 or hp > 65535:
return False, 'Choose host port between 1024 and 65535'
out[ckey] = str(hp)
return True, out
def saveContainerSettings(self, userID=None, data=None):
try:
name = data['name']
@@ -1375,6 +1418,15 @@ class ContainerManager(multi.Thread):
continue
volumes[volume['src']] = {'bind': volume['dest'],
'mode': 'rw'}
ports_for_recreate = json.loads(con.ports)
if 'ports' in data and data.get('ports') is not None:
ok_ports, norm_ports = self._normalize_ports_for_save(data.get('ports'))
if not ok_ports:
data_ret = {'saveSettingsStatus': 0, 'error_message': norm_ports}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
if norm_ports is not None:
ports_for_recreate = norm_ports
# Prepare data for recreate function
data = {
'name': name,
@@ -1382,7 +1434,7 @@ class ContainerManager(multi.Thread):
'image': con.image,
'tag': con.tag,
'env': envDict,
'ports': json.loads(con.ports),
'ports': ports_for_recreate,
'volumes': volumes,
'memory': con.memory
}
@@ -1395,6 +1447,7 @@ class ContainerManager(multi.Thread):
con.env = json.dumps(envDict)
con.volumes = json.dumps(volumes)
con.ports = json.dumps(ports_for_recreate)
con.save()
data_ret = {'saveSettingsStatus': 1, 'error_message': 'None'}

View File

@@ -1389,6 +1389,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.cName = "";
$scope.status = "";
$scope.savingSettings = false;
$scope.currentPorts = {};
$scope.initialPortsForSettings = {};
$scope.loadingTop = false;
$scope.statusInterval = null;
$scope.statsInterval = null;
@@ -1398,7 +1400,37 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.advancedEnvText = '';
$scope.advancedEnvCount = 0;
$scope.parsedEnvVars = {};
/** Flat map from DB (container_port -> host_port), set from template ng-init */
$scope.serverPortMap = {};
$scope.initAngularPortsFromServer = function (obj) {
if (obj === undefined || obj === null) {
$scope.serverPortMap = {};
$scope.ports = {};
return;
}
if (angular.isString(obj)) {
try {
obj = angular.fromJson(obj);
} catch (e1) {
try {
obj = JSON.parse(obj);
} catch (e2) {
$scope.serverPortMap = {};
$scope.ports = {};
return;
}
}
}
if (!angular.isObject(obj)) {
$scope.serverPortMap = {};
$scope.ports = {};
return;
}
$scope.serverPortMap = obj;
$scope.ports = obj;
};
// Auto-refresh status every 5 seconds
$scope.startStatusMonitoring = function() {
$scope.statusInterval = $interval(function() {
@@ -2028,6 +2060,13 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
}
};
/** Used after legacy port-update path; full page reload syncs Django-rendered state */
$scope.refreshContainerInfo = function () {
$timeout(function () {
window.location.reload();
}, 300);
};
$scope.addVolField = function () {
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
$scope.volListNumber = $scope.volListNumber + 1;
@@ -2038,6 +2077,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
};
$scope.saveSettings = function () {
if ($scope.portsDirty && $scope.portsDirty() && !$scope.envConfirmation) {
new PNotify({
title: 'Confirmation required',
text: 'Check the confirmation box to apply changes to ports, environment variables, or volumes (the container will be recreated).',
type: 'warning'
});
return;
}
$('#containerSettingLoading').show();
url = "/docker/saveContainerSettings";
$scope.savingSettings = true;
@@ -2063,7 +2111,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
envConfirmation: $scope.envConfirmation,
envList: finalEnvList,
volList: $scope.volList,
advancedEnvMode: $scope.advancedEnvMode
advancedEnvMode: $scope.advancedEnvMode,
ports: $scope.currentPorts || {}
};
@@ -2088,6 +2137,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
title: 'Settings Saved',
type: 'success'
});
$scope.initialPortsForSettings = angular.copy($scope.currentPorts || {});
}
}
else {
@@ -2213,22 +2263,55 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$("#commandModal").modal("show");
};
// Port editing functionality
$scope.showPortEditModal = function() {
// Initialize current ports from container data
// Port editing (in Container Settings modal)
$scope.initSettingsPortsFromContainer = function () {
$scope.currentPorts = {};
if ($scope.ports) {
for (var iport in $scope.ports) {
var eport = $scope.ports[iport];
if (eport && eport.length > 0) {
var src = $scope.serverPortMap && Object.keys($scope.serverPortMap).length
? $scope.serverPortMap
: ($scope.ports || {});
if (src && typeof src === 'object') {
for (var iport in src) {
if (!Object.prototype.hasOwnProperty.call(src, iport)) {
continue;
}
var eport = src[iport];
if (angular.isArray(eport) && eport.length > 0 && eport[0] && eport[0].HostPort) {
$scope.currentPorts[iport] = eport[0].HostPort;
} else if (eport !== undefined && eport !== null && eport !== '') {
$scope.currentPorts[iport] = String(eport);
}
}
}
$("#portEditModal").modal("show");
$scope.initialPortsForSettings = angular.copy($scope.currentPorts);
};
$scope.addNewPortMapping = function() {
$scope.portsJsonSnapshot = function (obj) {
if (!obj || typeof obj !== 'object') {
return '{}';
}
var keys = Object.keys(obj).sort();
var flat = {};
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
var v = obj[k];
flat[k] = v === null || v === undefined ? '' : String(v);
}
return JSON.stringify(flat);
};
$scope.portsDirty = function () {
return $scope.portsJsonSnapshot($scope.currentPorts) !== $scope.portsJsonSnapshot($scope.initialPortsForSettings);
};
$scope.showPortEditModal = function () {
$scope.initSettingsPortsFromContainer();
$("#settings").modal("show");
};
$scope.addNewPortMapping = function () {
if (!$scope.envConfirmation) {
return;
}
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
if (containerPort) {
$scope.currentPorts[containerPort] = '';
@@ -2236,14 +2319,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
}
};
$scope.removePortMapping = function(containerPort) {
$scope.removePortMapping = function (containerPort) {
if (!$scope.envConfirmation) {
return;
}
if (confirm('Are you sure you want to remove this port mapping?')) {
delete $scope.currentPorts[containerPort];
}
};
$scope.updatePortMappings = function() {
$("#portEditLoading").show();
$scope.updatePortMappings = function () {
$scope.updatingPorts = true;
var url = "/docker/updateContainerPorts";
@@ -2259,18 +2344,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
};
$http.post(url, data, config).then(function(response) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
if (response.data.status === 1) {
$("#portEditModal").modal("hide");
// Refresh container status and ports
$scope.refreshContainerInfo();
$("#settings").modal("hide");
new PNotify({
title: 'Success',
text: 'Port mappings updated successfully',
type: 'success'
});
$scope.refreshContainerInfo();
} else {
new PNotify({
title: 'Error',
@@ -2279,11 +2362,10 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
});
}
}, function(error) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
new PNotify({
title: 'Error',
text: 'Error updating port mappings: ' + error.data.error_message,
text: 'Error updating port mappings: ' + (error.data && error.data.error_message ? error.data.error_message : 'unknown'),
type: 'error'
});
});
@@ -2388,12 +2470,55 @@ app.controller('manageImages', function ($scope, $http) {
$scope.showingSearch = false;
$("#searchResult").hide();
function dockerHistoryFormatSize(raw) {
var n = parseInt(raw, 10);
if (isNaN(n) || n < 0) {
return (raw === undefined || raw === null) ? '' : String(raw);
}
if (n === 0) {
return '0 B';
}
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
var v = n / Math.pow(1024, i);
var s = (i === 0) ? String(v) : (Math.round(v * 10) / 10).toFixed(1);
return s + ' ' + units[i];
}
function dockerHistoryFormatCreated(raw) {
if (raw === undefined || raw === null) {
return '';
}
var n = Number(raw);
if (isNaN(n)) {
return String(raw);
}
var sec = n;
if (n > 1e14) {
sec = Math.floor(n / 1e9);
} else if (n > 1e12) {
sec = Math.floor(n / 1e6);
}
var d = new Date(sec * 1000);
if (isNaN(d.getTime())) {
return String(raw);
}
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
}
function dockerHistoryEnrichLayer(h) {
var o = angular.extend({}, h);
o.SizeHuman = dockerHistoryFormatSize(h.Size);
o.CreatedDisplay = dockerHistoryFormatCreated(h.Created);
return o;
}
$scope.pullImage = function (image, tag) {
function ListInitialDatas(response) {
if (response.data.installImageStatus === 1) {
new PNotify({
title: 'Image pulled successfully',
text: 'Reloading...',
text: 'Running containers keep the old image until you recreate them from Docker > Containers. Reloading',
type: 'success'
});
location.reload()
@@ -2424,7 +2549,8 @@ app.controller('manageImages', function ($scope, $http) {
url = "/docker/installImage";
var data = {
image: image,
tag: tag
tag: tag,
force_update: true
};
var config = {
headers: {
@@ -2445,6 +2571,30 @@ app.controller('manageImages', function ($scope, $http) {
}
$scope.refreshLocalImage = function (rowId, imageName) {
var sel = document.getElementById(String(rowId));
if (!sel || sel.selectedIndex < 0) {
new PNotify({
title: 'Unable to complete request',
text: 'Please select a tag for this image',
type: 'info'
});
return;
}
var raw = sel.options[sel.selectedIndex].text;
if (!raw) {
new PNotify({
title: 'Unable to complete request',
text: 'Please select a tag for this image',
type: 'info'
});
return;
}
var li = raw.lastIndexOf(':');
var tag = li > -1 ? raw.substring(li + 1) : raw;
$scope.pullImage(imageName, tag);
}
$scope.searchImages = function () {
console.log($scope.searchString);
if (!$scope.searchString) {
@@ -2580,7 +2730,8 @@ app.controller('manageImages', function ($scope, $http) {
$scope.getHistory = function (counter) {
$('#imageLoading').show();
var name = $("#" + counter).val()
var sel = $("#" + counter);
var name = (sel.find('option:selected').text() || sel.val() || '').trim();
url = "/docker/getImageHistory";
@@ -2600,7 +2751,8 @@ app.controller('manageImages', function ($scope, $http) {
if (response.data.imageHistoryStatus === 1) {
$('#history').modal('show');
$scope.historyList = response.data.history;
var raw = response.data.history || [];
$scope.historyList = raw.map(dockerHistoryEnrichLayer);
}
else {
new PNotify({

View File

@@ -375,8 +375,20 @@
padding: 1rem 2rem;
}
.modal-dialog-history {
width: 96vw;
max-width: min(96vw, 1400px);
margin: 1.75rem auto;
}
.history-modal-scroll {
overflow-x: auto;
max-width: 100%;
}
.history-table {
width: 100%;
table-layout: fixed;
background: var(--bg-hover, #f8f9ff);
border-radius: 8px;
overflow: hidden;
@@ -390,13 +402,74 @@
color: var(--text-primary, #1e293b);
font-size: 0.875rem;
}
.history-table th.history-col-size {
text-align: right;
width: 9%;
}
.history-table th.history-col-created {
width: 14%;
}
.history-table th.history-col-comment {
width: 12%;
}
.history-table th.history-col-id {
width: 14%;
}
.history-table td {
padding: 0.75rem;
color: var(--text-secondary, #64748b);
font-size: 0.875rem;
border-bottom: 1px solid var(--border-color, #e8e9ff);
word-break: break-all;
vertical-align: top;
word-break: normal;
overflow-wrap: anywhere;
}
.history-table td.history-col-id {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
white-space: nowrap;
overflow-x: auto;
max-width: 0;
}
.history-table td.history-col-cmd {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.8125rem;
}
.history-layer-cmd {
margin: 0;
white-space: pre;
overflow: auto;
max-height: 14rem;
max-width: 100%;
background: var(--bg-primary, #fff);
border: 1px solid var(--border-color, #e8e9ff);
border-radius: 6px;
padding: 0.5rem 0.6rem;
}
.history-table td.history-col-created {
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.history-table td.history-col-comment {
overflow-x: auto;
max-width: 0;
font-size: 0.8125rem;
}
.history-table td.history-col-size {
white-space: nowrap;
text-align: right;
font-variant-numeric: tabular-nums;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
@keyframes spin {
@@ -583,6 +656,11 @@
</td>
<td style="text-align: center;">
<div class="action-buttons">
<button type="button" class="btn-primary" title="{% trans 'Pull latest from registry for the selected tag. Running containers are unchanged until recreated.' %}"
ng-click="refreshLocalImage({{ forloop.counter }}, '{{ image.name|escapejs }}')">
<i class="fas fa-sync-alt"></i>
{% trans "Refresh" %}
</button>
<button class="btn-info" title="{% trans 'View History' %}"
ng-click="getHistory({{ forloop.counter }})">
<i class="fas fa-history"></i>
@@ -603,7 +681,7 @@
<!-- History Modal -->
<div id="history" class="modal fade" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-dialog modal-lg modal-dialog-history">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
@@ -614,26 +692,28 @@
style="font-size: 1.5rem; background: transparent; border: none;">&times;</button>
</div>
<div class="modal-body">
<div class="history-modal-scroll">
<table class="history-table">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th class="history-col-id">{% trans "ID" %}</th>
<th>{% trans "Created By" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Comment" %}</th>
<th>{% trans "Size" %}</th>
<th class="history-col-created">{% trans "Created" %}</th>
<th class="history-col-comment">{% trans "Comment" %}</th>
<th class="history-col-size">{% trans "Size" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="history in historyList track by $index">
<td ng-bind="history.Id"></td>
<td ng-bind="history.CreatedBy"></td>
<td ng-bind="history.Created"></td>
<td ng-bind="history.Comment"></td>
<td ng-bind="history.Size"></td>
<td class="history-col-id" ng-bind="history.Id"></td>
<td class="history-col-cmd"><pre class="history-layer-cmd" ng-bind="history.CreatedBy"></pre></td>
<td class="history-col-created" ng-bind="history.CreatedDisplay"></td>
<td class="history-col-comment" ng-bind="history.Comment"></td>
<td class="history-col-size"><span ng-bind="history.SizeHuman"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" data-dismiss="modal">

View File

@@ -589,7 +589,7 @@
}
</style>
<div class="modern-container" ng-controller="viewContainer">
<div class="modern-container" ng-controller="viewContainer" ng-init='initAngularPortsFromServer({{ ports_json|safe }})'>
<div class="page-header">
<div class="container-header">
<div class="container-icon-large">
@@ -738,7 +738,7 @@
<div class="action-text">{% trans "Remove" %}</div>
</div>
<div class="action-btn" data-toggle="modal" data-target="#settings">
<div class="action-btn" data-toggle="modal" data-target="#settings" ng-click="initSettingsPortsFromContainer()">
<i class="fas fa-sliders-h action-icon" style="color: #6366f1;"></i>
<div class="action-text">{% trans "Settings" %}</div>
</div>
@@ -762,11 +762,6 @@
<i class="fas fa-code action-icon" style="color: #10b981;"></i>
<div class="action-text">{% trans "Run Command" %}</div>
</div>
<div class="action-btn" ng-click="showPortEditModal()" ng-disabled="status!='running'">
<i class="fas fa-edit action-icon" style="color: #8b5cf6;"></i>
<div class="action-text">{% trans "Edit Ports" %}</div>
</div>
</div>
</div>
</div>
@@ -826,6 +821,51 @@
</div>
</div>
<hr>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Port mappings" %}</label>
<div class="col-sm-9">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>{% trans "Important:" %}</strong>
{% trans "Changing host ports requires checking the confirmation box below; the container will be recreated." %}
</div>
<div class="port-mapping-container">
<div ng-repeat="(containerPort, hostPort) in currentPorts" class="port-mapping-row" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
<div style="flex: 1;">
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Container Port" %}</label>
<input type="text" class="form-control" ng-model="containerPort" disabled style="font-family: monospace;">
</div>
<div style="color: #007bff; font-size: 1.25rem;">
<i class="fas fa-arrow-right"></i>
</div>
<div style="flex: 1;">
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Host Port" %}</label>
<input type="number" class="form-control" ng-model="currentPorts[containerPort]"
placeholder="{% trans 'e.g., 8080' %}" min="1024" max="65535" ng-disabled="!envConfirmation">
</div>
<div style="display: flex; align-items: center; height: 38px;">
<button type="button" class="btn btn-danger btn-sm" ng-click="removePortMapping(containerPort)"
ng-show="Object.keys(currentPorts).length > 0" title="{% trans 'Remove port mapping' %}" ng-disabled="!envConfirmation">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div ng-show="Object.keys(currentPorts).length === 0" class="text-center text-muted" style="padding: 2rem;">
<i class="fas fa-info-circle fa-2x" style="margin-bottom: 1rem;"></i>
<p>{% trans "No port mappings configured" %}</p>
</div>
</div>
<div class="form-group" style="margin-top: 0.5rem;">
<button type="button" class="btn btn-primary" ng-click="addNewPortMapping()" ng-disabled="!envConfirmation">
<i class="fas fa-plus"></i>
{% trans "Add Port Mapping" %}
</button>
</div>
</div>
</div>
<hr>
<div class="form-group">
@@ -834,7 +874,7 @@
<div class="checkbox">
<label>
<input ng-model="envConfirmation" type="checkbox">
<strong>{% trans "I understand that editing ENV or Volumes will recreate the container" %}</strong>
<strong>{% trans "I understand that editing ENV, Volumes, or Ports will recreate the container" %}</strong>
</label>
</div>
</div>
@@ -856,10 +896,10 @@
</p>
</div>
<div class="toggle-switch">
<label class="switch" style="position: relative; display: inline-block; width: 60px; height: 34px;">
<input type="checkbox" ng-model="advancedEnvMode" ng-change="toggleEnvMode()" style="opacity: 0; width: 0; height: 0;">
<span class="slider" style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px;">
<span class="slider-thumb" style="position: absolute; content: ''; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2);"></span>
<label class="switch cp-adv-env-switch" style="position: relative; display: inline-block; width: 60px; height: 34px;">
<input type="checkbox" ng-model="advancedEnvMode" ng-change="toggleEnvMode()">
<span class="slider cp-adv-env-slider">
<span class="slider-thumb cp-adv-env-thumb"></span>
</span>
</label>
</div>
@@ -1246,77 +1286,6 @@
</div>
</div>
<!-- Port Editing Modal -->
<div id="portEditModal" class="modal fade" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<i class="fas fa-edit" style="margin-right: 0.5rem;"></i>
{% trans "Edit Port Mappings" %}
</h4>
<button type="button" class="close" data-dismiss="modal"
style="font-size: 1.5rem; background: transparent; border: none;">&times;</button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>{% trans "Important:" %}</strong> {% trans "Editing port mappings will temporarily stop and recreate the container. Any unsaved data in the container may be lost." %}
</div>
<div class="form-group">
<label class="control-label">
<i class="fas fa-plug" style="margin-right: 0.5rem;"></i>
{% trans "Port Mappings" %}
</label>
<div class="port-mapping-container">
<div ng-repeat="(containerPort, hostPort) in currentPorts" class="port-mapping-row" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
<div style="flex: 1;">
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Container Port" %}</label>
<input type="text" class="form-control" ng-model="containerPort" disabled style="font-family: monospace;">
</div>
<div style="color: #007bff; font-size: 1.25rem;">
<i class="fas fa-arrow-right"></i>
</div>
<div style="flex: 1;">
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Host Port" %}</label>
<input type="number" class="form-control" ng-model="currentPorts[containerPort]"
placeholder="{% trans 'e.g., 8080' %}" min="1024" max="65535">
</div>
<div style="display: flex; align-items: center; height: 38px;">
<button type="button" class="btn btn-danger btn-sm" ng-click="removePortMapping(containerPort)"
ng-show="Object.keys(currentPorts).length > 1" title="{% trans 'Remove port mapping' %}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div ng-show="Object.keys(currentPorts).length === 0" class="text-center text-muted" style="padding: 2rem;">
<i class="fas fa-info-circle fa-2x" style="margin-bottom: 1rem;"></i>
<p>{% trans "No port mappings configured" %}</p>
</div>
</div>
</div>
<div class="form-group">
<button type="button" class="btn btn-primary" ng-click="addNewPortMapping()">
<i class="fas fa-plus"></i>
{% trans "Add Port Mapping" %}
</button>
</div>
</div>
<div class="modal-footer">
<img id="portEditLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
<button type="button" class="btn btn-primary" ng-disabled="updatingPorts" ng-click="updatePortMappings()">
<i class="fas fa-save"></i> {% trans "Update Port Mappings" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fas fa-times"></i> {% trans "Cancel" %}
</button>
</div>
</div>
</div>
</div>
</div>
<style>
@@ -1439,13 +1408,79 @@
color: #495057;
}
.compose-benefits {
padding-bottom: 0.75rem;
}
.compose-actions {
margin-top: 1rem;
margin-top: 1.25rem;
padding-top: 0.5rem;
border-top: 1px solid rgba(0, 123, 255, 0.15);
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Container Settings modal: checkbox alignment + advanced mode switch */
#settings .checkbox label {
display: flex;
align-items: flex-start;
gap: 0.65rem;
padding-left: 0;
cursor: pointer;
font-weight: normal;
}
#settings .checkbox label input[type="checkbox"] {
position: static !important;
margin-left: 0 !important;
margin-top: 0.2rem;
flex-shrink: 0;
width: auto;
height: auto;
opacity: 1;
}
#settings .cp-adv-env-switch input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
margin: 0;
}
#settings .cp-adv-env-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.3s;
border-radius: 34px;
}
#settings .cp-adv-env-thumb {
position: absolute;
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: #fff;
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
#settings .cp-adv-env-switch input:checked + .cp-adv-env-slider {
background-color: #6366f1;
}
#settings .cp-adv-env-switch input:checked + .cp-adv-env-slider .cp-adv-env-thumb {
left: calc(100% - 30px);
}
.compose-actions .btn {
border-radius: 6px;
font-weight: 500;

View File

@@ -2,6 +2,26 @@
* Created by usman on 6/22/18.
*/
/* Ensure CyberCP Angular module exists (system-status.js normally sets window.app in <head>) */
(function () {
if (typeof window === 'undefined' || typeof angular === 'undefined') {
return;
}
if (typeof window.app !== 'undefined' && window.app) {
return;
}
try {
window.app = angular.module('CyberCP');
} catch (e) {
try {
window.app = angular.module('CyberCP', []);
} catch (e2) {
return;
}
}
})();
var app = window.app;
/* Java script code to list accounts */
app.controller('listDomains', function ($scope, $http) {

View File

@@ -611,7 +611,7 @@
<div ng-hide="RspamdNotifyBox">
<div ng-hide="failedToStartInallation" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Failed to start installation. Error message:" %} {$ errorMessage $}</span>
<span>{% trans "Failed to start installation. Error message:" %} <span ng-bind="errorMessage"></span></span>
</div>
<div ng-hide="couldNotConnect" class="alert alert-danger">
@@ -621,7 +621,7 @@
<div ng-hide="installationFailed" class="alert alert-danger">
<i class="fas fa-times-circle alert-icon"></i>
<span>{% trans "Installation failed." %} {$ errorMessage $}</span>
<span>{% trans "Installation failed." %} <span ng-bind="errorMessage"></span></span>
</div>
<div ng-hide="RspamdSuccessfullyInstalled" class="alert alert-success">
@@ -637,7 +637,7 @@
<h3 class="install-log-title">{% trans "Installation Progress" %}</h3>
<span ng-hide="RspamdLoading" class="loading-spinner"></span>
</div>
<textarea ng-model="requestData" rows="15" class="log-textarea" readonly>{{ requestData }}</textarea>
<textarea ng-model="requestData" rows="15" class="log-textarea" readonly></textarea>
</div>
</div>
{% else %}
@@ -663,20 +663,20 @@
</button>
</div>
<p style="color: var(--text-secondary, #64748b); font-size: 14px; margin-top: 16px; line-height: 1.6;">
{% trans "Opens the official Rspamd web interface in a new tab (path /emailPremium/Rspamd/ui/ — proxied through CyberPanel, admin session required)." %}
{% trans "Opens the Rspamd controller UI over HTTPS using the same hostname and port as CyberPanel (path /emailPremium/Rspamd/ui/). Rspamd itself only speaks HTTP on 127.0.0.1:11334; the panel reverse-proxies it so you do not need https://YOUR_SERVER_IP:11334 (that URL is not served by Rspamd)." %}
</p>
</div>
<div class="content-section">
<h2 class="section-title">{% trans "Alternative: SSH tunnel" %}</h2>
<p style="color: var(--text-secondary, #64748b); font-size: 14px; line-height: 1.6;">
{% trans "If the proxied UI misbehaves, connect to port 11334 on the server via SSH and use your local browser." %}
{% trans "If the proxied UI misbehaves, forward port 11334 to your machine and open the link below (HTTP on localhost only — not HTTPS)." %}
</p>
<pre style="background: var(--bg-secondary,#f8f9ff); padding: 12px; border-radius: 8px; font-size: 13px; overflow-x: auto; border: 1px solid var(--border-light, #e8e9ff);">ssh -N -L 11334:127.0.0.1:11334 root@{{ ipAddress }}</pre>
<p style="margin-top: 12px;">
<a href="http://127.0.0.1:11334/" target="_blank" rel="noopener noreferrer" class="action-btn secondary">
<i class="fas fa-external-link-alt"></i>
{% trans "Open Rspamd UI (when tunnel is active)" %}
{% trans "Open Rspamd UI (when tunnel is active — http://127.0.0.1:11334/)" %}
</a>
</p>
</div>
@@ -772,7 +772,7 @@
<div style="margin-top: 20px;">
<div ng-hide="failedToSave" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Failed to save RSPAMD configurations. Error message:" %} {$ errorMessage $}</span>
<span>{% trans "Failed to save RSPAMD configurations. Error message:" %} <span ng-bind="errorMessage"></span></span>
</div>
<div ng-hide="successfullySaved" class="alert alert-success">
@@ -848,7 +848,7 @@
<div style="margin-top: 20px;">
<div ng-hide="ClamAVfailedToSave" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Failed to save ClamAV configurations. Error message:" %} {$ errorMessage $}</span>
<span>{% trans "Failed to save ClamAV configurations. Error message:" %} <span ng-bind="errorMessage"></span></span>
</div>
<div ng-hide="ClamAVsuccessfullySaved" class="alert alert-success">
@@ -906,7 +906,7 @@
<div style="margin-top: 20px;">
<div ng-hide="postfixfailedToSave" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Failed to save Postfix configurations. Error message:" %} {$ errorMessage $}</span>
<span>{% trans "Failed to save Postfix configurations. Error message:" %} <span ng-bind="errorMessage"></span></span>
</div>
<div ng-hide="postfixsuccessfullySaved" class="alert alert-success">
@@ -964,7 +964,7 @@
<div style="margin-top: 20px;">
<div ng-hide="RedisfailedToSave" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Failed to save Redis configurations. Error message:" %} {$ errorMessage $}</span>
<span>{% trans "Failed to save Redis configurations. Error message:" %} <span ng-bind="errorMessage"></span></span>
</div>
<div ng-hide="RedissuccessfullySaved" class="alert alert-success">
@@ -989,7 +989,7 @@
<div ng-hide="uninstallRspamdNotifyBox">
<div ng-hide="failedToStartInallation" class="alert alert-danger">
<i class="fas fa-exclamation-circle alert-icon"></i>
<span>{% trans "Failed to start uninstallation. Error message:" %} {$ errorMessage $}</span>
<span>{% trans "Failed to start uninstallation. Error message:" %} <span ng-bind="errorMessage"></span></span>
</div>
<div ng-hide="couldNotConnect" class="alert alert-danger">
@@ -999,7 +999,7 @@
<div ng-hide="installationFailed" class="alert alert-danger">
<i class="fas fa-times-circle alert-icon"></i>
<span>{% trans "Installation failed." %} {$ errorMessage $}</span>
<span>{% trans "Installation failed." %} <span ng-bind="errorMessage"></span></span>
</div>
<div ng-hide="RspamdSuccessfullyInstalled" class="alert alert-success">
@@ -1015,7 +1015,7 @@
<h3 class="install-log-title">{% trans "Uninstallation Progress" %}</h3>
<span ng-hide="RspamdLoading" class="loading-spinner"></span>
</div>
<textarea ng-model="requestData" rows="15" class="log-textarea" readonly>{{ requestData }}</textarea>
<textarea ng-model="requestData" rows="15" class="log-textarea" readonly></textarea>
</div>
</div>
@@ -1052,7 +1052,7 @@
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<textarea ng-model="RspamdlogsData" class="log-textarea" rows="20" readonly>{{ Rspamdlogs }}</textarea>
<textarea ng-model="RspamdlogsData" class="log-textarea" rows="20" readonly></textarea>
</div>
<div class="modal-footer">
<button type="button" class="modal-btn secondary" data-dismiss="modal">

View File

@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
import os
import time
import http.client
from django.shortcuts import redirect
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from loginSystem.models import Administrator
from mailServer.models import Domains, EUsers
@@ -1244,18 +1246,177 @@ def Rspamd(request):
checkIfRspamdInstalled = 0
ipFile = "/etc/cyberpanel/machineIP"
f = open(ipFile)
ipData = f.read()
ipAddress = ipData.split('\n', 1)[0]
ipAddress = '127.0.0.1'
try:
ipFile = "/etc/cyberpanel/machineIP"
with open(ipFile, 'r') as f:
ipData = f.read()
first_line = ipData.split('\n', 1)[0].strip()
if first_line:
ipAddress = first_line
except (OSError, IOError, IndexError):
pass
if mailUtilities.checkIfRspamdInstalled() == 1:
checkIfRspamdInstalled = 1
rspamd_ui_url = request.build_absolute_uri('/emailPremium/Rspamd/ui/')
proc = httpProc(request, 'emailPremium/Rspamd.html',
{'checkIfRspamdInstalled': checkIfRspamdInstalled, 'ipAddress': ipAddress}, 'admin')
{
'checkIfRspamdInstalled': checkIfRspamdInstalled,
'ipAddress': ipAddress,
'rspamd_ui_url': rspamd_ui_url,
}, 'admin')
return proc.render()
###Rspamd
_RSPAMD_UPSTREAM = ('127.0.0.1', 11334)
_RSPAMD_HOP_RESPONSE = frozenset({
'connection', 'transfer-encoding', 'keep-alive', 'proxy-authenticate',
'proxy-authorization', 'te', 'trailers', 'upgrade', 'content-encoding',
})
@csrf_exempt
def rspamd_ui_proxy(request, subpath=None):
"""Reverse-proxy Rspamd controller UI (localhost:11334) for logged-in admins only."""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return ACLManager.loadError()
except KeyError:
return redirect(loadLoginPage)
if mailUtilities.checkIfRspamdInstalled() != 1:
return HttpResponse(
'Rspamd is not installed.',
status=503,
content_type='text/plain; charset=utf-8',
)
proxy_base = request.build_absolute_uri('/emailPremium/Rspamd/ui').rstrip('/')
path = '/'
if subpath:
path = '/' + subpath.lstrip('/')
q = request.META.get('QUERY_STRING', '')
if q:
full_path = path + '?' + q
else:
full_path = path
forward_method = request.method
if forward_method == 'HEAD':
forward_method = 'GET'
body = None
if forward_method in ('POST', 'PUT', 'PATCH', 'DELETE'):
body = request.body
headers = {}
acc = request.META.get('HTTP_ACCEPT')
if acc:
headers['Accept'] = acc
al = request.META.get('HTTP_ACCEPT_LANGUAGE')
if al:
headers['Accept-Language'] = al
ua = request.META.get('HTTP_USER_AGENT')
if ua:
headers['User-Agent'] = ua
auth = request.META.get('HTTP_AUTHORIZATION')
if auth:
headers['Authorization'] = auth
ct = request.META.get('CONTENT_TYPE')
if ct and forward_method in ('POST', 'PUT', 'PATCH'):
headers['Content-Type'] = ct
cookie = request.META.get('HTTP_COOKIE')
if cookie:
headers['Cookie'] = cookie
xhr = request.META.get('HTTP_X_REQUESTED_WITH')
if xhr:
headers['X-Requested-With'] = xhr
# Rspamd 3.8+ controller uses custom headers (not only query params). The stock
# proxy whitelist omitted these, which breaks getmap/savemap and can break saveactions.
_rspamd_hdr_map = (
('HTTP_PASSWORD', 'Password'),
('HTTP_MAP', 'Map'),
('HTTP_IP', 'IP'),
('HTTP_USER', 'User'),
('HTTP_FROM', 'From'),
('HTTP_RCPT', 'Rcpt'),
('HTTP_HELO', 'Helo'),
('HTTP_HOSTNAME', 'Hostname'),
('HTTP_PASS', 'Pass'),
('HTTP_CLASSIFIER', 'classifier'),
('HTTP_CLASS', 'class'),
('HTTP_FLAG', 'flag'),
('HTTP_WEIGHT', 'weight'),
('HTTP_HASH', 'Hash'),
)
for meta_key, out_name in _rspamd_hdr_map:
val = request.META.get(meta_key)
if val:
headers[out_name] = val
conn = None
try:
conn = http.client.HTTPConnection(
_RSPAMD_UPSTREAM[0], _RSPAMD_UPSTREAM[1], timeout=120,
)
conn.request(forward_method, full_path, body=body, headers=headers)
upstream = conn.getresponse()
data = upstream.read()
status = upstream.status
except (ConnectionRefusedError, OSError, http.client.HTTPException) as _e:
logging.CyberCPLogFileWriter.writeToFile(
'rspamd_ui_proxy upstream error: %s' % (type(_e).__name__,),
)
return HttpResponse(
'Could not reach Rspamd on 127.0.0.1:11334. Is rspamd running?',
status=502,
content_type='text/plain; charset=utf-8',
)
finally:
if conn is not None:
try:
conn.close()
except Exception:
pass
if request.method == 'HEAD':
out = HttpResponse(status=status)
data = b''
else:
out = HttpResponse(data, status=status)
for hdr, val in upstream.getheaders():
key = hdr.lower()
if key in _RSPAMD_HOP_RESPONSE:
continue
if key == 'location':
val = _rewrite_rspamd_location(val, proxy_base)
if request.method == 'HEAD' and key == 'content-length':
continue
out[hdr] = val
return out
def _rewrite_rspamd_location(location, proxy_base):
if not location:
return location
if location.startswith('http://127.0.0.1:11334'):
return proxy_base + location[len('http://127.0.0.1:11334'):]
if location.startswith('http://[::1]:11334'):
return proxy_base + location[len('http://[::1]:11334'):]
if location.startswith('/') and not location.startswith('//'):
return proxy_base + location
return location
def installRspamd(request):
try:
userID = request.session['userID']

View File

@@ -1475,8 +1475,8 @@
</div>
</div>
<!-- Firewall Rules Pagination (show when there are rules or a total count) -->
<div class="pagination-bar" ng-if="(rules && rules.length > 0) || (rulesTotalCount > 0)">
<!-- Firewall Rules Pagination: use ng-show (not ng-if) so ng-model binds to firewallController; ng-if creates a child scope and breaks Per page / Go to page. -->
<div class="pagination-bar" ng-show="(rules && rules.length > 0) || (rulesTotalCount > 0)">
<div class="pagination-info">
<span>{% trans "Showing" %} {$ rulesRangeStart() || 0 $} {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $}</span>
<span class="pagination-size">
@@ -1680,8 +1680,8 @@
</table>
</div>
<!-- Banned IPs Pagination (show when there are rows or a total count) -->
<div class="pagination-bar" ng-if="(bannedIPs && bannedIPs.length > 0) || (bannedTotalCount > 0)">
<!-- Banned IPs Pagination: ng-show avoids child scope from ng-if (same ng-model issue as rules). -->
<div class="pagination-bar" ng-show="(bannedIPs && bannedIPs.length > 0) || (bannedTotalCount > 0)">
<div class="pagination-info">
<span>{% trans "Showing" %} {$ bannedRangeStart() || 0 $} {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $}</span>
<span class="pagination-size banned-ips-per-page">

View File

@@ -4839,6 +4839,13 @@ user_query = SELECT email as user, password, 'vmail' as uid, 'vmail' as gid, '/h
command = f'/usr/local/lsws/lsphp80/bin/php /usr/local/CyberCP/snappymail_cyberpanel.php'
preFlightsChecks.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
try:
from plogical.snappymail_plugin_utilities import install_and_enable_list_unsubscribe_header_plugin
if install_and_enable_list_unsubscribe_header_plugin():
logging.InstallLog.writeToFile("SnappyMail list-unsubscribe-header plugin installed and enabled", 0)
except BaseException as plug_msg:
logging.InstallLog.writeToFile("Warning: list-unsubscribe SnappyMail plugin: " + str(plug_msg), 0)
except BaseException as msg:
logging.InstallLog.writeToFile('[ERROR] ' + str(msg) + " [downoad_and_install_snappymail]")

View File

@@ -0,0 +1,83 @@
<?php
/**
* Bundled with CyberPanel (see plogical/snappymail_plugin_utilities.py).
* Upstream: https://github.com/master3395/snappymail-list-unsubscribe-header
*
* Adds List-Unsubscribe and List-Unsubscribe-Post on outbound mail (RFC 2369 / RFC 8058).
*/
class ListUnsubscribeHeaderPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'List-Unsubscribe headers',
AUTHOR = 'Master3395',
URL = 'https://newstargeted.com/',
VERSION = '1.1.0',
RELEASE = '2026-04-11',
REQUIRED = '2.0.0',
DESCRIPTION = 'Adds List-Unsubscribe and List-Unsubscribe-Post for bulk/mailing-list best practices.';
public function Init() : void
{
$this->addHook('filter.send-message', 'FilterSendMessage');
}
/**
* @return array
*/
protected function configMapping() : array
{
return array(
\RainLoop\Plugins\Property::NewInstance('unsubscribe_https_url')
->SetLabel('HTTPS unsubscribe URL')
->SetType(\RainLoop\Enumerations\PluginPropertyType::URL)
->SetDescription('Must be https:// — used in List-Unsubscribe (RFC 8058 one-click target).')
->SetDefaultValue('https://newstargeted.com/list-unsubscribe-endpoint.php'),
\RainLoop\Plugins\Property::NewInstance('mailto_unsubscribe')
->SetLabel('Unsubscribe mailto address')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)
->SetDescription('Email only (e.g. postmaster@example.com) or full mailto:user@example.com')
->SetDefaultValue('postmaster@newstargeted.com'),
\RainLoop\Plugins\Property::NewInstance('one_click_post')
->SetLabel('Send List-Unsubscribe-Post (one-click)')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDescription('RFC 8058 — disable if your HTTPS URL does not accept POST.')
->SetDefaultValue(true)
);
}
/**
* @param \MailSo\Mime\Message $oMessage
*/
public function FilterSendMessage(&$oMessage) : void
{
if ($oMessage instanceof \MailSo\Mime\Message) {
$sHttps = \trim((string) $this->Config()->Get('plugin', 'unsubscribe_https_url', 'https://newstargeted.com/list-unsubscribe-endpoint.php'));
$sMailRaw = \trim((string) $this->Config()->Get('plugin', 'mailto_unsubscribe', 'postmaster@newstargeted.com'));
$bOneClick = (bool) $this->Config()->Get('plugin', 'one_click_post', true);
if ($sHttps === '' || !\preg_match('#^https://#i', $sHttps)) {
$sHttps = 'https://newstargeted.com/list-unsubscribe-endpoint.php';
}
if ($sMailRaw === '') {
$sMailto = 'mailto:postmaster@newstargeted.com';
} elseif (\stripos($sMailRaw, 'mailto:') === 0) {
$sMailto = $sMailRaw;
} else {
$sMailto = 'mailto:' . $sMailRaw;
}
if (!\preg_match('#^mailto:[^<>\s]+$#i', $sMailto)) {
$sMailto = 'mailto:postmaster@newstargeted.com';
}
$sListUnsub = '<' . $sHttps . '>, <' . $sMailto . '>';
$oMessage->SetCustomHeader(
\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE,
$sListUnsub
);
if ($bOneClick) {
$oMessage->SetCustomHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
}
}
}
}

View File

@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
"""
Bundle and enable the SnappyMail list-unsubscribe-header plugin (CyberPanel).
Data may live under snappymail/ or legacy rainloop/ until migration completes.
Upstream plugin: https://github.com/master3395/snappymail-list-unsubscribe-header
"""
import os
import re
import shutil
import subprocess
PLUGIN_ID_LIST_UNSUBSCRIBE = 'list-unsubscribe-header'
BUNDLE_REL = 'install/snappymail/plugins/list-unsubscribe-header/index.php'
CYBERCP_ROOT = '/usr/local/CyberCP'
DATA_ROOT_CANDIDATES = (
'/usr/local/lscp/cyberpanel/snappymail/data',
'/usr/local/lscp/cyberpanel/rainloop/data',
)
def bundled_list_unsubscribe_src():
return os.path.join(CYBERCP_ROOT, BUNDLE_REL)
def _merge_enabled_list_line(line, plugin_id):
"""Merge plugin_id into SnappyMail enabled_list = \"...\" line."""
m = re.match(r'^(\s*enabled_list\s*=\s*")([^"]*)("\s*)\r?$', line)
if not m:
return line
inner = m.group(2)
parts = [p.strip() for p in inner.split(',') if p.strip()]
if plugin_id not in parts:
parts.append(plugin_id)
inner_new = ','.join(parts)
return m.group(1) + inner_new + m.group(3) + ('\n' if line.endswith('\n') else '')
def _force_plugins_enable_on_line(line):
"""Under [plugins], ensure enable = On."""
if not re.match(r'^\s*enable\s*=', line):
return line
if re.search(r'(?i)=\s*On\b', line):
return line
return re.sub(r'(?i)=\s*\S+', '= On', line, count=1)
def merge_plugin_into_application_ini(application_ini_path, plugin_id):
"""
Merge plugin_id into [plugins] enabled_list; set enable = On when that key is present.
"""
if not os.path.isfile(application_ini_path):
return False
with open(application_ini_path, 'r') as f:
lines = f.readlines()
out = []
in_plugins = False
for line in lines:
stripped = line.lstrip()
if stripped.startswith('[plugins]'):
in_plugins = True
out.append(line)
continue
if in_plugins and stripped.startswith('[') and not stripped.startswith('[plugins]'):
in_plugins = False
if in_plugins and re.match(r'^\s*enabled_list\s*=', line):
out.append(_merge_enabled_list_line(line, plugin_id))
continue
if in_plugins and re.match(r'^\s*enable\s*=', line):
out.append(_force_plugins_enable_on_line(line))
continue
out.append(line)
with open(application_ini_path, 'w') as f:
f.writelines(out)
return True
def _chown_lscpd(path):
try:
subprocess.check_call(['chown', 'lscpd:lscpd', path], stderr=subprocess.DEVNULL)
except (OSError, subprocess.CalledProcessError):
pass
def _copy_bundled_plugin_to_data_root(data_root):
"""Copy bundled index.php into .../plugins/list-unsubscribe-header/."""
src = bundled_list_unsubscribe_src()
if not os.path.isfile(src):
return False
dest_dir = os.path.join(
data_root, '_data_', '_default_', 'plugins', PLUGIN_ID_LIST_UNSUBSCRIBE
)
try:
os.makedirs(dest_dir, mode=0o700, exist_ok=True)
except OSError:
return False
dest_file = os.path.join(dest_dir, 'index.php')
try:
shutil.copy2(src, dest_file)
except (OSError, IOError):
return False
try:
os.chmod(dest_file, 0o644)
except OSError:
pass
_chown_lscpd(dest_file)
_chown_lscpd(dest_dir)
return True
def install_and_enable_list_unsubscribe_header_plugin():
"""
Copy bundled plugin into each existing SnappyMail data root and merge into enabled_list.
Idempotent. Returns 1 if at least one data root was updated, else 0.
"""
if not os.path.isfile(bundled_list_unsubscribe_src()):
return 0
ok = 0
for root in DATA_ROOT_CANDIDATES:
default_path = os.path.join(root, '_data_', '_default_')
if not os.path.isdir(default_path):
continue
if not _copy_bundled_plugin_to_data_root(root):
continue
app_ini = os.path.join(default_path, 'configs', 'application.ini')
if os.path.isfile(app_ini):
merge_plugin_into_application_ini(app_ini, PLUGIN_ID_LIST_UNSUBSCRIBE)
try:
os.chmod(app_ini, 0o600)
except OSError:
pass
_chown_lscpd(app_ini)
ok = 1
return ok

View File

@@ -1587,6 +1587,13 @@ $cfg['Servers'][$i]['port'] = '3306';
command = f'/usr/local/lsws/lsphp83/bin/php /usr/local/CyberCP/snappymail_cyberpanel.php'
Upgrade.executioner_silent(command, 'verify certificate', 0)
try:
from plogical.snappymail_plugin_utilities import install_and_enable_list_unsubscribe_header_plugin
if install_and_enable_list_unsubscribe_header_plugin():
Upgrade.stdOut("SnappyMail list-unsubscribe-header plugin installed and enabled", 0)
except BaseException as plug_msg:
Upgrade.stdOut("Warning: list-unsubscribe SnappyMail plugin: " + str(plug_msg), 0)
# labsPath = '/usr/local/lscp/cyberpanel/rainloop/data/_data_/_default_/configs/application.ini'
# labsData = """[labs]

View File

@@ -974,6 +974,15 @@ app.controller('listContainers', function ($scope, $http) {
return;
}
if ($scope.dockerUpdateInProgress) {
new PNotify({
title: 'Update in progress',
text: 'Wait until the current update finishes before starting another.',
type: 'warning'
});
return;
}
// If no new image specified, use current image
if (!$scope.newImage) {
$scope.newImage = $scope.currentImage;
@@ -1000,9 +1009,18 @@ app.controller('listContainers', function ($scope, $http) {
history: false
}
})).get().on('pnotify.confirm', function () {
var dockerUpdateNotificationId = null;
$scope.dockerUpdateInProgress = true;
$('#imageLoading').show();
$("#updateContainer").modal("hide");
if (typeof window.cpDockerUpdateNotifyStart === 'function') {
dockerUpdateNotificationId = window.cpDockerUpdateNotifyStart(
$scope.updateContainerName,
$scope.newImage + ':' + $scope.newTag
);
}
url = "/docker/updateContainer";
var data = {
name: $scope.updateContainerName,
@@ -1020,15 +1038,27 @@ app.controller('listContainers', function ($scope, $http) {
function ListInitialData(response) {
console.log(response);
$scope.dockerUpdateInProgress = false;
$('#imageLoading').hide();
if (response.data.updateContainerStatus === 1) {
var ok = response.data && response.data.updateContainerStatus === 1;
var imgLabel = ok
? (response.data.new_image || response.data.message || 'Updated')
: (response.data && response.data.error_message ? response.data.error_message : 'Update failed');
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, ok, imgLabel);
}
if (ok) {
new PNotify({
title: 'Container Updated Successfully',
text: `Container updated to ${response.data.new_image}`,
text: 'Container updated to ' + (response.data.new_image || response.data.message || 'new image'),
type: 'success'
});
location.reload();
setTimeout(function () {
location.reload();
}, 2200);
} else {
new PNotify({
title: 'Update Failed',
@@ -1039,7 +1069,11 @@ app.controller('listContainers', function ($scope, $http) {
}
function cantLoadInitialData(response) {
$scope.dockerUpdateInProgress = false;
$('#imageLoading').hide();
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, false, 'Could not connect to server');
}
new PNotify({
title: 'Update Failed',
text: 'Could not connect to server',
@@ -1355,6 +1389,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.cName = "";
$scope.status = "";
$scope.savingSettings = false;
$scope.currentPorts = {};
$scope.initialPortsForSettings = {};
$scope.loadingTop = false;
$scope.statusInterval = null;
$scope.statsInterval = null;
@@ -1364,7 +1400,37 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.advancedEnvText = '';
$scope.advancedEnvCount = 0;
$scope.parsedEnvVars = {};
/** Flat map from DB (container_port -> host_port), set from template ng-init */
$scope.serverPortMap = {};
$scope.initAngularPortsFromServer = function (obj) {
if (obj === undefined || obj === null) {
$scope.serverPortMap = {};
$scope.ports = {};
return;
}
if (angular.isString(obj)) {
try {
obj = angular.fromJson(obj);
} catch (e1) {
try {
obj = JSON.parse(obj);
} catch (e2) {
$scope.serverPortMap = {};
$scope.ports = {};
return;
}
}
}
if (!angular.isObject(obj)) {
$scope.serverPortMap = {};
$scope.ports = {};
return;
}
$scope.serverPortMap = obj;
$scope.ports = obj;
};
// Auto-refresh status every 5 seconds
$scope.startStatusMonitoring = function() {
$scope.statusInterval = $interval(function() {
@@ -1994,6 +2060,13 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
}
};
/** Used after legacy port-update path; full page reload syncs Django-rendered state */
$scope.refreshContainerInfo = function () {
$timeout(function () {
window.location.reload();
}, 300);
};
$scope.addVolField = function () {
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
$scope.volListNumber = $scope.volListNumber + 1;
@@ -2004,6 +2077,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
};
$scope.saveSettings = function () {
if ($scope.portsDirty && $scope.portsDirty() && !$scope.envConfirmation) {
new PNotify({
title: 'Confirmation required',
text: 'Check the confirmation box to apply changes to ports, environment variables, or volumes (the container will be recreated).',
type: 'warning'
});
return;
}
$('#containerSettingLoading').show();
url = "/docker/saveContainerSettings";
$scope.savingSettings = true;
@@ -2029,7 +2111,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
envConfirmation: $scope.envConfirmation,
envList: finalEnvList,
volList: $scope.volList,
advancedEnvMode: $scope.advancedEnvMode
advancedEnvMode: $scope.advancedEnvMode,
ports: $scope.currentPorts || {}
};
@@ -2054,6 +2137,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
title: 'Settings Saved',
type: 'success'
});
$scope.initialPortsForSettings = angular.copy($scope.currentPorts || {});
}
}
else {
@@ -2179,22 +2263,55 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$("#commandModal").modal("show");
};
// Port editing functionality
$scope.showPortEditModal = function() {
// Initialize current ports from container data
// Port editing (in Container Settings modal)
$scope.initSettingsPortsFromContainer = function () {
$scope.currentPorts = {};
if ($scope.ports) {
for (var iport in $scope.ports) {
var eport = $scope.ports[iport];
if (eport && eport.length > 0) {
var src = $scope.serverPortMap && Object.keys($scope.serverPortMap).length
? $scope.serverPortMap
: ($scope.ports || {});
if (src && typeof src === 'object') {
for (var iport in src) {
if (!Object.prototype.hasOwnProperty.call(src, iport)) {
continue;
}
var eport = src[iport];
if (angular.isArray(eport) && eport.length > 0 && eport[0] && eport[0].HostPort) {
$scope.currentPorts[iport] = eport[0].HostPort;
} else if (eport !== undefined && eport !== null && eport !== '') {
$scope.currentPorts[iport] = String(eport);
}
}
}
$("#portEditModal").modal("show");
$scope.initialPortsForSettings = angular.copy($scope.currentPorts);
};
$scope.addNewPortMapping = function() {
$scope.portsJsonSnapshot = function (obj) {
if (!obj || typeof obj !== 'object') {
return '{}';
}
var keys = Object.keys(obj).sort();
var flat = {};
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
var v = obj[k];
flat[k] = v === null || v === undefined ? '' : String(v);
}
return JSON.stringify(flat);
};
$scope.portsDirty = function () {
return $scope.portsJsonSnapshot($scope.currentPorts) !== $scope.portsJsonSnapshot($scope.initialPortsForSettings);
};
$scope.showPortEditModal = function () {
$scope.initSettingsPortsFromContainer();
$("#settings").modal("show");
};
$scope.addNewPortMapping = function () {
if (!$scope.envConfirmation) {
return;
}
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
if (containerPort) {
$scope.currentPorts[containerPort] = '';
@@ -2202,14 +2319,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
}
};
$scope.removePortMapping = function(containerPort) {
$scope.removePortMapping = function (containerPort) {
if (!$scope.envConfirmation) {
return;
}
if (confirm('Are you sure you want to remove this port mapping?')) {
delete $scope.currentPorts[containerPort];
}
};
$scope.updatePortMappings = function() {
$("#portEditLoading").show();
$scope.updatePortMappings = function () {
$scope.updatingPorts = true;
var url = "/docker/updateContainerPorts";
@@ -2225,18 +2344,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
};
$http.post(url, data, config).then(function(response) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
if (response.data.status === 1) {
$("#portEditModal").modal("hide");
// Refresh container status and ports
$scope.refreshContainerInfo();
$("#settings").modal("hide");
new PNotify({
title: 'Success',
text: 'Port mappings updated successfully',
type: 'success'
});
$scope.refreshContainerInfo();
} else {
new PNotify({
title: 'Error',
@@ -2245,11 +2362,10 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
});
}
}, function(error) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
new PNotify({
title: 'Error',
text: 'Error updating port mappings: ' + error.data.error_message,
text: 'Error updating port mappings: ' + (error.data && error.data.error_message ? error.data.error_message : 'unknown'),
type: 'error'
});
});
@@ -2354,12 +2470,55 @@ app.controller('manageImages', function ($scope, $http) {
$scope.showingSearch = false;
$("#searchResult").hide();
function dockerHistoryFormatSize(raw) {
var n = parseInt(raw, 10);
if (isNaN(n) || n < 0) {
return (raw === undefined || raw === null) ? '' : String(raw);
}
if (n === 0) {
return '0 B';
}
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
var v = n / Math.pow(1024, i);
var s = (i === 0) ? String(v) : (Math.round(v * 10) / 10).toFixed(1);
return s + ' ' + units[i];
}
function dockerHistoryFormatCreated(raw) {
if (raw === undefined || raw === null) {
return '';
}
var n = Number(raw);
if (isNaN(n)) {
return String(raw);
}
var sec = n;
if (n > 1e14) {
sec = Math.floor(n / 1e9);
} else if (n > 1e12) {
sec = Math.floor(n / 1e6);
}
var d = new Date(sec * 1000);
if (isNaN(d.getTime())) {
return String(raw);
}
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
}
function dockerHistoryEnrichLayer(h) {
var o = angular.extend({}, h);
o.SizeHuman = dockerHistoryFormatSize(h.Size);
o.CreatedDisplay = dockerHistoryFormatCreated(h.Created);
return o;
}
$scope.pullImage = function (image, tag) {
function ListInitialDatas(response) {
if (response.data.installImageStatus === 1) {
new PNotify({
title: 'Image pulled successfully',
text: 'Reloading...',
text: 'Running containers keep the old image until you recreate them from Docker > Containers. Reloading',
type: 'success'
});
location.reload()
@@ -2390,7 +2549,8 @@ app.controller('manageImages', function ($scope, $http) {
url = "/docker/installImage";
var data = {
image: image,
tag: tag
tag: tag,
force_update: true
};
var config = {
headers: {
@@ -2411,6 +2571,30 @@ app.controller('manageImages', function ($scope, $http) {
}
$scope.refreshLocalImage = function (rowId, imageName) {
var sel = document.getElementById(String(rowId));
if (!sel || sel.selectedIndex < 0) {
new PNotify({
title: 'Unable to complete request',
text: 'Please select a tag for this image',
type: 'info'
});
return;
}
var raw = sel.options[sel.selectedIndex].text;
if (!raw) {
new PNotify({
title: 'Unable to complete request',
text: 'Please select a tag for this image',
type: 'info'
});
return;
}
var li = raw.lastIndexOf(':');
var tag = li > -1 ? raw.substring(li + 1) : raw;
$scope.pullImage(imageName, tag);
}
$scope.searchImages = function () {
console.log($scope.searchString);
if (!$scope.searchString) {
@@ -2546,7 +2730,8 @@ app.controller('manageImages', function ($scope, $http) {
$scope.getHistory = function (counter) {
$('#imageLoading').show();
var name = $("#" + counter).val()
var sel = $("#" + counter);
var name = (sel.find('option:selected').text() || sel.val() || '').trim();
url = "/docker/getImageHistory";
@@ -2566,7 +2751,8 @@ app.controller('manageImages', function ($scope, $http) {
if (response.data.imageHistoryStatus === 1) {
$('#history').modal('show');
$scope.historyList = response.data.history;
var raw = response.data.history || [];
$scope.historyList = raw.map(dockerHistoryEnrichLayer);
}
else {
new PNotify({
@@ -2713,4 +2899,3 @@ app.controller('manageImages', function ($scope, $http) {
})
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -1389,6 +1389,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.cName = "";
$scope.status = "";
$scope.savingSettings = false;
$scope.currentPorts = {};
$scope.initialPortsForSettings = {};
$scope.loadingTop = false;
$scope.statusInterval = null;
$scope.statsInterval = null;
@@ -1398,7 +1400,37 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.advancedEnvText = '';
$scope.advancedEnvCount = 0;
$scope.parsedEnvVars = {};
/** Flat map from DB (container_port -> host_port), set from template ng-init */
$scope.serverPortMap = {};
$scope.initAngularPortsFromServer = function (obj) {
if (obj === undefined || obj === null) {
$scope.serverPortMap = {};
$scope.ports = {};
return;
}
if (angular.isString(obj)) {
try {
obj = angular.fromJson(obj);
} catch (e1) {
try {
obj = JSON.parse(obj);
} catch (e2) {
$scope.serverPortMap = {};
$scope.ports = {};
return;
}
}
}
if (!angular.isObject(obj)) {
$scope.serverPortMap = {};
$scope.ports = {};
return;
}
$scope.serverPortMap = obj;
$scope.ports = obj;
};
// Auto-refresh status every 5 seconds
$scope.startStatusMonitoring = function() {
$scope.statusInterval = $interval(function() {
@@ -2028,6 +2060,13 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
}
};
/** Used after legacy port-update path; full page reload syncs Django-rendered state */
$scope.refreshContainerInfo = function () {
$timeout(function () {
window.location.reload();
}, 300);
};
$scope.addVolField = function () {
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
$scope.volListNumber = $scope.volListNumber + 1;
@@ -2038,6 +2077,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
};
$scope.saveSettings = function () {
if ($scope.portsDirty && $scope.portsDirty() && !$scope.envConfirmation) {
new PNotify({
title: 'Confirmation required',
text: 'Check the confirmation box to apply changes to ports, environment variables, or volumes (the container will be recreated).',
type: 'warning'
});
return;
}
$('#containerSettingLoading').show();
url = "/docker/saveContainerSettings";
$scope.savingSettings = true;
@@ -2063,7 +2111,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
envConfirmation: $scope.envConfirmation,
envList: finalEnvList,
volList: $scope.volList,
advancedEnvMode: $scope.advancedEnvMode
advancedEnvMode: $scope.advancedEnvMode,
ports: $scope.currentPorts || {}
};
@@ -2088,6 +2137,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
title: 'Settings Saved',
type: 'success'
});
$scope.initialPortsForSettings = angular.copy($scope.currentPorts || {});
}
}
else {
@@ -2213,22 +2263,55 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$("#commandModal").modal("show");
};
// Port editing functionality
$scope.showPortEditModal = function() {
// Initialize current ports from container data
// Port editing (in Container Settings modal)
$scope.initSettingsPortsFromContainer = function () {
$scope.currentPorts = {};
if ($scope.ports) {
for (var iport in $scope.ports) {
var eport = $scope.ports[iport];
if (eport && eport.length > 0) {
var src = $scope.serverPortMap && Object.keys($scope.serverPortMap).length
? $scope.serverPortMap
: ($scope.ports || {});
if (src && typeof src === 'object') {
for (var iport in src) {
if (!Object.prototype.hasOwnProperty.call(src, iport)) {
continue;
}
var eport = src[iport];
if (angular.isArray(eport) && eport.length > 0 && eport[0] && eport[0].HostPort) {
$scope.currentPorts[iport] = eport[0].HostPort;
} else if (eport !== undefined && eport !== null && eport !== '') {
$scope.currentPorts[iport] = String(eport);
}
}
}
$("#portEditModal").modal("show");
$scope.initialPortsForSettings = angular.copy($scope.currentPorts);
};
$scope.addNewPortMapping = function() {
$scope.portsJsonSnapshot = function (obj) {
if (!obj || typeof obj !== 'object') {
return '{}';
}
var keys = Object.keys(obj).sort();
var flat = {};
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
var v = obj[k];
flat[k] = v === null || v === undefined ? '' : String(v);
}
return JSON.stringify(flat);
};
$scope.portsDirty = function () {
return $scope.portsJsonSnapshot($scope.currentPorts) !== $scope.portsJsonSnapshot($scope.initialPortsForSettings);
};
$scope.showPortEditModal = function () {
$scope.initSettingsPortsFromContainer();
$("#settings").modal("show");
};
$scope.addNewPortMapping = function () {
if (!$scope.envConfirmation) {
return;
}
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
if (containerPort) {
$scope.currentPorts[containerPort] = '';
@@ -2236,14 +2319,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
}
};
$scope.removePortMapping = function(containerPort) {
$scope.removePortMapping = function (containerPort) {
if (!$scope.envConfirmation) {
return;
}
if (confirm('Are you sure you want to remove this port mapping?')) {
delete $scope.currentPorts[containerPort];
}
};
$scope.updatePortMappings = function() {
$("#portEditLoading").show();
$scope.updatePortMappings = function () {
$scope.updatingPorts = true;
var url = "/docker/updateContainerPorts";
@@ -2259,18 +2344,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
};
$http.post(url, data, config).then(function(response) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
if (response.data.status === 1) {
$("#portEditModal").modal("hide");
// Refresh container status and ports
$scope.refreshContainerInfo();
$("#settings").modal("hide");
new PNotify({
title: 'Success',
text: 'Port mappings updated successfully',
type: 'success'
});
$scope.refreshContainerInfo();
} else {
new PNotify({
title: 'Error',
@@ -2279,11 +2362,10 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
});
}
}, function(error) {
$("#portEditLoading").hide();
$scope.updatingPorts = false;
new PNotify({
title: 'Error',
text: 'Error updating port mappings: ' + error.data.error_message,
text: 'Error updating port mappings: ' + (error.data && error.data.error_message ? error.data.error_message : 'unknown'),
type: 'error'
});
});
@@ -2388,12 +2470,55 @@ app.controller('manageImages', function ($scope, $http) {
$scope.showingSearch = false;
$("#searchResult").hide();
function dockerHistoryFormatSize(raw) {
var n = parseInt(raw, 10);
if (isNaN(n) || n < 0) {
return (raw === undefined || raw === null) ? '' : String(raw);
}
if (n === 0) {
return '0 B';
}
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
var v = n / Math.pow(1024, i);
var s = (i === 0) ? String(v) : (Math.round(v * 10) / 10).toFixed(1);
return s + ' ' + units[i];
}
function dockerHistoryFormatCreated(raw) {
if (raw === undefined || raw === null) {
return '';
}
var n = Number(raw);
if (isNaN(n)) {
return String(raw);
}
var sec = n;
if (n > 1e14) {
sec = Math.floor(n / 1e9);
} else if (n > 1e12) {
sec = Math.floor(n / 1e6);
}
var d = new Date(sec * 1000);
if (isNaN(d.getTime())) {
return String(raw);
}
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
}
function dockerHistoryEnrichLayer(h) {
var o = angular.extend({}, h);
o.SizeHuman = dockerHistoryFormatSize(h.Size);
o.CreatedDisplay = dockerHistoryFormatCreated(h.Created);
return o;
}
$scope.pullImage = function (image, tag) {
function ListInitialDatas(response) {
if (response.data.installImageStatus === 1) {
new PNotify({
title: 'Image pulled successfully',
text: 'Reloading...',
text: 'Running containers keep the old image until you recreate them from Docker > Containers. Reloading',
type: 'success'
});
location.reload()
@@ -2424,7 +2549,8 @@ app.controller('manageImages', function ($scope, $http) {
url = "/docker/installImage";
var data = {
image: image,
tag: tag
tag: tag,
force_update: true
};
var config = {
headers: {
@@ -2445,6 +2571,30 @@ app.controller('manageImages', function ($scope, $http) {
}
$scope.refreshLocalImage = function (rowId, imageName) {
var sel = document.getElementById(String(rowId));
if (!sel || sel.selectedIndex < 0) {
new PNotify({
title: 'Unable to complete request',
text: 'Please select a tag for this image',
type: 'info'
});
return;
}
var raw = sel.options[sel.selectedIndex].text;
if (!raw) {
new PNotify({
title: 'Unable to complete request',
text: 'Please select a tag for this image',
type: 'info'
});
return;
}
var li = raw.lastIndexOf(':');
var tag = li > -1 ? raw.substring(li + 1) : raw;
$scope.pullImage(imageName, tag);
}
$scope.searchImages = function () {
console.log($scope.searchString);
if (!$scope.searchString) {
@@ -2580,7 +2730,8 @@ app.controller('manageImages', function ($scope, $http) {
$scope.getHistory = function (counter) {
$('#imageLoading').show();
var name = $("#" + counter).val()
var sel = $("#" + counter);
var name = (sel.find('option:selected').text() || sel.val() || '').trim();
url = "/docker/getImageHistory";
@@ -2600,7 +2751,8 @@ app.controller('manageImages', function ($scope, $http) {
if (response.data.imageHistoryStatus === 1) {
$('#history').modal('show');
$scope.historyList = response.data.history;
var raw = response.data.history || [];
$scope.historyList = raw.map(dockerHistoryEnrichLayer);
}
else {
new PNotify({

View File

@@ -2,6 +2,26 @@
* Created by usman on 6/22/18.
*/
/* Ensure CyberCP Angular module exists (system-status.js normally sets window.app in <head>) */
(function () {
if (typeof window === 'undefined' || typeof angular === 'undefined') {
return;
}
if (typeof window.app !== 'undefined' && window.app) {
return;
}
try {
window.app = angular.module('CyberCP');
} catch (e) {
try {
window.app = angular.module('CyberCP', []);
} catch (e2) {
return;
}
}
})();
var app = window.app;
/* Java script code to list accounts */
app.controller('listDomains', function ($scope, $http) {