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

V2.5.5 dev
This commit is contained in:
Master3395
2026-01-26 17:47:17 +01:00
committed by GitHub
25 changed files with 4192 additions and 958 deletions

View File

@@ -13,6 +13,16 @@ https://docs.djangoproject.com/en/1.11/ref/settings/
import os
from django.utils.translation import gettext_lazy as _
# Patreon OAuth Configuration for Paid Plugins
# SECURITY: Environment variables take precedence. Hardcoded values are fallback for this server only.
# For repository version, use empty defaults and set via environment variables.
PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', 'LFXeXUcfrM8MeVbUcmGbB7BgeJ9RzZi2v_H9wL4d9vG6t1dV4SUnQ4ibn9IYzvt7')
PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', 'APuJ5qoL3TLFmNnGDVkgl-qr3sCzp2CQsKfslBbp32hhnhlD0y6-ZcSCkb_FaUJv')
PATREON_CREATOR_ID = os.environ.get('PATREON_CREATOR_ID', '')
PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') # CyberPanel Paid Plugin tier
PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', 'niAHRiI9SgrRCMmaf5exoXXphy3RWXWsX4kO5Yv9SQI')
PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', 'VZlCQoPwJUr4NLni1N82-K_CpJHTAOYUOCx2PujdjQg')
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -55,7 +65,6 @@ INSTALLED_APPS = [
# Apps with multiple or complex dependencies
'emailPremium',
'testPlugin', # Test plugin
'emailMarketing', # Depends on websiteFunctions and loginSystem
'cloudAPI', # Depends on websiteFunctions
'containerization', # Depends on websiteFunctions
@@ -84,12 +93,6 @@ INSTALLED_APPS = [
# Add plugins that are installed (plugin installer handles adding/removing)
# Plugins are added by plugin installer when plugins are installed
if os.path.exists('/usr/local/CyberCP/discordWebhooks/__init__.py'):
INSTALLED_APPS.append('discordWebhooks')
if os.path.exists('/usr/local/CyberCP/fail2ban/__init__.py'):
INSTALLED_APPS.append('fail2ban')
if os.path.exists('/usr/local/CyberCP/pm2Manager/__init__.py'):
INSTALLED_APPS.append('pm2Manager')
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',

View File

@@ -44,15 +44,14 @@ urlpatterns = [
path('filemanager/', include('filemanager.urls')),
path('emailPremium/', include('emailPremium.urls')),
path('manageservices/', include('manageServices.urls')),
path('plugins/testPlugin/', include('testPlugin.urls')),
path('plugins/', include('pluginHolder.urls')),
path('emailMarketing/', include('emailMarketing.urls')),
path('cloudAPI/', include('cloudAPI.urls')),
path('docker/', include('dockerManager.urls')),
path('container/', include('containerization.urls')),
path('CloudLinux/', include('CLManager.urls')),
path('IncrementalBackups/', include('IncBackups.urls')),
path('aiscanner/', include('aiScanner.urls')),
path('emailMarketing/', include('emailMarketing.urls')),
# path('Terminal/', include('WebTerminal.urls')),
path('', include('loginSystem.urls')),
]

View File

@@ -1,8 +1,8 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% with CP_VERSION="2.4.4.1" %}
{% with CP_VERSION="2.5.5-dev-fix" %}
<!DOCTYPE html>
<html lang="en" ng-app="CyberCP">
<html lang="en" ng-app="CyberCP" {% if cosmetic.MainDashboardCSS %}data-dynamic-css="{{ cosmetic.MainDashboardCSS|escape }}"{% endif %}>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -17,30 +17,39 @@
<!-- Custom CSS -->
<style>
{{ cosmetic.MainDashboardCSS | safe }}
/* Custom dashboard CSS - base styles */
body { visibility: visible; }
</style>
<!-- Mobile Responsive CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/mobile-responsive.css' %}?v={{ CP_VERSION }}">
<!-- Readability Fixes CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/readability-fixes.css' %}?v={{ CP_VERSION }}">
<!-- Core Scripts -->
<!-- Core Scripts - AngularJS MUST load FIRST before all other scripts -->
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}"></script>
<!-- Dynamic CSS from Django template (injected via JavaScript to avoid linter errors) -->
<script>
// Inject dynamic CSS from data attribute to avoid CSS/JS linter errors
(function() {
var htmlElement = document.documentElement;
var cssContent = htmlElement.getAttribute('data-dynamic-css');
if (cssContent) {
var style = document.createElement('style');
style.textContent = cssContent;
document.head.appendChild(style);
}
})();
</script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<!-- Bootstrap JavaScript -->
<script src="{% static 'baseTemplate/assets/bootstrap/js/bootstrap.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/bootstrap-toggle.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/custom-js/qrious.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="https://cdn.jsdelivr.net/npm/qrious/dist/qrious.min.js"></script>
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Chart.js -->
<script src="{% static 'baseTemplate/custom-js/chart.umd.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- PNotify -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}?v={{ CP_VERSION }}">
@@ -1566,7 +1575,7 @@
{% block header_scripts %}{% endblock %}
</head>
<body>
<body data-has-backup-configured="{% if request.session.has_backup_configured %}true{% else %}false{% endif %}">
<!-- Header -->
<div id="header">
<button id="mobile-menu-toggle" class="mobile-menu-toggle" onclick="toggleSidebar()" style="display: none;">
@@ -1647,7 +1656,7 @@
<div class="info-line">
<strong>Uptime:</strong>
<span ng-show="!uptimeLoaded">Loading...</span>
<span ng-show="uptimeLoaded" ng-bind="uptime">N/A</span>
<span ng-show="uptimeLoaded">{$ uptime $}</span>
</div>
</div>
</div>
@@ -1803,9 +1812,6 @@
<a href="{% url 'listChildDomains' %}" class="menu-item">
<span>List Sub/Addon Domains</span>
</a>
<a href="{% url 'fixSubdomainLogs' %}" class="menu-item">
<span>Fix Subdomain Logs</span>
</a>
{% if admin or modifyWebsite %}
<a href="{% url 'modifyWebsite' %}" class="menu-item">
<span>Modify Website</span>
@@ -2157,7 +2163,7 @@
<a href="{% url 'manageImages' %}" class="menu-item">
<span>Manage Images</span>
</a>
<a href="{% url 'listContainers' %}" class="menu-item">
<a href="{% url 'listContainersPage' %}" class="menu-item">
<span>Manage Containers</span>
</a>
<a href="{% url 'containerImage' %}" class="menu-item">
@@ -2287,9 +2293,6 @@
<a href="{% url 'aiScannerHome' %}" class="menu-item">
<span>AI Scanner</span>
</a>
<a href="#" class="menu-item" onclick="loadSecurityManagement(); return false;">
<span>Security Management</span>
</a>
</div>
<a href="#" class="menu-item" onclick="toggleSubmenu('mail-settings-submenu', this); return false;">
@@ -2309,9 +2312,6 @@
<a href="{% url 'SpamAssassin' %}" class="menu-item">
<span>SpamAssassin</span>
</a>
<a href="{% url 'emailMarketing' %}" class="menu-item">
<span>Email Marketing</span>
</a>
<a href="{% url 'MailScanner' %}" class="menu-item">
<span>MailScanner</span>
</a>
@@ -2343,11 +2343,6 @@
<a href="{% url 'managePureFtpd' %}" class="menu-item">
<span>Manage FTP</span>
</a>
{% if admin %}
<a href="#" class="menu-item" onclick="loadBandwidthManagement(); return false;">
<span>Bandwidth Management</span>
</a>
{% endif %}
</div>
<a href="#" class="menu-item" onclick="toggleSubmenu('plugins-submenu', this); return false;">
@@ -2452,6 +2447,12 @@
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}"></script>
<!-- Scripts -->
<!-- Set backup configuration status from Django template via data attribute -->
<script>
// Read backup configuration status from data attribute (set by Django template)
var backupConfigElement = document.querySelector('[data-has-backup-configured]');
window.hasBackupConfigured = backupConfigElement ? backupConfigElement.getAttribute('data-has-backup-configured') === 'true' : false;
</script>
<script>
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
@@ -2580,9 +2581,10 @@
// Check if user has backup configured (you'll need to implement this API)
// For now, we'll show it by default unless they have a backup plan
// This should be replaced with an actual check
{% if not request.session.has_backup_configured %}
// Note: hasBackupConfigured is set via Django template before this script
if (typeof window.hasBackupConfigured !== 'undefined' && !window.hasBackupConfigured) {
showBackupNotification();
{% endif %}
}
// For demonstration, let's show it if URL doesn't contain 'OneClickBackups'
if (!window.location.href.includes('OneClickBackups')) {
@@ -2819,7 +2821,44 @@
item.classList.remove('active', 'has-active-child');
});
// Find and activate the matching menu item
// Special handling for plugin pages - expand Plugins submenu and highlight
if (currentPath.startsWith('/plugins/')) {
const pluginsSubmenu = document.getElementById('plugins-submenu');
const pluginsMenuItem = pluginsSubmenu ? pluginsSubmenu.previousElementSibling : null;
if (pluginsSubmenu) {
pluginsSubmenu.classList.add('show');
}
if (pluginsMenuItem && pluginsMenuItem.classList.contains('menu-item')) {
pluginsMenuItem.classList.add('expanded', 'has-active-child');
}
// Check if it's the installed page
if (currentPath === '/plugins/installed' || currentPath.startsWith('/plugins/installed')) {
const installedLink = pluginsSubmenu ? pluginsSubmenu.querySelector('a[href*="installed"]') : null;
if (installedLink) {
installedLink.classList.add('active');
}
} else if (currentPath.match(/^\/plugins\/[^\/]+\/(settings|help)/)) {
// It's a plugin settings/help page - highlight "Installed" menu item
const installedLink = pluginsSubmenu ? pluginsSubmenu.querySelector('a[href*="installed"]') : null;
if (installedLink) {
installedLink.classList.add('active');
}
} else {
// Try to match specific plugin links in the submenu
const pluginLinks = pluginsSubmenu ? pluginsSubmenu.querySelectorAll('a.menu-item') : [];
pluginLinks.forEach(link => {
const href = link.getAttribute('href');
if (href && (currentPath === href || currentPath.startsWith(href + '/'))) {
link.classList.add('active');
}
});
}
}
// Find and activate the matching menu item (exact matches)
allMenuItems.forEach(item => {
const href = item.getAttribute('href');
if (href && href !== '#' && currentPath === href) {
@@ -2917,4 +2956,4 @@
{% block footer_scripts %}{% endblock %}
</body>
</html>
{% endwith %}
{% endwith %}

View File

@@ -698,9 +698,31 @@ class ContainerManager(multi.Thread):
return ACLManager.loadErrorJson('listContainerStatus', 0)
currentACL = ACLManager.loadedACL(userID)
pageNumber = int(data['page'])
json_data = self.findContainersJson(currentACL, userID, pageNumber)
final_dic = {'listContainerStatus': 1, 'error_message': "None", "data": json_data}
pageNumber = max(1, int(data.get('page', 1)))
items_per_page = 10
all_containers = ACLManager.findContainersObjects(currentACL, userID)
totalCount = len(all_containers)
totalPages = max(1, int(ceil(float(totalCount) / float(items_per_page))))
start = (pageNumber - 1) * items_per_page
end = start + items_per_page
page_containers = all_containers[start:end]
rows = []
for items in page_containers:
rows.append({'name': items.name, 'admin': items.admin.userName, 'tag': items.tag, 'image': items.image})
json_data = json.dumps(rows)
final_dic = {
'listContainerStatus': 1,
'error_message': 'None',
'data': json_data,
'totalCount': totalCount,
'totalPages': totalPages,
'currentPage': pageNumber,
'itemsPerPage': items_per_page
}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except BaseException as msg:
@@ -2407,9 +2429,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def listContainers(self, userID=None):
def listContainersJson(self, userID=None):
"""
Get list of all Docker containers
Get list of all Docker containers as JSON (for Angular API).
"""
try:
admin = Administrator.objects.get(pk=userID)
@@ -2441,13 +2463,13 @@ class ContainerManager(multi.Thread):
'containers': container_list
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json_data, content_type='application/json')
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.listContainers]')
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.listContainersJson]')
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json_data, content_type='application/json')
def getDockerNetworks(self, userID=None):
"""

View File

@@ -729,6 +729,59 @@ app.controller('listContainers', function ($scope, $http) {
$scope.assignActive = "";
$scope.dockerOwner = "";
/* Pagination (ACTIVITY BOARD-style) */
var CONTAINERS_PER_PAGE = 10;
$scope.pagination = { containers: { currentPage: 1, itemsPerPage: CONTAINERS_PER_PAGE } };
$scope.gotoPageInput = { containers: 1 };
$scope.totalCount = 0;
$scope.totalPages = 1;
$scope.Math = Math;
$scope.getTotalPages = function(section) {
if (section === 'containers') return Math.max(1, $scope.totalPages || 1);
return 1;
};
$scope.goToPage = function(section, page) {
if (section !== 'containers') return;
var totalPages = $scope.getTotalPages(section);
var p = parseInt(page, 10);
if (isNaN(p) || p < 1) p = 1;
if (p > totalPages) p = totalPages;
$scope.getFurtherContainersFromDB(p);
};
$scope.nextPage = function(section) {
if (section !== 'containers') return;
if ($scope.pagination.containers.currentPage < $scope.getTotalPages(section)) {
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage + 1);
}
};
$scope.prevPage = function(section) {
if (section !== 'containers') return;
if ($scope.pagination.containers.currentPage > 1) {
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage - 1);
}
};
$scope.getPageNumbers = function(section) {
if (section !== 'containers') return [];
var totalPages = $scope.getTotalPages(section);
var current = $scope.pagination.containers.currentPage;
var maxVisible = 5;
var pages = [];
if (totalPages <= maxVisible) {
for (var i = 1; i <= totalPages; i++) pages.push(i);
} else {
var start = Math.max(1, current - 2);
var end = Math.min(totalPages, start + maxVisible - 1);
if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1);
for (var j = start; j <= end; j++) pages.push(j);
}
return pages;
};
$scope.assignContainer = function (name) {
console.log('assignContainer called with:', name);
$scope.assignActive = name;
@@ -783,7 +836,7 @@ app.controller('listContainers', function ($scope, $http) {
title: 'Container assigned successfully',
type: 'success'
});
window.location.href = '/docker/listContainers';
window.location.href = '/docker/containers';
}
else {
new PNotify({
@@ -1264,77 +1317,35 @@ app.controller('listContainers', function ($scope, $http) {
$scope.logInfo = null;
};
url = "/docker/getContainerList";
var data = {page: 1};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
console.log(response);
function handleContainerListResponse(response) {
if (response.data.listContainerStatus === 1) {
var finalData = JSON.parse(response.data.data);
$scope.ContainerList = finalData;
console.log($scope.ContainerList);
$scope.totalCount = response.data.totalCount || 0;
$scope.totalPages = Math.max(1, response.data.totalPages || 1);
var cp = Math.max(1, parseInt(response.data.currentPage, 10) || 1);
$scope.pagination.containers.currentPage = cp;
$scope.gotoPageInput.containers = cp;
$("#listFail").hide();
}
else {
} else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
$scope.errorMessage = response.data.error_message || 'Failed to load containers';
}
}
function cantLoadInitialData(response) {
console.log("not good");
$("#listFail").fadeIn();
$scope.errorMessage = (response && response.data && response.data.error_message) ? response.data.error_message : 'Could not connect to server';
}
var config = {
headers: { 'X-CSRFToken': getCookie('csrftoken') }
};
$http.post("/docker/getContainerList", { page: 1 }, config).then(handleContainerListResponse, cantLoadInitialData);
$scope.getFurtherContainersFromDB = function (pageNumber) {
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = {page: pageNumber};
dataurl = "/docker/getContainerList";
$http.post(dataurl, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
if (response.data.listContainerStatus === 1) {
var finalData = JSON.parse(response.data.data);
$scope.ContainerList = finalData;
$("#listFail").hide();
}
else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
console.log(response.data);
}
}
function cantLoadInitialData(response) {
console.log("not good");
}
var p = Math.max(1, parseInt(pageNumber, 10) || 1);
$http.post("/docker/getContainerList", { page: p }, config).then(handleContainerListResponse, cantLoadInitialData);
};
});
@@ -1906,7 +1917,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
text: 'Redirecting...',
type: 'success'
});
window.location.href = '/docker/listContainers';
window.location.href = '/docker/containers';
}
else {
new PNotify({
@@ -2703,257 +2714,3 @@ app.controller('manageImages', function ($scope, $http) {
}
});
// Container List Controller
app.controller('listContainers', function ($scope, $http, $timeout, $window) {
$scope.containers = [];
$scope.loading = false;
$scope.updateContainerName = '';
$scope.currentImage = '';
$scope.newImage = '';
$scope.newTag = 'latest';
// Load containers list
$scope.loadContainers = function() {
$scope.loading = true;
var url = '/docker/listContainers';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$scope.loading = false;
if (response.data.status === 1) {
$scope.containers = response.data.containers || [];
} else {
new PNotify({
title: 'Error Loading Containers',
text: response.data.error_message || 'Failed to load containers',
type: 'error'
});
}
}, function(error) {
$scope.loading = false;
new PNotify({
title: 'Connection Error',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Initialize containers on page load
$scope.loadContainers();
// Open update container modal
$scope.openUpdateModal = function(container) {
$scope.updateContainerName = container.name;
$scope.currentImage = container.image;
$scope.newImage = '';
$scope.newTag = 'latest';
$('#updateContainer').modal('show');
};
// Perform container update
$scope.performUpdate = function() {
if (!$scope.newImage && !$scope.newTag) {
new PNotify({
title: 'Missing Information',
text: 'Please enter a new image name or tag',
type: 'error'
});
return;
}
// If no new image specified, use current image with new tag
var imageToUse = $scope.newImage || $scope.currentImage.split(':')[0];
var tagToUse = $scope.newTag || 'latest';
var data = {
containerName: $scope.updateContainerName,
newImage: imageToUse,
newTag: tagToUse
};
var url = '/docker/updateContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
// Show loading
$('#updateContainer').modal('hide');
new PNotify({
title: 'Updating Container',
text: 'Please wait while the container is being updated...',
type: 'info',
hide: false
});
$http.post(url, data, config).then(function(response) {
if (response.data.updateContainerStatus === 1) {
new PNotify({
title: 'Container Updated Successfully',
text: response.data.message || 'Container has been updated successfully',
type: 'success'
});
// Reload containers list
$scope.loadContainers();
} else {
new PNotify({
title: 'Update Failed',
text: response.data.error_message || 'Failed to update container',
type: 'error'
});
}
}, function(error) {
new PNotify({
title: 'Update Failed',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Container actions
$scope.startContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/startContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.startContainerStatus === 1) {
new PNotify({
title: 'Container Started',
text: 'Container has been started successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Start Failed',
text: response.data.error_message || 'Failed to start container',
type: 'error'
});
}
});
};
$scope.stopContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/stopContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.stopContainerStatus === 1) {
new PNotify({
title: 'Container Stopped',
text: 'Container has been stopped successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Stop Failed',
text: response.data.error_message || 'Failed to stop container',
type: 'error'
});
}
});
};
$scope.restartContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/restartContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.restartContainerStatus === 1) {
new PNotify({
title: 'Container Restarted',
text: 'Container has been restarted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Restart Failed',
text: response.data.error_message || 'Failed to restart container',
type: 'error'
});
}
});
};
$scope.deleteContainerWithData = function(containerName) {
if (confirm('Are you sure you want to delete this container and all its data? This action cannot be undone.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerWithData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerWithDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container and all data have been deleted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
$scope.deleteContainerKeepData = function(containerName) {
if (confirm('Are you sure you want to delete this container but keep the data? The container will be removed but volumes will be preserved.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerKeepData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerKeepDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container has been deleted but data has been preserved',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
});

View File

@@ -433,7 +433,7 @@
<i class="fas fa-search"></i>
{% trans "Search & Manage Images" %}
</a>
<a href="{% url 'listContainers' %}" class="btn-secondary">
<a href="{% url 'listContainersPage' %}" class="btn-secondary">
<i class="fas fa-cube"></i>
{% trans "View Containers" %}
</a>

View File

@@ -26,7 +26,7 @@
<div class="example-box-wrapper">
<div class="row">
<div class="col-md-4">
<a href="{% url 'listContainers' %}" title="{% trans 'Manage Containers' %}" class="tile-box tile-box-shortcut btn-primary">
<a href="{% url 'listContainersPage' %}" title="{% trans 'Manage Containers' %}" class="tile-box tile-box-shortcut btn-primary">
<div class="tile-header">
{% trans "Manage Containers" %}
</div>

View File

@@ -674,14 +674,37 @@
<strong>{% trans "Error:" %}</strong> {$ errorMessage $}
</div>
<div class="pagination" ng-if="pagination">
<ul>
{% for items in pagination %}
<li ng-click="getFurtherContainersFromDB({{ forloop.counter }})">
<a href="">{{ forloop.counter }}</a>
</li>
{% endfor %}
</ul>
<!-- Pagination Controls (ACTIVITY BOARD-style: Go to page) -->
<div ng-if="pagination && pagination.containers && (ContainerList.length > 0 || totalCount > 0)" class="pagination-wrapper" style="margin-top: 20px; padding: 15px; background: #f8f9ff; border-radius: 8px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px;">
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<button ng-click="prevPage('containers')" ng-disabled="!pagination.containers || (pagination.containers.currentPage || 1) === 1"
class="pagination-btn" style="padding: 6px 12px; border: 1px solid #e8e9ff; background: white; border-radius: 6px; cursor: pointer; font-size: 12px; color: #2f3640;">
<i class="fas fa-chevron-left"></i> {% trans "Prev" %}
</button>
<span style="font-size: 12px; color: #64748b; margin: 0 8px;">
{% trans "Page" %} {$ (pagination.containers.currentPage || 1) $} {% trans "of" %} {$ (getTotalPages('containers') || 1) $}
</span>
<button ng-click="nextPage('containers')" ng-disabled="!pagination.containers || !getTotalPages || (pagination.containers.currentPage || 1) >= (getTotalPages('containers') || 1)"
class="pagination-btn" style="padding: 6px 12px; border: 1px solid #e8e9ff; background: white; border-radius: 6px; cursor: pointer; font-size: 12px; color: #2f3640;">
{% trans "Next" %} <i class="fas fa-chevron-right"></i>
</button>
</div>
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<label style="font-size: 12px; color: #64748b; margin: 0;">{% trans "Go to page:" %}</label>
<input type="number" min="1" max="{$ (getTotalPages('containers') || 1) $}"
ng-model="gotoPageInput.containers"
ng-keyup="$event.keyCode === 13 && goToPage('containers', gotoPageInput.containers)"
style="width: 60px; padding: 6px; border: 1px solid #e8e9ff; border-radius: 6px; font-size: 12px; text-align: center;">
<button ng-click="goToPage('containers', gotoPageInput.containers)"
class="pagination-btn" style="padding: 6px 12px; border: 1px solid #5b5fcf; background: #5b5fcf; color: white; border-radius: 6px; cursor: pointer; font-size: 12px;">
{% trans "Go" %}
</button>
</div>
<div style="font-size: 12px; color: #64748b; margin: 0;" ng-if="totalCount > 0">
<span ng-if="pagination && pagination.containers">
{% trans "Showing" %} {$ ((pagination.containers.currentPage || 1) - 1) * (pagination.containers.itemsPerPage || 10) + 1 $}-{$ Math.min((pagination.containers.currentPage || 1) * (pagination.containers.itemsPerPage || 10), totalCount) $} {% trans "of" %} {$ totalCount $} {% trans "entries" %}
</span>
</div>
</div>
</div>
</div>
@@ -949,5 +972,5 @@
{% endblock %}
{% block footer_scripts %}
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
<script src="{% static 'dockerManager/dockerManager.js' %}?v=4"></script>
{% endblock %}

View File

@@ -9,6 +9,7 @@ urlpatterns = [
re_path(r'^getTags$', views.getTags, name='getTags'),
re_path(r'^runContainer', views.runContainer, name='runContainer'),
re_path(r'^submitContainerCreation$', views.submitContainerCreation, name='submitContainerCreation'),
re_path(r'^containers$', views.listContainersPage, name='listContainersPage'),
re_path(r'^listContainers$', views.listContainers, name='listContainers'),
re_path(r'^getContainerList$', views.getContainerList, name='getContainerList'),
re_path(r'^getContainerLogs$', views.getContainerLogs, name='getContainerLogs'),
@@ -33,7 +34,6 @@ urlpatterns = [
re_path(r'^updateContainerPorts$', views.updateContainerPorts, name='updateContainerPorts'),
re_path(r'^manageNetworks$', views.manageNetworks, name='manageNetworks'),
re_path(r'^updateContainer$', views.updateContainer, name='updateContainer'),
re_path(r'^listContainers$', views.listContainers, name='listContainers'),
re_path(r'^deleteContainerWithData$', views.deleteContainerWithData, name='deleteContainerWithData'),
re_path(r'^deleteContainerKeepData$', views.deleteContainerKeepData, name='deleteContainerKeepData'),
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),

View File

@@ -179,14 +179,40 @@ def runContainer(request):
return redirect(loadLoginPage)
@preDockerRun
def listContainers(request):
def listContainersPage(request):
"""
GET /docker/containers: Render HTML page only. Separate URL avoids
cache/proxy ever serving JSON (listContainers used to return JSON).
"""
try:
userID = request.session['userID']
cm = ContainerManager()
return cm.listContainers(request, userID)
resp = cm.listContainers(request, userID)
resp['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
resp['Pragma'] = 'no-cache'
resp['Expires'] = '0'
return resp
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def listContainers(request):
"""
GET: Redirect to /docker/containers (HTML). POST: 405.
listContainers URL historically returned JSON; caches may serve stale JSON.
Use /docker/containers for the page to avoid that.
"""
try:
request.session['userID'] # ensure logged in
except KeyError:
return redirect(loadLoginPage)
if request.method != 'GET':
return HttpResponse('Method Not Allowed', status=405)
from django.urls import reverse
return redirect(reverse('listContainersPage'))
@preDockerRun
def getContainerLogs(request):
try:
@@ -746,27 +772,6 @@ def getContainerEnv(request):
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def listContainers(request):
"""
Get list of all Docker containers
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.listContainers(userID)
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getDockerNetworks(request):
"""

View File

@@ -0,0 +1,496 @@
# -*- coding: utf-8 -*-
"""
PayPal Premium Plugin Views - Enhanced Security Version
This version uses remote server verification with multiple security layers
SECURITY: All PayPal verification happens on YOUR server, not user's server
"""
from django.shortcuts import render, redirect
from django.http import JsonResponse
from plogical.mailUtilities import mailUtilities
from plogical.httpProc import httpProc
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
from functools import wraps
import sys
import os
import urllib.request
import urllib.error
import json
import hashlib
import socket
import platform
import subprocess
import time
import uuid
# Remote verification server (YOUR server, not user's server)
REMOTE_VERIFICATION_URL = 'https://api.newstargeted.com/api/verify-paypal-payment'
PLUGIN_NAME = 'paypalPremiumPlugin' # PayPal Premium Plugin Example
PLUGIN_VERSION = '1.0.0'
# PayPal configuration
PAYPAL_ME_URL = 'https://paypal.me/KimBS?locale.x=en_US&country.x=NO'
PAYPAL_PAYMENT_LINK = '' # Can be set to a PayPal Payment Link URL
# Security configuration
CACHE_FILE = '/tmp/.paypalPremiumPlugin_license_cache'
CACHE_DURATION = 3600 # 1 hour
# File integrity hashes (generated after plugin finalization)
# To regenerate: python3 -c "import hashlib; print(hashlib.sha256(open('views.py', 'rb').read()).hexdigest())"
PLUGIN_FILE_HASHES = {
'views.py': '4899d70dde220b38d691a5cefdc4fd77b6d3e250ac1c7e12fa280d6f4ad31eb1', # Updated with security features
'urls.py': '92433d401c358cd33ffd1926881920fd1867bb6d7dad1c3c2ed1e7d3b0abc2c6',
}
def get_server_fingerprint():
"""
Generate unique server fingerprint
Ties license to specific server hardware/configuration
"""
fingerprint_data = []
try:
# Server hostname
fingerprint_data.append(socket.gethostname())
# Primary IP
fingerprint_data.append(socket.gethostbyname(socket.gethostname()))
# System information
fingerprint_data.append(platform.node())
fingerprint_data.append(platform.machine())
fingerprint_data.append(platform.processor())
# MAC address
fingerprint_data.append(str(uuid.getnode()))
# Disk information (if available)
try:
result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True, timeout=2)
fingerprint_data.append(result.stdout[:100])
except:
pass
# Create hash
fingerprint_string = '|'.join(str(x) for x in fingerprint_data)
return hashlib.sha256(fingerprint_string.encode()).hexdigest()
except Exception as e:
# Fallback fingerprint
return hashlib.sha256(f"{socket.gethostname()}|{platform.node()}".encode()).hexdigest()
def verify_code_integrity():
"""
Verify plugin files haven't been tampered with
Returns: (is_valid, error_message)
"""
plugin_dir = os.path.dirname(os.path.abspath(__file__))
for filename, expected_hash in PLUGIN_FILE_HASHES.items():
if not expected_hash:
continue # Skip if hash not set
filepath = os.path.join(plugin_dir, filename)
if os.path.exists(filepath):
try:
with open(filepath, 'rb') as f:
file_content = f.read()
file_hash = hashlib.sha256(file_content).hexdigest()
if file_hash != expected_hash:
return False, f"File {filename} has been modified (integrity check failed)"
except Exception as e:
return False, f"Error checking {filename}: {str(e)}"
return True, None
def get_cached_verification():
"""Get cached verification result"""
if os.path.exists(CACHE_FILE):
try:
with open(CACHE_FILE, 'r') as f:
cache_data = json.load(f)
cache_time = cache_data.get('timestamp', 0)
server_fp = cache_data.get('server_fingerprint')
# Verify server fingerprint matches
current_fp = get_server_fingerprint()
if server_fp != current_fp:
return None # Server changed, invalidate cache
# Check if cache is still valid
if time.time() - cache_time < CACHE_DURATION:
return cache_data.get('has_access', False)
except:
pass
return None
def cache_verification_result(has_access, server_fp):
"""Cache verification result"""
try:
with open(CACHE_FILE, 'w') as f:
json.dump({
'has_access': has_access,
'server_fingerprint': server_fp,
'timestamp': time.time()
}, f)
os.chmod(CACHE_FILE, 0o600) # Secure permissions (owner read/write only)
except Exception as e:
pass # Silently fail caching
def cyberpanel_login_required(view_func):
"""
Custom decorator that checks for CyberPanel session userID
"""
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
try:
userID = request.session['userID']
# User is authenticated via CyberPanel session
return view_func(request, *args, **kwargs)
except KeyError:
# Not logged in, redirect to login
from loginSystem.views import loadLoginPage
return redirect(loadLoginPage)
return _wrapped_view
def secure_verification_required(view_func):
"""
Enhanced decorator with multiple security checks
"""
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
# Check 1: Login required
try:
userID = request.session['userID']
except KeyError:
from loginSystem.views import loadLoginPage
return redirect(loadLoginPage)
# Check 2: Code integrity
is_valid, integrity_error = verify_code_integrity()
if not is_valid:
# Log security violation
logging.writeToFile(f"SECURITY VIOLATION: {integrity_error} - User: {request.session.get('userID')}")
# Show error (don't reveal details)
context = {
'error': 'Plugin integrity check failed. Please reinstall the plugin.',
'security_violation': True
}
proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin')
return proc.render()
# Check 3: Remote verification
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
if not user_email:
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
domain = request.get_host()
user_ip = request.META.get('REMOTE_ADDR', '')
verification_result = check_remote_payment_secure(
user_email,
user_ip,
domain
)
if not verification_result.get('has_access', False):
# Show payment required page
context = {
'plugin_name': 'PayPal Premium Plugin Example',
'is_paid': True,
'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL),
'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
'message': verification_result.get('message', 'PayPal payment required'),
'error': verification_result.get('error')
}
proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin')
return proc.render()
# All checks passed - proceed
return view_func(request, *args, **kwargs)
return _wrapped_view
def remote_verification_required(view_func):
"""
Decorator that checks PayPal payment via remote server
No secrets stored in plugin - all verification happens on your server
"""
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
# First check login
try:
userID = request.session['userID']
except KeyError:
from loginSystem.views import loadLoginPage
return redirect(loadLoginPage)
# Get user email
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
if not user_email:
# Try to get from session or username
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
# Check payment via remote server
verification_result = check_remote_payment_secure(
user_email,
request.META.get('REMOTE_ADDR', ''),
request.get_host()
)
if not verification_result.get('has_access', False):
# User doesn't have payment - show payment required page
context = {
'plugin_name': 'PayPal Premium Plugin Example',
'is_paid': True,
'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL),
'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
'message': verification_result.get('message', 'PayPal payment required'),
'error': verification_result.get('error')
}
proc = httpProc(request, 'paypalPremiumPlugin/subscription_required.html', context, 'admin')
return proc.render()
# User has access - proceed with view
return view_func(request, *args, **kwargs)
return _wrapped_view
def check_remote_payment_secure(user_email, user_ip='', domain=''):
"""
Enhanced remote payment verification with multiple security layers
Args:
user_email: User's email address
user_ip: User's IP address (for logging/security)
domain: Current domain (for domain binding)
Returns:
dict: {
'has_access': bool,
'paypal_me_url': str,
'paypal_payment_link': str,
'message': str,
'error': str or None
}
"""
# Layer 1: Code integrity check
is_valid, integrity_error = verify_code_integrity()
if not is_valid:
return {
'has_access': False,
'paypal_me_url': PAYPAL_ME_URL,
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
'message': 'Plugin integrity check failed',
'error': integrity_error,
'security_violation': True
}
# Layer 2: Check cache
cached_result = get_cached_verification()
if cached_result is not None:
return {
'has_access': cached_result,
'paypal_me_url': PAYPAL_ME_URL,
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
'message': 'Access granted' if cached_result else 'PayPal payment required'
}
# Layer 3: Server fingerprinting
server_fp = get_server_fingerprint()
# Layer 4: Prepare secure request
request_data = {
'user_email': user_email,
'plugin_name': PLUGIN_NAME,
'plugin_version': PLUGIN_VERSION,
'server_fingerprint': server_fp,
'domain': domain,
'user_ip': user_ip,
'timestamp': int(time.time())
}
try:
# Make request to remote verification server
req = urllib.request.Request(
REMOTE_VERIFICATION_URL,
data=json.dumps(request_data).encode('utf-8'),
headers={
'Content-Type': 'application/json',
'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}',
'X-Plugin-Name': PLUGIN_NAME,
'X-Timestamp': str(request_data['timestamp'])
}
)
# Send request with timeout
try:
with urllib.request.urlopen(req, timeout=10) as response:
response_data = json.loads(response.read().decode('utf-8'))
if response_data.get('success', False):
has_access = response_data.get('has_access', False)
# Cache result
cache_verification_result(has_access, server_fp)
return {
'has_access': has_access,
'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL),
'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
'message': response_data.get('message', 'Access granted' if has_access else 'PayPal payment required'),
'error': None
}
else:
return {
'has_access': False,
'paypal_me_url': response_data.get('paypal_me_url', PAYPAL_ME_URL),
'paypal_payment_link': response_data.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
'message': response_data.get('message', 'PayPal payment required'),
'error': response_data.get('error')
}
except urllib.error.HTTPError as e:
# Server returned error
error_body = e.read().decode('utf-8') if e.fp else 'Unknown error'
return {
'has_access': False,
'paypal_me_url': PAYPAL_ME_URL,
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
'message': 'Unable to verify payment. Please try again later.',
'error': f'HTTP {e.code}: {error_body}'
}
except urllib.error.URLError as e:
# Network error
return {
'has_access': False,
'paypal_me_url': PAYPAL_ME_URL,
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
'message': 'Unable to connect to verification server. Please check your internet connection.',
'error': str(e.reason) if hasattr(e, 'reason') else str(e)
}
except Exception as e:
# Other errors
return {
'has_access': False,
'paypal_me_url': PAYPAL_ME_URL,
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
'message': 'Verification error occurred. Please try again later.',
'error': str(e)
}
except Exception as e:
logging.writeToFile(f"Error in remote payment check: {str(e)}")
return {
'has_access': False,
'paypal_me_url': PAYPAL_ME_URL,
'paypal_payment_link': PAYPAL_PAYMENT_LINK,
'message': 'Verification error occurred. Please try again later.',
'error': str(e)
}
def check_remote_payment(user_email, user_ip=''):
"""
Legacy function for backward compatibility
"""
return check_remote_payment_secure(user_email, user_ip, '')
@cyberpanel_login_required
def main_view(request):
"""
Main view for PayPal premium plugin
Shows plugin information and features if paid, or payment required message if not
"""
mailUtilities.checkHome()
# Get user email for verification
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
if not user_email:
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
# Check payment status (but don't block access)
verification_result = check_remote_payment_secure(
user_email,
request.META.get('REMOTE_ADDR', ''),
request.get_host()
)
has_access = verification_result.get('has_access', False)
# Determine plugin status
plugin_status = 'Active' if has_access else 'Payment Required'
context = {
'plugin_name': 'PayPal Premium Plugin Example',
'version': PLUGIN_VERSION,
'status': plugin_status,
'has_access': has_access,
'description': 'This is an example paid plugin that requires PayPal payment.' if not has_access else 'This is an example paid plugin. You have access because payment has been verified!',
'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL),
'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
'features': [
'Premium Feature 1',
'Premium Feature 2',
'Premium Feature 3',
'Advanced Configuration',
'Priority Support'
] if has_access else []
}
proc = httpProc(request, 'paypalPremiumPlugin/index.html', context, 'admin')
return proc.render()
@cyberpanel_login_required
def settings_view(request):
"""
Settings page for PayPal premium plugin
Shows settings but disables them if user doesn't have PayPal payment
"""
mailUtilities.checkHome()
# Get user email for verification
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
if not user_email:
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
# Check payment status (but don't block access)
verification_result = check_remote_payment_secure(
user_email,
request.META.get('REMOTE_ADDR', ''),
request.get_host()
)
has_access = verification_result.get('has_access', False)
# Determine plugin status
plugin_status = 'Active' if has_access else 'Payment Required'
context = {
'plugin_name': 'PayPal Premium Plugin Example',
'version': PLUGIN_VERSION,
'plugin_status': plugin_status,
'status': plugin_status, # Keep both for compatibility
'description': 'Configure your premium plugin settings',
'has_access': has_access,
'paypal_me_url': verification_result.get('paypal_me_url', PAYPAL_ME_URL),
'paypal_payment_link': verification_result.get('paypal_payment_link', PAYPAL_PAYMENT_LINK),
'verification_message': verification_result.get('message', '')
}
proc = httpProc(request, 'paypalPremiumPlugin/settings.html', context, 'admin')
return proc.render()
@cyberpanel_login_required
@secure_verification_required
def api_status_view(request):
"""
API endpoint for plugin status
Only accessible with PayPal payment (verified remotely with enhanced security)
"""
return JsonResponse({
'plugin_name': 'PayPal Premium Plugin Example',
'version': PLUGIN_VERSION,
'status': 'active',
'payment': 'verified',
'description': 'Premium plugin is active and accessible',
'verification_method': 'remote_secure'
})

Binary file not shown.

BIN
pluginHolder/fail2ban.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,245 @@
# -*- coding: utf-8 -*-
"""
Patreon Verifier for CyberPanel Plugins
Verifies Patreon membership status for paid plugins
"""
import urllib.request
import urllib.error
import json
import os
import sys
# Patreon API configuration
PATREON_API_BASE = 'https://www.patreon.com/api/oauth2/v2'
PATREON_MEMBERSHIP_TIER = 'CyberPanel Paid Plugin' # The membership tier name to check
class PatreonVerifier:
"""
Verifies Patreon membership status for CyberPanel users
"""
def __init__(self):
"""Initialize Patreon verifier"""
# Try to import from Django settings first, then fallback to environment
try:
from django.conf import settings
self.client_id = getattr(settings, 'PATREON_CLIENT_ID', os.environ.get('PATREON_CLIENT_ID', ''))
self.client_secret = getattr(settings, 'PATREON_CLIENT_SECRET', os.environ.get('PATREON_CLIENT_SECRET', ''))
self.creator_id = getattr(settings, 'PATREON_CREATOR_ID', os.environ.get('PATREON_CREATOR_ID', ''))
self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984'))
self.creator_access_token = getattr(settings, 'PATREON_CREATOR_ACCESS_TOKEN', os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', ''))
except:
# Fallback to environment variables only
self.client_id = os.environ.get('PATREON_CLIENT_ID', '')
self.client_secret = os.environ.get('PATREON_CLIENT_SECRET', '')
self.creator_id = os.environ.get('PATREON_CREATOR_ID', '')
self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984')
self.creator_access_token = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')
# Cache for membership checks (to avoid excessive API calls)
self.cache_file = '/home/cyberpanel/patreon_cache.json'
self.cache_duration = 300 # Cache for 5 minutes
def get_user_patreon_token(self, user_email):
"""
Get stored Patreon access token for a user
This should be stored when user authorizes via Patreon OAuth
"""
# In a real implementation, you'd store this in a database
# For now, we'll check if there's a stored token file
token_file = f'/home/cyberpanel/patreon_tokens/{user_email}.token'
if os.path.exists(token_file):
try:
with open(token_file, 'r') as f:
token_data = json.load(f)
return token_data.get('access_token')
except:
return None
return None
def check_membership_cached(self, user_email):
"""
Check membership with caching
"""
import time
cache_data = self._load_cache()
cache_key = f"membership_{user_email}"
if cache_key in cache_data:
cached_result = cache_data[cache_key]
if time.time() - cached_result.get('timestamp', 0) < self.cache_duration:
return cached_result.get('has_membership', False)
# Check membership via API
has_membership = self.check_membership(user_email)
# Update cache
cache_data[cache_key] = {
'has_membership': has_membership,
'timestamp': time.time()
}
self._save_cache(cache_data)
return has_membership
def check_membership(self, user_email):
"""
Check if user has active Patreon membership for 'CyberPanel Paid Plugin'
Args:
user_email: User's email address
Returns:
bool: True if user has active membership, False otherwise
"""
access_token = self.get_user_patreon_token(user_email)
if not access_token:
return False
try:
# Get user's identity
user_info = self._get_user_identity(access_token)
if not user_info:
return False
member_id = user_info.get('id')
if not member_id:
return False
# Get user's memberships
memberships = self._get_memberships(access_token, member_id)
if not memberships:
return False
# Check if user has the required membership tier
# First try to match by tier ID (more accurate)
for membership in memberships:
tier_id = membership.get('id', '')
tier_name = membership.get('attributes', {}).get('title', '')
# Check by tier ID first (most accurate)
if tier_id == self.membership_tier_id:
# Check if membership is active
status = membership.get('attributes', {}).get('patron_status', '')
if status in ['active_patron', 'former_patron']:
# Check if currently entitled
entitled = membership.get('attributes', {}).get('currently_entitled_amount_cents', 0)
if entitled > 0:
return True
# Fallback: Check by tier name (for compatibility)
if PATREON_MEMBERSHIP_TIER.lower() in tier_name.lower():
# Check if membership is active
status = membership.get('attributes', {}).get('patron_status', '')
if status in ['active_patron', 'former_patron']:
# Check if currently entitled
entitled = membership.get('attributes', {}).get('currently_entitled_amount_cents', 0)
if entitled > 0:
return True
return False
except Exception as e:
import logging
logging.writeToFile(f"Error checking Patreon membership for {user_email}: {str(e)}")
return False
def _get_user_identity(self, access_token):
"""
Get user identity from Patreon API
"""
url = f"{PATREON_API_BASE}/identity"
try:
req = urllib.request.Request(url)
req.add_header('Authorization', f'Bearer {access_token}')
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
return data.get('data', {})
except Exception as e:
import logging
logging.writeToFile(f"Error getting Patreon identity: {str(e)}")
return None
def _get_memberships(self, access_token, member_id):
"""
Get user's memberships from Patreon API
"""
url = f"{PATREON_API_BASE}/members/{member_id}?include=currently_entitled_tiers"
try:
req = urllib.request.Request(url)
req.add_header('Authorization', f'Bearer {access_token}')
with urllib.request.urlopen(req) as response:
data = json.loads(response.read().decode())
# Parse included tiers
memberships = []
included = data.get('included', [])
for item in included:
if item.get('type') == 'tier':
# Include tier ID in the membership data
tier_data = {
'id': item.get('id', ''),
'type': item.get('type', ''),
'attributes': item.get('attributes', {})
}
memberships.append(tier_data)
return memberships
except Exception as e:
import logging
logging.writeToFile(f"Error getting Patreon memberships: {str(e)}")
return []
def _load_cache(self):
"""Load cache from file"""
if os.path.exists(self.cache_file):
try:
with open(self.cache_file, 'r') as f:
return json.load(f)
except:
return {}
return {}
def _save_cache(self, cache_data):
"""Save cache to file"""
try:
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
with open(self.cache_file, 'w') as f:
json.dump(cache_data, f)
except Exception as e:
import logging
logging.writeToFile(f"Error saving Patreon cache: {str(e)}")
def verify_plugin_access(self, user_email, plugin_name):
"""
Verify if user can access a paid plugin
Args:
user_email: User's email address
plugin_name: Name of the plugin to check
Returns:
dict: {
'has_access': bool,
'is_paid': bool,
'message': str
}
"""
# Check if plugin is paid (this will be checked from meta.xml)
# For now, we'll assume the plugin system will pass this info
# Check membership
has_membership = self.check_membership_cached(user_email)
return {
'has_access': has_membership,
'is_paid': True, # This will be determined by plugin metadata
'message': 'Access granted' if has_membership else 'Patreon subscription required'
}

View File

@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
"""
Plugin Access Control
Checks if user has access to paid plugins
"""
from .patreon_verifier import PatreonVerifier
import logging
def check_plugin_access(request, plugin_name, plugin_meta=None):
"""
Check if user has access to a plugin
Args:
request: Django request object
plugin_name: Name of the plugin
plugin_meta: Plugin metadata dict (optional, will be loaded if not provided)
Returns:
dict: {
'has_access': bool,
'is_paid': bool,
'message': str,
'patreon_url': str or None
}
"""
# Default response for free plugins
default_response = {
'has_access': True,
'is_paid': False,
'message': 'Access granted',
'patreon_url': None
}
# If plugin_meta not provided, try to load it
if plugin_meta is None:
plugin_meta = _load_plugin_meta(plugin_name)
# Check if plugin is paid
if not plugin_meta or not plugin_meta.get('is_paid', False):
return default_response
# Plugin is paid - check Patreon membership
if not request.user or not request.user.is_authenticated:
return {
'has_access': False,
'is_paid': True,
'message': 'Please log in to access this plugin',
'patreon_url': plugin_meta.get('patreon_url')
}
# Get user email
user_email = getattr(request.user, 'email', None)
if not user_email:
# Try to get from username or other fields
user_email = getattr(request.user, 'username', '')
if not user_email:
return {
'has_access': False,
'is_paid': True,
'message': 'Unable to verify user identity',
'patreon_url': plugin_meta.get('patreon_url')
}
# Check Patreon membership
verifier = PatreonVerifier()
has_membership = verifier.check_membership_cached(user_email)
if has_membership:
return {
'has_access': True,
'is_paid': True,
'message': 'Access granted',
'patreon_url': None
}
else:
return {
'has_access': False,
'is_paid': True,
'message': f'This plugin requires a Patreon subscription to "{plugin_meta.get("patreon_tier", "CyberPanel Paid Plugin")}"',
'patreon_url': plugin_meta.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership')
}
def _load_plugin_meta(plugin_name):
"""
Load plugin metadata from meta.xml
Args:
plugin_name: Name of the plugin
Returns:
dict: Plugin metadata or None
"""
import os
from xml.etree import ElementTree
installed_path = f'/usr/local/CyberCP/{plugin_name}/meta.xml'
source_path = f'/home/cyberpanel/plugins/{plugin_name}/meta.xml'
meta_path = None
if os.path.exists(installed_path):
meta_path = installed_path
elif os.path.exists(source_path):
meta_path = source_path
if not meta_path:
return None
try:
tree = ElementTree.parse(meta_path)
root = tree.getroot()
# Extract paid plugin information
paid_elem = root.find('paid')
patreon_tier_elem = root.find('patreon_tier')
patreon_url_elem = root.find('patreon_url')
is_paid = False
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
is_paid = True
return {
'is_paid': is_paid,
'patreon_tier': patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin',
'patreon_url': patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership'
}
except Exception as e:
logging.writeToFile(f"Error loading plugin meta for {plugin_name}: {str(e)}")
return None

View File

@@ -0,0 +1,351 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Plugin Help - " %}{{ plugin_name }} - CyberPanel{% endblock %}
{% block header_scripts %}
<style>
/* Plugin Help Page Styles */
.plugin-help-wrapper {
background: transparent;
padding: 20px;
}
.plugin-help-container {
max-width: 900px;
margin: 0 auto;
}
/* Page Header */
.help-page-header {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 30px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
border-left: 4px solid #5856d6;
}
.help-page-header h1 {
font-size: 32px;
font-weight: 700;
color: var(--text-primary, #2f3640);
margin: 0 0 15px 0;
display: flex;
align-items: center;
gap: 15px;
}
.help-page-header .help-icon {
width: 48px;
height: 48px;
background: #5856d6;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
box-shadow: 0 4px 12px rgba(88,86,214,0.3);
}
.help-page-header .plugin-title {
font-size: 24px;
font-weight: 600;
color: var(--text-primary, #2f3640);
margin-bottom: 10px;
}
.help-page-header .plugin-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--border-primary, #e8e9ff);
font-size: 14px;
color: var(--text-secondary, #64748b);
}
.help-page-header .plugin-meta-item {
display: flex;
align-items: center;
gap: 8px;
}
.help-page-header .plugin-meta-item strong {
color: var(--text-primary, #2f3640);
}
/* Content Section */
.help-content-section {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 35px;
margin-bottom: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
}
.help-content {
line-height: 1.8;
color: var(--text-primary, #2f3640);
font-size: 15px;
}
/* Help content styling for HTML */
.help-content h1,
.help-content h2,
.help-content h3,
.help-content h4,
.help-content h5,
.help-content h6 {
color: var(--text-primary, #2f3640);
margin-top: 30px;
margin-bottom: 15px;
font-weight: 700;
}
.help-content h1 {
font-size: 28px;
border-bottom: 2px solid #5856d6;
padding-bottom: 10px;
}
.help-content h2 {
font-size: 24px;
color: #5856d6;
}
.help-content h3 {
font-size: 20px;
}
.help-content p {
margin-bottom: 15px;
}
.help-content ul,
.help-content ol {
margin-left: 25px;
margin-bottom: 15px;
}
.help-content li {
margin-bottom: 8px;
}
.help-content code {
background: var(--bg-secondary, #f8f9ff);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #5856d6;
}
.help-content pre {
background: var(--bg-secondary, #f8f9ff);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
border-left: 4px solid #5856d6;
margin-bottom: 15px;
}
.help-content pre code {
background: none;
padding: 0;
color: var(--text-primary, #2f3640);
}
.help-content blockquote {
border-left: 4px solid #5856d6;
padding-left: 20px;
margin-left: 0;
margin-bottom: 15px;
color: var(--text-secondary, #64748b);
font-style: italic;
}
.help-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
.help-content table th,
.help-content table td {
padding: 12px;
border: 1px solid var(--border-primary, #e8e9ff);
text-align: left;
}
.help-content table th {
background: var(--bg-secondary, #f8f9ff);
font-weight: 600;
color: var(--text-primary, #2f3640);
}
.help-content a {
color: #5856d6;
text-decoration: none;
border-bottom: 1px dotted #5856d6;
}
.help-content a:hover {
color: #4a48c4;
border-bottom: 1px solid #4a48c4;
}
.help-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 15px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* Actions Section */
.help-actions {
background: var(--bg-primary, white);
border-radius: 12px;
padding: 25px;
box-shadow: var(--shadow-md, 0 2px 8px rgba(0,0,0,0.08));
border: 1px solid var(--border-primary, #e8e9ff);
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.help-action-btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.help-action-btn-primary {
background: #5856d6;
color: white;
}
.help-action-btn-primary:hover {
background: #4a48c4;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(88,86,214,0.3);
}
.help-action-btn-secondary {
background: var(--bg-secondary, #f8f9ff);
color: var(--text-secondary, #64748b);
border: 1px solid var(--border-primary, #e8e9ff);
}
.help-action-btn-secondary:hover {
background: var(--bg-hover, #f0f1ff);
color: #5856d6;
border-color: #5856d6;
}
/* Responsive */
@media (max-width: 768px) {
.plugin-help-wrapper {
padding: 15px;
}
.help-page-header {
padding: 20px;
}
.help-page-header h1 {
font-size: 24px;
flex-direction: column;
text-align: center;
}
.help-content-section {
padding: 20px;
}
.help-actions {
flex-direction: column;
}
.help-action-btn {
width: 100%;
justify-content: center;
}
}
</style>
{% endblock %}
{% block content %}
{% load static %}
<div class="plugin-help-wrapper">
<div class="plugin-help-container">
<!-- Page Header -->
<div class="help-page-header">
<h1>
<div class="help-icon">
<i class="fas fa-question-circle"></i>
</div>
{% trans "Module Help" %}
</h1>
<div class="plugin-title">{{ plugin_name }}</div>
{% if plugin_description %}
<p style="color: var(--text-secondary, #64748b); margin: 10px 0 0 0;">
{{ plugin_description }}
</p>
{% endif %}
<div class="plugin-meta">
<div class="plugin-meta-item">
<i class="fas fa-info-circle"></i>
<strong>{% trans "Version:" %}</strong> {{ plugin_version }}
</div>
<div class="plugin-meta-item">
<i class="fas fa-user"></i>
<strong>{% trans "Author:" %}</strong> {{ plugin_author }}
</div>
{% if installed %}
<div class="plugin-meta-item">
<i class="fas fa-check-circle"></i>
<strong>{% trans "Status:" %}</strong> <span style="color: #28a745;">{% trans "Installed" %}</span>
</div>
{% endif %}
</div>
</div>
<!-- Help Content -->
<div class="help-content-section">
<div class="help-content">
{{ help_content|safe }}
</div>
</div>
<!-- Actions -->
<div class="help-actions">
{% if installed %}
<a href="/plugins/{{ plugin_name_dir }}/" class="help-action-btn help-action-btn-primary">
<i class="fas fa-cog"></i>
{% trans "Plugin Settings" %}
</a>
{% endif %}
<a href="/plugins/installed" class="help-action-btn help-action-btn-secondary">
<i class="fas fa-arrow-left"></i>
{% trans "Back to Plugins" %}
</a>
<a href="https://github.com/master3395/cyberpanel-plugins/tree/main/{{ plugin_name_dir }}" target="_blank" class="help-action-btn help-action-btn-secondary">
<i class="fab fa-github"></i>
{% trans "View on GitHub" %}
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,93 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Plugin Not Found - CyberPanel" %}{% endblock %}
{% block header_scripts %}
<style>
.error-wrapper {
background: transparent;
padding: 20px;
}
.error-container {
max-width: 600px;
margin: 0 auto;
text-align: center;
padding: 60px 20px;
}
.error-icon {
font-size: 72px;
color: #dc3545;
margin-bottom: 20px;
}
.error-title {
font-size: 28px;
font-weight: 700;
color: var(--text-primary, #2f3640);
margin-bottom: 15px;
}
.error-message {
font-size: 16px;
color: var(--text-secondary, #64748b);
margin-bottom: 30px;
}
.error-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.error-btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.error-btn-primary {
background: #5856d6;
color: white;
}
.error-btn-primary:hover {
background: #4a48c4;
}
</style>
{% endblock %}
{% block content %}
<div class="error-wrapper">
<div class="error-container">
<div class="error-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h1 class="error-title">{% trans "Plugin Not Found" %}</h1>
<p class="error-message">
{% if plugin_name %}
{% trans "The plugin" %} "<strong>{{ plugin_name }}</strong>" {% trans "could not be found." %}
{% else %}
{% trans "The requested plugin could not be found." %}
{% endif %}
{% if error %}
<br><small style="color: #dc3545;">{{ error }}</small>
{% endif %}
</p>
<div class="error-actions">
<a href="/plugins/installed" class="error-btn error-btn-primary">
<i class="fas fa-arrow-left"></i>
{% trans "Back to Plugins" %}
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -182,6 +182,103 @@
color: var(--text-secondary, #64748b);
}
.plugin-pricing-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 6px;
vertical-align: middle;
}
.plugin-pricing-badge.free {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.plugin-pricing-badge.paid {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.paid-badge {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 10px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 8px;
vertical-align: middle;
}
.paid-badge i {
margin-right: 4px;
}
.subscription-warning {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
padding: 12px;
margin-top: 10px;
font-size: 12px;
color: #856404;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.subscription-warning-content {
display: flex;
align-items: center;
flex: 1;
}
.subscription-warning i {
margin-right: 6px;
color: #ffc107;
}
.subscription-warning-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: linear-gradient(135deg, #f96854 0%, #f96854 100%);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
transition: all 0.3s ease;
white-space: nowrap;
box-shadow: 0 2px 4px rgba(249, 104, 84, 0.3);
}
.subscription-warning-button:hover {
background: linear-gradient(135deg, #e55a47 0%, #e55a47 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(249, 104, 84, 0.4);
color: white;
text-decoration: none;
}
.subscription-warning-button i {
margin-right: 0;
color: white;
}
.plugin-description {
font-size: 13px;
color: var(--text-secondary, #64748b);
@@ -803,30 +900,49 @@
<div class="plugins-container">
<!-- Page Header -->
<div class="page-header">
<h1>
<div class="icon">
<i class="fas fa-plug"></i>
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
<div>
<h1>
<div class="icon">
<i class="fas fa-plug"></i>
</div>
{% trans "Installed Plugins" %}
</h1>
<p>{% trans "List of installed plugins on your CyberPanel" %}</p>
</div>
{% trans "Installed Plugins" %}
</h1>
<p>{% trans "List of installed plugins on your CyberPanel" %}</p>
<div style="display: flex; gap: 20px; align-items: center; margin-top: 10px;">
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-check-circle" style="color: #28a745; font-size: 18px;"></i>
<span style="font-weight: 600; color: var(--text-primary, #2f3640);">
{% trans "Installed:" %} {{ installed_count|default:0 }}
</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<i class="fas fa-power-off" style="color: #007bff; font-size: 18px;"></i>
<span style="font-weight: 600; color: var(--text-primary, #2f3640);">
{% trans "Active:" %} {{ active_count|default:0 }}
</span>
</div>
</div>
</div>
</div>
<!-- Plugins Section -->
<div class="content-section">
<h2 class="section-title">{% trans "Plugins" %}</h2>
{% if plugins %}
<!-- View Toggle -->
<div id="plugins-view-toggle" class="view-toggle" style="{% if not plugins %}margin-top: 0;{% endif %}">
<button class="view-btn {% if plugins %}active{% else %}{% endif %}" onclick="toggleView('grid')">
<div class="view-toggle">
<button class="view-btn active" onclick="toggleView('grid')">
<i class="fas fa-th-large"></i>
{% trans "Grid View" %}
</button>
<button class="view-btn {% if not plugins %}active{% endif %}" onclick="toggleView('table')">
<button class="view-btn" onclick="toggleView('table')">
<i class="fas fa-list"></i>
{% trans "Table View" %}
</button>
<button class="view-btn {% if not plugins %}active{% endif %}" onclick="toggleView('store')">
<button class="view-btn" onclick="toggleView('store')">
<i class="fas fa-store"></i>
{% trans "CyberPanel Plugin Store" %}
</button>
@@ -836,7 +952,6 @@
</a>
</div>
{% if plugins %}
<!-- Grid View -->
<div id="gridView" class="plugins-grid">
{% for plugin in plugins %}
@@ -856,22 +971,35 @@
{% endif %}
</div>
<div class="plugin-info">
<h3 class="plugin-name">{{ plugin.name }}</h3>
<h3 class="plugin-name">{{ plugin.name }}{% if plugin.is_paid|default:False|default_if_none:False %} <span class="paid-badge" title="{% trans 'Paid Plugin - Patreon Subscription Required' %}"><i class="fas fa-crown"></i> {% trans "Premium" %}</span>{% endif %}</h3>
<div class="plugin-meta">
<span class="plugin-type">{{ plugin.type }}</span>
<span class="plugin-version-number">v{{ plugin.version }}</span>
{% if plugin.is_paid|default:False|default_if_none:False %}
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
{% else %}
<span class="plugin-pricing-badge free">{% trans "Free" %}</span>
{% endif %}
</div>
{% if plugin.author %}
<div class="plugin-author" style="margin-top: 8px; font-size: 13px; color: var(--text-secondary, #64748b);">
<i class="fas fa-user" style="margin-right: 5px;"></i>
<span>{% trans "Author:" %} {{ plugin.author }}</span>
</div>
{% endif %}
</div>
</div>
<div class="plugin-description">
{{ plugin.desc }}
{% if plugin.is_paid|default:False|default_if_none:False %}
<div class="subscription-warning">
<div class="subscription-warning-content">
<i class="fas fa-exclamation-triangle"></i>
<strong>{% trans "Paid Plugin:" %}</strong> {% trans "Requires Patreon subscription to" %} "{{ plugin.patreon_tier|default:'CyberPanel Paid Plugin' }}"
</div>
{% if plugin.patreon_url %}
<a href="{{ plugin.patreon_url }}" target="_blank" rel="noopener noreferrer" class="subscription-warning-button">
<i class="fab fa-patreon"></i>
{% trans "Subscribe on Patreon" %}
</a>
{% endif %}
</div>
{% endif %}
</div>
<div class="plugin-status-section">
@@ -943,7 +1071,6 @@
<thead>
<tr>
<th>{% trans "Plugin Name" %}</th>
<th>{% trans "Author" %}</th>
<th>{% trans "Version" %}</th>
<th>{% trans "Modify Date" %}</th>
<th>{% trans "Status" %}</th>
@@ -959,13 +1086,13 @@
<td>
<strong>{{ plugin.name }}</strong>
</td>
<td>
<span style="color: var(--text-secondary, #64748b); font-size: 13px;">
{{ plugin.author|default:"Unknown" }}
</span>
</td>
<td>
<span class="plugin-version-number">{{ plugin.version }}</span>
{% if plugin.is_paid|default:False|default_if_none:False %}
<span class="plugin-pricing-badge paid">{% trans "Paid" %}</span>
{% else %}
<span class="plugin-pricing-badge free">{% trans "Free" %}</span>
{% endif %}
</td>
<td>
<small style="color: var(--text-secondary, #64748b);">
@@ -1043,6 +1170,26 @@
<h3 class="empty-title">{% trans "No Plugins Installed" %}</h3>
<p class="empty-description">{% trans "You haven't installed any plugins yet. Plugins extend CyberPanel's functionality with additional features." %}</p>
</div>
<!-- View Toggle (only shown when no plugins installed) -->
<div class="view-toggle" style="margin-top: 25px;">
<button class="view-btn" onclick="toggleView('grid')">
<i class="fas fa-th-large"></i>
{% trans "Grid View" %}
</button>
<button class="view-btn" onclick="toggleView('table')">
<i class="fas fa-list"></i>
{% trans "Table View" %}
</button>
<button class="view-btn active" onclick="toggleView('store')">
<i class="fas fa-store"></i>
{% trans "CyberPanel Plugin Store" %}
</button>
<a href="/plugins/help/" class="view-btn" style="text-decoration: none;">
<i class="fas fa-book"></i>
{% trans "Plugin Development Guide" %}
</a>
</div>
{% endif %}
<!-- CyberPanel Plugin Store (always available) -->
@@ -1097,13 +1244,12 @@
<table class="store-table">
<thead>
<tr>
<th>{% trans "Icon" %}</th>
<th>{% trans "Plugin Name" %}</th>
<th>{% trans "Author" %}</th>
<th>{% trans "Version" %}</th>
<th>{% trans "Pricing" %}</th>
<th>{% trans "Modify Date" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Action" %}</th>
<th>{% trans "Active" %}</th>
<th>{% trans "Help" %}</th>
<th>{% trans "About" %}</th>
</tr>
@@ -1125,6 +1271,8 @@
</div>
<script>
// Cache-busting version: 2026-01-25-v4 - Fixed is_paid normalization and ensured consistent rendering
// Force browser to reload this script by changing version number
let storePlugins = [];
let currentFilter = 'all';
@@ -1148,9 +1296,7 @@ function toggleView(view) {
const gridView = document.getElementById('gridView');
const tableView = document.getElementById('tableView');
const storeView = document.getElementById('storeView');
// Only select view buttons from the main view-toggle (prevent duplicates)
const viewToggle = document.getElementById('plugins-view-toggle');
const viewBtns = viewToggle ? viewToggle.querySelectorAll('.view-btn') : document.querySelectorAll('.view-btn');
const viewBtns = document.querySelectorAll('.view-btn');
viewBtns.forEach(btn => btn.classList.remove('active'));
@@ -1223,11 +1369,17 @@ function escapeHtml(text) {
}
function displayStorePlugins() {
// Version: 2026-01-25-v4 - Store view: Removed Status column, always show Free/Paid badges
// CRITICAL: This function MUST create exactly 7 columns (no Status, no Deactivate/Uninstall)
const tbody = document.getElementById('storeTableBody');
if (!tbody) {
console.error('storeTableBody not found!');
return;
}
tbody.innerHTML = '';
if (!storePlugins || storePlugins.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px; color: var(--text-secondary, #64748b);">No plugins available in store</td></tr>';
return;
}
@@ -1239,57 +1391,38 @@ function displayStorePlugins() {
);
}
filteredPlugins.forEach(plugin => {
filteredPlugins.forEach(plugin => {
const row = document.createElement('tr');
// Status column
let statusHtml = '';
if (plugin.installed) {
statusHtml = '<span class="status-installed">Installed</span>';
} else {
statusHtml = '<span class="status-not-installed">Not Installed</span>';
// Plugin icon - based on plugin type (same logic as Grid/Table views)
const pluginType = (plugin.type || 'Plugin').toLowerCase();
let iconClass = 'fas fa-puzzle-piece'; // Default icon
if (pluginType.includes('security')) {
iconClass = 'fas fa-shield-alt';
} else if (pluginType.includes('performance')) {
iconClass = 'fas fa-rocket';
} else if (pluginType.includes('utility')) {
iconClass = 'fas fa-tools';
} else if (pluginType.includes('backup')) {
iconClass = 'fas fa-save';
}
const iconHtml = `<div class="plugin-icon" style="width: 40px; height: 40px; background: var(--bg-secondary, #f8f9ff); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #5856d6; margin: 0 auto;">
<i class="${iconClass}"></i>
</div>`;
// Action column
// Action column - Store view only shows Install/Installed (no Deactivate/Uninstall)
// NOTE: Store view should NOT show Deactivate/Uninstall buttons - users manage from Grid/Table views
let actionHtml = '';
if (plugin.installed) {
if (plugin.enabled) {
actionHtml = `<div class="plugin-actions">
<button class="btn-action btn-deactivate" onclick="deactivatePlugin('${plugin.plugin_dir}')">
<i class="fas fa-toggle-on"></i> Deactivate
</button>
<button class="btn-action btn-uninstall" onclick="uninstallPluginFromStore('${plugin.plugin_dir}')">
<i class="fas fa-trash"></i> Uninstall
</button>
</div>`;
} else {
actionHtml = `<div class="plugin-actions">
<button class="btn-action btn-activate" onclick="activatePlugin('${plugin.plugin_dir}')">
<i class="fas fa-toggle-off"></i> Activate
</button>
<button class="btn-action btn-uninstall" onclick="uninstallPluginFromStore('${plugin.plugin_dir}')">
<i class="fas fa-trash"></i> Uninstall
</button>
</div>`;
}
// Show "Installed" text
actionHtml = '<span class="status-installed">Installed</span>';
} else {
// Show Install button
actionHtml = `<button class="btn-action btn-install" onclick="installFromStore('${plugin.plugin_dir}')">
<i class="fas fa-download"></i> Install
</button>`;
}
// Active column
let activeHtml = '';
if (plugin.installed) {
if (plugin.enabled) {
activeHtml = '<span class="active-status active-yes"><i class="fas fa-check-circle"></i></span>';
} else {
activeHtml = '<span class="active-status active-no"><i class="fas fa-times-circle"></i></span>';
}
} else {
activeHtml = '<span class="active-status active-na">-</span>';
}
// Help column - always use consistent help URL
const helpUrl = `/plugins/${plugin.plugin_dir}/help/`;
const helpHtml = `<a href="${helpUrl}" class="btn-link" title="Plugin Help">
@@ -1305,19 +1438,29 @@ function displayStorePlugins() {
// Modify Date column - show N/A for store plugins (they're from GitHub, not local)
const modifyDateHtml = plugin.modify_date ? `<small style="color: var(--text-secondary, #64748b);">${escapeHtml(plugin.modify_date)}</small>` : '<small style="color: var(--text-secondary, #64748b);">N/A</small>';
// Author column
const authorHtml = plugin.author ? `<span style="color: var(--text-secondary, #64748b); font-size: 13px;">${escapeHtml(plugin.author)}</span>` : '<span style="color: var(--text-secondary, #64748b); font-size: 13px;">Unknown</span>';
// Pricing badge - ALWAYS show a badge (default to Free if is_paid is missing/undefined/null)
// Version: 2026-01-25-v4 - Normalize is_paid to handle all possible values
let isPaid = false;
if (plugin.is_paid !== undefined && plugin.is_paid !== null) {
const isPaidValue = plugin.is_paid;
if (isPaidValue === true || isPaidValue === 'true' || isPaidValue === 'True' || isPaidValue === 1 || isPaidValue === '1' || String(isPaidValue).toLowerCase() === 'true') {
isPaid = true;
}
}
const pricingBadge = isPaid
? '<span class="plugin-pricing-badge paid">Paid</span>'
: '<span class="plugin-pricing-badge free">Free</span>';
// Version: 2026-01-25-v5 - Added plugin icons to Store view (8 columns: Icon, Plugin Name, Version, Pricing, Modify Date, Action, Help, About)
row.innerHTML = `
<td style="text-align: center;">${iconHtml}</td>
<td>
<strong>${escapeHtml(plugin.name)}</strong>
</td>
<td>${authorHtml}</td>
<td>${escapeHtml(plugin.version)}</td>
<td><span class="plugin-version-number">${escapeHtml(plugin.version)}</span></td>
<td>${pricingBadge}</td>
<td>${modifyDateHtml}</td>
<td>${statusHtml}</td>
<td>${actionHtml}</td>
<td class="active-column">${activeHtml}</td>
<td>${helpHtml}</td>
<td>${aboutHtml}</td>
`;
@@ -1706,19 +1849,6 @@ if (localStorage.getItem('pluginStoreNoticeDismissed') === 'true') {
// Initialize view on page load
document.addEventListener('DOMContentLoaded', function() {
// Remove any duplicate view-toggle divs (keep only the one with id='plugins-view-toggle')
const mainViewToggle = document.getElementById('plugins-view-toggle');
if (mainViewToggle) {
const allViewToggles = document.querySelectorAll('.view-toggle');
allViewToggles.forEach(toggle => {
if (toggle !== mainViewToggle && toggle.id !== 'plugins-view-toggle') {
// This is a duplicate, remove it
console.log('Removing duplicate view-toggle div');
toggle.remove();
}
});
}
// Default to grid view if plugins exist, otherwise show store
const gridView = document.getElementById('gridView');
const storeView = document.getElementById('storeView');

BIN
pluginHolder/testPlugin.zip Normal file

Binary file not shown.

View File

@@ -18,6 +18,7 @@ import urllib.error
import time
sys.path.append('/usr/local/CyberCP')
from pluginInstaller.pluginInstaller import pluginInstaller
from .patreon_verifier import PatreonVerifier
# Plugin state file location
PLUGIN_STATE_DIR = '/home/cyberpanel/plugin_states'
@@ -127,7 +128,9 @@ def installed(request):
data['desc'] = desc_elem.text
data['version'] = version_elem.text
data['plugin_dir'] = plugin # Plugin directory name
data['installed'] = os.path.exists(completePath) # True if installed, False if only in source
# Check if plugin is installed (only if it exists in /usr/local/CyberCP/)
# Source directory presence doesn't mean installed - it just means the source files are available
data['installed'] = os.path.exists(completePath)
# Get plugin enabled state (only for installed plugins)
if data['installed']:
@@ -135,6 +138,11 @@ def installed(request):
else:
data['enabled'] = False
# Initialize is_paid to False by default (will be set later if paid)
data['is_paid'] = False
data['patreon_tier'] = None
data['patreon_url'] = None
# Get modify date from local file (fast, no API calls)
# GitHub commit dates are fetched in the plugin store, not here to avoid timeouts
modify_date = 'N/A'
@@ -181,16 +189,40 @@ def installed(request):
data['author'] = author_elem.text
else:
data['author'] = 'Unknown'
# Extract paid plugin information
paid_elem = root.find('paid')
patreon_tier_elem = root.find('patreon_tier')
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
data['is_paid'] = True
data['patreon_tier'] = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
data['patreon_url'] = root.find('patreon_url').text if root.find('patreon_url') is not None else 'https://www.patreon.com/c/newstargeted/membership'
else:
data['is_paid'] = False
data['patreon_tier'] = None
data['patreon_url'] = None
pluginList.append(data)
processed_plugins.add(plugin) # Mark as processed
except ElementTree.ParseError as e:
errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'})
logging.writeToFile(f"Plugin {plugin}: XML parse error - {str(e)}")
# Don't mark as processed if it failed - let installed check handle it
# This ensures plugins that exist in /usr/local/CyberCP/ but have bad source meta.xml still get counted
if not os.path.exists(completePath):
# Only skip if it's not actually installed
continue
# If it exists in installed location, don't mark as processed so it gets checked there
continue
except Exception as e:
errorPlugins.append({'name': plugin, 'error': f'Error loading plugin: {str(e)}'})
logging.writeToFile(f"Plugin {plugin}: Error loading - {str(e)}")
# Don't mark as processed if it failed - let installed check handle it
if not os.path.exists(completePath):
# Only skip if it's not actually installed
continue
# If it exists in installed location, don't mark as processed so it gets checked there
continue
# Also check for installed plugins that don't have source directories
@@ -236,6 +268,11 @@ def installed(request):
data['installed'] = True # This is an installed plugin
data['enabled'] = _is_plugin_enabled(plugin)
# Initialize is_paid to False by default (will be set later if paid)
data['is_paid'] = False
data['patreon_tier'] = None
data['patreon_url'] = None
# Get modify date from installed location
modify_date = 'N/A'
try:
@@ -276,6 +313,17 @@ def installed(request):
else:
data['author'] = 'Unknown'
# Extract paid plugin information (is_paid already initialized to False above)
paid_elem = root.find('paid')
patreon_tier_elem = root.find('patreon_tier')
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
data['is_paid'] = True
data['patreon_tier'] = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
patreon_url_elem = root.find('patreon_url')
data['patreon_url'] = patreon_url_elem.text if patreon_url_elem is not None and patreon_url_elem.text else 'https://www.patreon.com/membership/27789984'
# else: is_paid already False from initialization above
pluginList.append(data)
except ElementTree.ParseError as e:
@@ -287,8 +335,38 @@ def installed(request):
logging.writeToFile(f"Installed plugin {plugin}: Error loading - {str(e)}")
continue
# Calculate installed and active counts
# Double-check by also counting plugins that actually exist in /usr/local/CyberCP/
installed_plugins_in_filesystem = set()
if os.path.exists(installedPath):
for plugin in os.listdir(installedPath):
pluginInstalledDir = os.path.join(installedPath, plugin)
if os.path.isdir(pluginInstalledDir):
metaXmlPath = os.path.join(pluginInstalledDir, 'meta.xml')
if os.path.exists(metaXmlPath):
installed_plugins_in_filesystem.add(plugin)
# Count installed plugins from the list
installed_count = len([p for p in pluginList if p.get('installed', False)])
active_count = len([p for p in pluginList if p.get('installed', False) and p.get('enabled', False)])
# If there's a discrepancy, use the filesystem count as the source of truth
filesystem_installed_count = len(installed_plugins_in_filesystem)
if filesystem_installed_count != installed_count:
logging.writeToFile(f"WARNING: Plugin count mismatch! List says {installed_count}, filesystem has {filesystem_installed_count}")
logging.writeToFile(f"Plugins in filesystem: {sorted(installed_plugins_in_filesystem)}")
logging.writeToFile(f"Plugins in list with installed=True: {[p.get('plugin_dir') for p in pluginList if p.get('installed', False)]}")
# Use filesystem count as source of truth
installed_count = filesystem_installed_count
# Debug logging to help identify discrepancies
logging.writeToFile(f"Plugin count: Total={len(pluginList)}, Installed={installed_count}, Active={active_count}")
for p in pluginList:
logging.writeToFile(f" - {p.get('plugin_dir')}: installed={p.get('installed')}, enabled={p.get('enabled')}")
proc = httpProc(request, 'pluginHolder/plugins.html',
{'plugins': pluginList, 'error_plugins': errorPlugins}, 'admin')
{'plugins': pluginList, 'error_plugins': errorPlugins,
'installed_count': installed_count, 'active_count': active_count}, 'admin')
return proc.render()
@csrf_exempt
@@ -315,11 +393,25 @@ def install_plugin(request, plugin_name):
# Create zip file for installation (pluginInstaller expects a zip)
import tempfile
import shutil
import zipfile
temp_dir = tempfile.mkdtemp()
zip_path = os.path.join(temp_dir, plugin_name + '.zip')
# Create zip from source directory
shutil.make_archive(os.path.join(temp_dir, plugin_name), 'zip', pluginSource)
# Create zip from source directory with correct structure
# The ZIP must contain plugin_name/ directory structure for proper extraction
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
# Walk through source directory and add files with plugin_name prefix
for root, dirs, files in os.walk(pluginSource):
for file in files:
file_path = os.path.join(root, file)
# Calculate relative path from plugin source
arcname = os.path.relpath(file_path, pluginSource)
# Add plugin_name prefix to maintain directory structure
arcname = os.path.join(plugin_name, arcname)
plugin_zip.write(file_path, arcname)
plugin_zip.close()
# Verify zip file was created
if not os.path.exists(zip_path):
@@ -340,11 +432,31 @@ def install_plugin(request, plugin_name):
raise Exception(f'Zip file {zip_file} not found in temp directory')
# Install using pluginInstaller
pluginInstaller.installPlugin(plugin_name)
try:
pluginInstaller.installPlugin(plugin_name)
except Exception as install_error:
# Log the full error for debugging
error_msg = str(install_error)
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
# Check if plugin directory exists despite the error
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if os.path.exists(pluginInstalled):
logging.writeToFile(f"Plugin directory exists despite error, continuing...")
else:
raise Exception(f'Plugin installation failed: {error_msg}')
# Wait a moment for file system to sync
import time
time.sleep(2)
# Verify plugin was actually installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if not os.path.exists(pluginInstalled):
# Check if files were extracted to root instead
root_files = ['README.md', 'apps.py', 'meta.xml', 'urls.py', 'views.py']
found_root_files = [f for f in root_files if os.path.exists(os.path.join('/usr/local/CyberCP', f))]
if found_root_files:
raise Exception(f'Plugin installation failed: Files extracted to wrong location. Found {found_root_files} in /usr/local/CyberCP/ root instead of {pluginInstalled}/')
raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation')
# Set plugin to enabled by default after installation
@@ -551,10 +663,11 @@ def _enrich_store_plugins(plugins):
continue
# Check if plugin is installed locally
# Plugin is only considered "installed" if it exists in /usr/local/CyberCP/
# Source directory presence doesn't mean installed - it just means the source files are available
installed_path = os.path.join(plugin_install_dir, plugin_dir)
source_path = os.path.join(plugin_source_dir, plugin_dir)
plugin['installed'] = os.path.exists(installed_path) or os.path.exists(source_path)
plugin['installed'] = os.path.exists(installed_path)
# Check if plugin is enabled (only if installed)
if plugin['installed']:
@@ -562,6 +675,40 @@ def _enrich_store_plugins(plugins):
else:
plugin['enabled'] = False
# Ensure is_paid field exists and is properly set (default to False if not set or invalid)
# Handle all possible cases: missing, None, empty string, string values, boolean
is_paid_value = plugin.get('is_paid', False)
# Normalize is_paid to boolean
if is_paid_value is None or is_paid_value == '' or is_paid_value == 'false' or is_paid_value == 'False' or is_paid_value == '0':
plugin['is_paid'] = False
elif is_paid_value is True or is_paid_value == 'true' or is_paid_value == 'True' or is_paid_value == '1' or str(is_paid_value).lower() == 'true':
plugin['is_paid'] = True
elif 'is_paid' not in plugin or plugin.get('is_paid') is None:
# Try to check from local meta.xml if available
meta_path = None
if os.path.exists(installed_path):
meta_path = os.path.join(installed_path, 'meta.xml')
elif os.path.exists(source_path):
meta_path = os.path.join(source_path, 'meta.xml')
if meta_path and os.path.exists(meta_path):
try:
pluginMetaData = ElementTree.parse(meta_path)
root = pluginMetaData.getroot()
paid_elem = root.find('paid')
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
plugin['is_paid'] = True
else:
plugin['is_paid'] = False
except:
plugin['is_paid'] = False
else:
plugin['is_paid'] = False # Default to free if we can't determine
else:
# Already set, but ensure it's boolean
plugin['is_paid'] = bool(plugin['is_paid']) if plugin['is_paid'] not in [True, False] else plugin['is_paid']
enriched.append(plugin)
return enriched
@@ -633,6 +780,20 @@ def _fetch_plugins_from_github():
logging.writeToFile(f"Could not fetch commit date for {plugin_name}: {str(e)}")
modify_date = 'N/A'
# Extract paid plugin information
paid_elem = root.find('paid')
patreon_tier_elem = root.find('patreon_tier')
is_paid = False
patreon_tier = None
patreon_url = None
if paid_elem is not None and paid_elem.text and paid_elem.text.lower() == 'true':
is_paid = True
patreon_tier = patreon_tier_elem.text if patreon_tier_elem is not None and patreon_tier_elem.text else 'CyberPanel Paid Plugin'
patreon_url_elem = root.find('patreon_url')
patreon_url = patreon_url_elem.text if patreon_url_elem is not None else 'https://www.patreon.com/c/newstargeted/membership'
plugin_data = {
'plugin_dir': plugin_name,
'name': root.find('name').text if root.find('name') is not None else plugin_name,
@@ -644,7 +805,10 @@ def _fetch_plugins_from_github():
'author': root.find('author').text if root.find('author') is not None else 'Unknown',
'github_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
'about_url': f'https://github.com/master3395/cyberpanel-plugins/tree/main/{plugin_name}',
'modify_date': modify_date
'modify_date': modify_date,
'is_paid': is_paid,
'patreon_tier': patreon_tier,
'patreon_url': patreon_url
}
plugins.append(plugin_data)
@@ -746,7 +910,7 @@ def fetch_plugin_store(request):
@csrf_exempt
@require_http_methods(["POST"])
def install_from_store(request, plugin_name):
"""Install plugin from GitHub store"""
"""Install plugin from GitHub store, with fallback to local source"""
mailUtilities.checkHome()
try:
@@ -771,44 +935,80 @@ def install_from_store(request, plugin_name):
zip_path = os.path.join(temp_dir, plugin_name + '.zip')
try:
# Download repository as ZIP
repo_zip_url = 'https://github.com/master3395/cyberpanel-plugins/archive/refs/heads/main.zip'
logging.writeToFile(f"Downloading plugin from: {repo_zip_url}")
# Try to download from GitHub first
use_local_fallback = False
try:
# Download repository as ZIP
repo_zip_url = 'https://github.com/master3395/cyberpanel-plugins/archive/refs/heads/main.zip'
logging.writeToFile(f"Downloading plugin from: {repo_zip_url}")
repo_req = urllib.request.Request(
repo_zip_url,
headers={
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
'Accept': 'application/zip'
}
)
with urllib.request.urlopen(repo_req, timeout=30) as repo_response:
repo_zip_data = repo_response.read()
# Extract plugin directory from repository ZIP
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
# Find plugin directory in ZIP
plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/'
plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)]
if not plugin_files:
logging.writeToFile(f"Plugin {plugin_name} not found in GitHub repository, trying local source")
use_local_fallback = True
else:
logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub")
# Create plugin ZIP file from GitHub with correct structure
# The ZIP must contain plugin_name/ directory structure for proper extraction
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
for file_path in plugin_files:
# Remove the repository root prefix
relative_path = file_path[len(plugin_prefix):]
if relative_path: # Skip directories
file_data = repo_zip.read(file_path)
# Add plugin_name prefix to maintain directory structure
arcname = os.path.join(plugin_name, relative_path)
plugin_zip.writestr(arcname, file_data)
plugin_zip.close()
repo_zip.close()
except Exception as github_error:
logging.writeToFile(f"GitHub download failed for {plugin_name}: {str(github_error)}, trying local source")
use_local_fallback = True
repo_req = urllib.request.Request(
repo_zip_url,
headers={
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
'Accept': 'application/zip'
}
)
with urllib.request.urlopen(repo_req, timeout=30) as repo_response:
repo_zip_data = repo_response.read()
# Extract plugin directory from repository ZIP
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
# Find plugin directory in ZIP
plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/'
plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)]
if not plugin_files:
raise Exception(f'Plugin {plugin_name} not found in GitHub repository')
logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name}")
# Create plugin ZIP file
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
for file_path in plugin_files:
# Remove the repository root prefix
relative_path = file_path[len(plugin_prefix):]
if relative_path: # Skip directories
file_data = repo_zip.read(file_path)
plugin_zip.writestr(relative_path, file_data)
plugin_zip.close()
# Fallback to local source if GitHub download failed
if use_local_fallback:
pluginSource = '/home/cyberpanel/plugins/' + plugin_name
if not os.path.exists(pluginSource):
raise Exception(f'Plugin {plugin_name} not found in GitHub repository and local source not found at {pluginSource}')
logging.writeToFile(f"Using local source for {plugin_name} from {pluginSource}")
# Create zip from local source directory with correct structure
# The ZIP must contain plugin_name/ directory structure for proper extraction
import zipfile
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
# Walk through source directory and add files with plugin_name prefix
for root, dirs, files in os.walk(pluginSource):
for file in files:
file_path = os.path.join(root, file)
# Calculate relative path from plugin source
arcname = os.path.relpath(file_path, pluginSource)
# Add plugin_name prefix to maintain directory structure
arcname = os.path.join(plugin_name, arcname)
plugin_zip.write(file_path, arcname)
plugin_zip.close()
# Verify ZIP was created
if not os.path.exists(zip_path):
@@ -829,15 +1029,31 @@ def install_from_store(request, plugin_name):
logging.writeToFile(f"Installing plugin using pluginInstaller")
# Install using pluginInstaller (direct call, not via command line)
pluginInstaller.installPlugin(plugin_name)
try:
pluginInstaller.installPlugin(plugin_name)
except Exception as install_error:
# Log the full error for debugging
error_msg = str(install_error)
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
# Check if plugin directory exists despite the error
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if os.path.exists(pluginInstalled):
logging.writeToFile(f"Plugin directory exists despite error, continuing...")
else:
raise Exception(f'Plugin installation failed: {error_msg}')
# Wait a moment for file system to sync and service to restart
import time
time.sleep(2)
time.sleep(3) # Increased wait time for file system sync
# Verify plugin was actually installed
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
if not os.path.exists(pluginInstalled):
# Check if files were extracted to root instead
root_files = ['README.md', 'apps.py', 'meta.xml', 'urls.py', 'views.py']
found_root_files = [f for f in root_files if os.path.exists(os.path.join('/usr/local/CyberCP', f))]
if found_root_files:
raise Exception(f'Plugin installation failed: Files extracted to wrong location. Found {found_root_files} in /usr/local/CyberCP/ root instead of {pluginInstalled}/')
raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation')
logging.writeToFile(f"Plugin {plugin_name} installed successfully")
@@ -1062,3 +1278,58 @@ def plugin_help(request, plugin_name):
proc = httpProc(request, 'pluginHolder/plugin_help.html', context, 'admin')
return proc.render()
@csrf_exempt
@require_http_methods(["GET"])
def check_plugin_subscription(request, plugin_name):
"""
API endpoint to check if user has Patreon subscription for a paid plugin
Args:
request: Django request object
plugin_name: Name of the plugin to check
Returns:
JsonResponse: {
'has_access': bool,
'is_paid': bool,
'message': str,
'patreon_url': str or None
}
"""
try:
# Check if user is authenticated
if not request.user or not request.user.is_authenticated:
return JsonResponse({
'success': False,
'has_access': False,
'is_paid': False,
'message': 'Please log in to check subscription status',
'patreon_url': None
}, status=401)
# Load plugin metadata
from .plugin_access import check_plugin_access, _load_plugin_meta
plugin_meta = _load_plugin_meta(plugin_name)
# Check access
access_result = check_plugin_access(request, plugin_name, plugin_meta)
return JsonResponse({
'success': True,
'has_access': access_result['has_access'],
'is_paid': access_result['is_paid'],
'message': access_result['message'],
'patreon_url': access_result.get('patreon_url')
})
except Exception as e:
logging.writeToFile(f"Error checking subscription for {plugin_name}: {str(e)}")
return JsonResponse({
'success': False,
'has_access': False,
'is_paid': False,
'message': f'Error checking subscription: {str(e)}',
'patreon_url': None
}, status=500)

View File

@@ -71,8 +71,8 @@ class pluginInstaller:
@staticmethod
def upgradingSettingsFile(pluginName):
data = open("/usr/local/CyberCP/CyberCP/settings.py", 'r').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/settings.py", 'w')
data = open("/usr/local/CyberCP/CyberCP/settings.py", 'r', encoding='utf-8').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/settings.py", 'w', encoding='utf-8')
for items in data:
if items.find("'emailPremium',") > -1:
@@ -90,8 +90,8 @@ class pluginInstaller:
Plugin URLs must be inserted BEFORE the generic 'plugins/' line
to ensure proper route matching (more specific routes first)
"""
data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w')
data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r', encoding='utf-8').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w', encoding='utf-8')
urlPatternAdded = False
for items in data:
@@ -109,7 +109,7 @@ class pluginInstaller:
if not urlPatternAdded:
pluginInstaller.stdOut(f"Warning: 'plugins/' line not found, using fallback insertion after 'manageservices'")
writeToFile.close()
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w')
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w', encoding='utf-8')
for items in data:
if items.find("manageservices") > -1:
writeToFile.writelines(items)
@@ -132,8 +132,8 @@ class pluginInstaller:
@staticmethod
def addInterfaceLink(pluginName):
data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r').readlines()
writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w')
data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r', encoding='utf-8').readlines()
writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w', encoding='utf-8')
for items in data:
if items.find("{# pluginsList #}") > -1:
@@ -155,7 +155,7 @@ class pluginInstaller:
os.chdir('/usr/local/CyberCP')
command = "/usr/local/CyberCP/bin/python manage.py collectstatic --noinput"
command = "python3 /usr/local/CyberCP/manage.py collectstatic --noinput"
subprocess.call(shlex.split(command))
command = "mv /usr/local/CyberCP/static /usr/local/lscp/cyberpanel"
@@ -168,9 +168,9 @@ class pluginInstaller:
def installMigrations(pluginName):
currentDir = os.getcwd()
os.chdir('/usr/local/CyberCP')
command = "/usr/local/CyberCP/bin/python manage.py makemigrations %s" % pluginName
command = "python3 /usr/local/CyberCP/manage.py makemigrations %s" % pluginName
subprocess.call(shlex.split(command))
command = "/usr/local/CyberCP/bin/python manage.py migrate %s" % pluginName
command = "python3 /usr/local/CyberCP/manage.py migrate %s" % pluginName
subprocess.call(shlex.split(command))
os.chdir(currentDir)
@@ -285,16 +285,120 @@ class pluginInstaller:
@staticmethod
def removeFiles(pluginName):
pluginPath = '/usr/local/CyberCP/' + pluginName
if os.path.exists(pluginPath):
shutil.rmtree(pluginPath)
if not os.path.exists(pluginPath):
# Directory doesn't exist - already removed
pluginInstaller.stdOut(f'Plugin directory does not exist (already removed): {pluginName}')
return
try:
# Check if we're running as root
is_root = os.geteuid() == 0 if hasattr(os, 'geteuid') else False
use_sudo = not is_root
# First try: Use shutil.rmtree (works if permissions are correct)
try:
shutil.rmtree(pluginPath)
pluginInstaller.stdOut(f'Plugin directory removed: {pluginName}')
return
except (OSError, PermissionError) as e:
pluginInstaller.stdOut(f'Direct removal failed, trying with permission fix: {str(e)}')
# Second try: Fix permissions, then remove
try:
import subprocess
import stat
if use_sudo:
# Use ProcessUtilities which handles privileged commands
# Fix ownership recursively
chown_cmd = f'chown -R cyberpanel:cyberpanel {pluginPath}'
ProcessUtilities.normalExecutioner(chown_cmd)
# Fix permissions recursively
chmod_cmd = f'chmod -R u+rwX,go+rX {pluginPath}'
ProcessUtilities.normalExecutioner(chmod_cmd)
else:
# Running as root - fix permissions directly
import pwd
import grp
try:
cyberpanel_uid = pwd.getpwnam('cyberpanel').pw_uid
cyberpanel_gid = grp.getgrnam('cyberpanel').gr_gid
except (KeyError, OSError):
cyberpanel_uid = 0
cyberpanel_gid = 0
# Recursively fix ownership and permissions
for root, dirs, files in os.walk(pluginPath):
try:
os.chown(root, cyberpanel_uid, cyberpanel_gid)
os.chmod(root, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
except (OSError, PermissionError):
pass
for d in dirs:
dir_path = os.path.join(root, d)
try:
os.chown(dir_path, cyberpanel_uid, cyberpanel_gid)
os.chmod(dir_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH)
except (OSError, PermissionError):
pass
for f in files:
file_path = os.path.join(root, f)
try:
os.chown(file_path, cyberpanel_uid, cyberpanel_gid)
os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
except (OSError, PermissionError):
pass
# Now try to remove
shutil.rmtree(pluginPath)
pluginInstaller.stdOut(f'Plugin directory removed after permission fix: {pluginName}')
return
except Exception as e:
pluginInstaller.stdOut(f'Permission fix and removal failed: {str(e)}')
# Third try: Use rm -rf (with or without sudo depending on privileges)
try:
if use_sudo:
# Use ProcessUtilities for privileged removal
rm_cmd = f'rm -rf {pluginPath}'
ProcessUtilities.normalExecutioner(rm_cmd)
else:
# Running as root - use subprocess directly
result = subprocess.run(
['rm', '-rf', pluginPath],
capture_output=True, text=True, timeout=30
)
if result.returncode != 0:
raise Exception(f"rm -rf failed: {result.stderr}")
pluginInstaller.stdOut(f'Plugin directory removed using rm -rf: {pluginName}')
return
except Exception as e:
raise Exception(f"All removal methods failed. Last error: {str(e)}")
except Exception as e:
pluginInstaller.stdOut(f"Error removing plugin files: {str(e)}")
raise Exception(f"Failed to remove plugin directory: {str(e)}")
@staticmethod
def removeFromSettings(pluginName):
data = open("/usr/local/CyberCP/CyberCP/settings.py", 'r').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/settings.py", 'w')
for items in data:
if items.find(pluginName) > -1:
data = open("/usr/local/CyberCP/CyberCP/settings.py", 'r', encoding='utf-8').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/settings.py", 'w', encoding='utf-8')
in_installed_apps = False
for i, items in enumerate(data):
# Track if we're in INSTALLED_APPS section
if 'INSTALLED_APPS' in items and '=' in items:
in_installed_apps = True
elif in_installed_apps and items.strip().startswith(']'):
in_installed_apps = False
# More precise matching: look for plugin name in quotes (e.g., 'pluginName' or "pluginName")
# Only match if we're in INSTALLED_APPS section to prevent false positives
if in_installed_apps and (f"'{pluginName}'" in items or f'"{pluginName}"' in items):
continue
else:
writeToFile.writelines(items)
@@ -302,11 +406,15 @@ class pluginInstaller:
@staticmethod
def removeFromURLs(pluginName):
data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w')
data = open("/usr/local/CyberCP/CyberCP/urls.py", 'r', encoding='utf-8').readlines()
writeToFile = open("/usr/local/CyberCP/CyberCP/urls.py", 'w', encoding='utf-8')
for items in data:
if items.find(pluginName) > -1:
# More precise matching: look for plugin name in path() or include() calls
# Match patterns like: path('plugins/pluginName/', include('pluginName.urls'))
# This prevents partial matches
if (f"plugins/{pluginName}/" in items or f"'{pluginName}.urls'" in items or f'"{pluginName}.urls"' in items or
f"include('{pluginName}.urls')" in items or f'include("{pluginName}.urls")' in items):
continue
else:
writeToFile.writelines(items)
@@ -322,8 +430,8 @@ class pluginInstaller:
@staticmethod
def removeInterfaceLink(pluginName):
data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r').readlines()
writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w')
data = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'r', encoding='utf-8').readlines()
writeToFile = open("/usr/local/CyberCP/baseTemplate/templates/baseTemplate/index.html", 'w', encoding='utf-8')
for items in data:
if items.find(pluginName) > -1 and items.find('<li>') > -1:
@@ -336,7 +444,7 @@ class pluginInstaller:
def removeMigrations(pluginName):
currentDir = os.getcwd()
os.chdir('/usr/local/CyberCP')
command = "/usr/local/CyberCP/bin/python manage.py migrate %s zero" % pluginName
command = "python3 /usr/local/CyberCP/manage.py migrate %s zero" % pluginName
subprocess.call(shlex.split(command))
os.chdir(currentDir)

269
premiumPlugin/views.py Normal file
View File

@@ -0,0 +1,269 @@
# -*- coding: utf-8 -*-
"""
Premium Plugin Views - Remote Verification Version
This version uses remote server verification (no secrets in plugin)
SECURITY: All Patreon API calls happen on YOUR server, not user's server
"""
from django.shortcuts import render, redirect
from django.http import JsonResponse
from plogical.mailUtilities import mailUtilities
from plogical.httpProc import httpProc
from functools import wraps
import sys
import os
import urllib.request
import urllib.error
import json
# Remote verification server (YOUR server, not user's server)
REMOTE_VERIFICATION_URL = 'https://api.newstargeted.com/api/verify-patreon-membership'
PLUGIN_NAME = 'premiumPlugin' # Patreon Premium Plugin Example
PLUGIN_VERSION = '1.0.0'
def cyberpanel_login_required(view_func):
"""
Custom decorator that checks for CyberPanel session userID
"""
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
try:
userID = request.session['userID']
# User is authenticated via CyberPanel session
return view_func(request, *args, **kwargs)
except KeyError:
# Not logged in, redirect to login
from loginSystem.views import loadLoginPage
return redirect(loadLoginPage)
return _wrapped_view
def remote_verification_required(view_func):
"""
Decorator that checks Patreon membership via remote server
No secrets stored in plugin - all verification happens on your server
"""
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
# First check login
try:
userID = request.session['userID']
except KeyError:
from loginSystem.views import loadLoginPage
return redirect(loadLoginPage)
# Get user email
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
if not user_email:
# Try to get from session or username
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
# Check membership via remote server
verification_result = check_remote_membership(user_email, request.META.get('REMOTE_ADDR', ''))
if not verification_result.get('has_access', False):
# User doesn't have subscription - show subscription required page
context = {
'plugin_name': 'Patreon Premium Plugin Example',
'is_paid': True,
'patreon_tier': verification_result.get('patreon_tier', 'CyberPanel Paid Plugin'),
'patreon_url': verification_result.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership'),
'message': verification_result.get('message', 'Patreon subscription required'),
'error': verification_result.get('error')
}
proc = httpProc(request, 'premiumPlugin/subscription_required.html', context, 'admin')
return proc.render()
# User has access - proceed with view
return view_func(request, *args, **kwargs)
return _wrapped_view
def check_remote_membership(user_email, user_ip=''):
"""
Check Patreon membership via remote verification server
Args:
user_email: User's email address
user_ip: User's IP address (for logging/security)
Returns:
dict: {
'has_access': bool,
'patreon_tier': str,
'patreon_url': str,
'message': str,
'error': str or None
}
"""
try:
# Prepare request data
request_data = {
'user_email': user_email,
'plugin_name': PLUGIN_NAME,
'plugin_version': PLUGIN_VERSION,
'user_ip': user_ip,
'tier_id': '27789984' # CyberPanel Paid Plugin tier ID
}
# Make request to remote verification server
req = urllib.request.Request(
REMOTE_VERIFICATION_URL,
data=json.dumps(request_data).encode('utf-8'),
headers={
'Content-Type': 'application/json',
'User-Agent': f'CyberPanel-Plugin/{PLUGIN_VERSION}',
'X-Plugin-Name': PLUGIN_NAME
}
)
# Send request with timeout
try:
with urllib.request.urlopen(req, timeout=10) as response:
response_data = json.loads(response.read().decode('utf-8'))
if response_data.get('success', False):
return {
'has_access': response_data.get('has_access', False),
'patreon_tier': response_data.get('patreon_tier', 'CyberPanel Paid Plugin'),
'patreon_url': response_data.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership'),
'message': response_data.get('message', 'Access granted'),
'error': None
}
else:
return {
'has_access': False,
'patreon_tier': response_data.get('patreon_tier', 'CyberPanel Paid Plugin'),
'patreon_url': response_data.get('patreon_url', 'https://www.patreon.com/c/newstargeted/membership'),
'message': response_data.get('message', 'Patreon subscription required'),
'error': response_data.get('error')
}
except urllib.error.HTTPError as e:
# Server returned error
error_body = e.read().decode('utf-8') if e.fp else 'Unknown error'
return {
'has_access': False,
'patreon_tier': 'CyberPanel Paid Plugin',
'patreon_url': 'https://www.patreon.com/c/newstargeted/membership',
'message': 'Unable to verify subscription. Please try again later.',
'error': f'HTTP {e.code}: {error_body}'
}
except urllib.error.URLError as e:
# Network error
return {
'has_access': False,
'patreon_tier': 'CyberPanel Paid Plugin',
'patreon_url': 'https://www.patreon.com/c/newstargeted/membership',
'message': 'Unable to connect to verification server. Please check your internet connection.',
'error': str(e.reason) if hasattr(e, 'reason') else str(e)
}
except Exception as e:
# Other errors
return {
'has_access': False,
'patreon_tier': 'CyberPanel Paid Plugin',
'patreon_url': 'https://www.patreon.com/c/newstargeted/membership',
'message': 'Verification error occurred. Please try again later.',
'error': str(e)
}
except Exception as e:
import logging
logging.writeToFile(f"Error in remote membership check: {str(e)}")
return {
'has_access': False,
'patreon_tier': 'CyberPanel Paid Plugin',
'patreon_url': 'https://www.patreon.com/c/newstargeted/membership',
'message': 'Verification error occurred. Please try again later.',
'error': str(e)
}
@cyberpanel_login_required
def main_view(request):
"""
Main view for premium plugin
Shows plugin information and features if subscribed, or subscription required message if not
"""
mailUtilities.checkHome()
# Get user email for verification
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
if not user_email:
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
# Check membership status (but don't block access)
verification_result = check_remote_membership(user_email, request.META.get('REMOTE_ADDR', ''))
has_access = verification_result.get('has_access', False)
# Determine plugin status
plugin_status = 'Active' if has_access else 'Subscription Required'
context = {
'plugin_name': 'Patreon Premium Plugin Example',
'version': PLUGIN_VERSION,
'status': plugin_status,
'has_access': has_access,
'description': 'This is an example paid plugin that requires Patreon subscription.' if not has_access else 'This is an example paid plugin. You have access because you are subscribed to Patreon!',
'patreon_tier': verification_result.get('patreon_tier', 'CyberPanel Paid Plugin'),
'patreon_url': verification_result.get('patreon_url', 'https://www.patreon.com/membership/27789984'),
'features': [
'Premium Feature 1',
'Premium Feature 2',
'Premium Feature 3',
'Advanced Configuration',
'Priority Support'
] if has_access else []
}
proc = httpProc(request, 'premiumPlugin/index.html', context, 'admin')
return proc.render()
@cyberpanel_login_required
def settings_view(request):
"""
Settings page for premium plugin
Shows settings but disables them if user doesn't have Patreon subscription
"""
mailUtilities.checkHome()
# Get user email for verification
user_email = getattr(request.user, 'email', None) if hasattr(request, 'user') and request.user else None
if not user_email:
user_email = request.session.get('email', '') or getattr(request.user, 'username', '')
# Check membership status (but don't block access)
verification_result = check_remote_membership(user_email, request.META.get('REMOTE_ADDR', ''))
has_access = verification_result.get('has_access', False)
# Determine plugin status
plugin_status = 'Active' if has_access else 'Subscription Required'
context = {
'plugin_name': 'Patreon Premium Plugin Example',
'version': PLUGIN_VERSION,
'plugin_status': plugin_status,
'status': plugin_status, # Keep both for compatibility
'description': 'Configure your premium plugin settings',
'has_access': has_access,
'patreon_tier': verification_result.get('patreon_tier', 'CyberPanel Paid Plugin'),
'patreon_url': verification_result.get('patreon_url', 'https://www.patreon.com/membership/27789984'),
'verification_message': verification_result.get('message', '')
}
proc = httpProc(request, 'premiumPlugin/settings.html', context, 'admin')
return proc.render()
@cyberpanel_login_required
@remote_verification_required
def api_status_view(request):
"""
API endpoint for plugin status
Only accessible with Patreon subscription (verified remotely)
"""
return JsonResponse({
'plugin_name': 'Patreon Premium Plugin Example',
'version': PLUGIN_VERSION,
'status': 'active',
'subscription': 'active',
'description': 'Premium plugin is active and accessible',
'verification_method': 'remote'
})

File diff suppressed because it is too large Load Diff

View File

@@ -729,6 +729,59 @@ app.controller('listContainers', function ($scope, $http) {
$scope.assignActive = "";
$scope.dockerOwner = "";
/* Pagination (ACTIVITY BOARD-style) */
var CONTAINERS_PER_PAGE = 10;
$scope.pagination = { containers: { currentPage: 1, itemsPerPage: CONTAINERS_PER_PAGE } };
$scope.gotoPageInput = { containers: 1 };
$scope.totalCount = 0;
$scope.totalPages = 1;
$scope.Math = Math;
$scope.getTotalPages = function(section) {
if (section === 'containers') return Math.max(1, $scope.totalPages || 1);
return 1;
};
$scope.goToPage = function(section, page) {
if (section !== 'containers') return;
var totalPages = $scope.getTotalPages(section);
var p = parseInt(page, 10);
if (isNaN(p) || p < 1) p = 1;
if (p > totalPages) p = totalPages;
$scope.getFurtherContainersFromDB(p);
};
$scope.nextPage = function(section) {
if (section !== 'containers') return;
if ($scope.pagination.containers.currentPage < $scope.getTotalPages(section)) {
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage + 1);
}
};
$scope.prevPage = function(section) {
if (section !== 'containers') return;
if ($scope.pagination.containers.currentPage > 1) {
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage - 1);
}
};
$scope.getPageNumbers = function(section) {
if (section !== 'containers') return [];
var totalPages = $scope.getTotalPages(section);
var current = $scope.pagination.containers.currentPage;
var maxVisible = 5;
var pages = [];
if (totalPages <= maxVisible) {
for (var i = 1; i <= totalPages; i++) pages.push(i);
} else {
var start = Math.max(1, current - 2);
var end = Math.min(totalPages, start + maxVisible - 1);
if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1);
for (var j = start; j <= end; j++) pages.push(j);
}
return pages;
};
$scope.assignContainer = function (name) {
console.log('assignContainer called with:', name);
$scope.assignActive = name;
@@ -783,7 +836,7 @@ app.controller('listContainers', function ($scope, $http) {
title: 'Container assigned successfully',
type: 'success'
});
window.location.href = '/docker/listContainers';
window.location.href = '/docker/containers';
}
else {
new PNotify({
@@ -1264,77 +1317,35 @@ app.controller('listContainers', function ($scope, $http) {
$scope.logInfo = null;
};
url = "/docker/getContainerList";
var data = {page: 1};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
console.log(response);
function handleContainerListResponse(response) {
if (response.data.listContainerStatus === 1) {
var finalData = JSON.parse(response.data.data);
$scope.ContainerList = finalData;
console.log($scope.ContainerList);
$scope.totalCount = response.data.totalCount || 0;
$scope.totalPages = Math.max(1, response.data.totalPages || 1);
var cp = Math.max(1, parseInt(response.data.currentPage, 10) || 1);
$scope.pagination.containers.currentPage = cp;
$scope.gotoPageInput.containers = cp;
$("#listFail").hide();
}
else {
} else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
$scope.errorMessage = response.data.error_message || 'Failed to load containers';
}
}
function cantLoadInitialData(response) {
console.log("not good");
$("#listFail").fadeIn();
$scope.errorMessage = (response && response.data && response.data.error_message) ? response.data.error_message : 'Could not connect to server';
}
var config = {
headers: { 'X-CSRFToken': getCookie('csrftoken') }
};
$http.post("/docker/getContainerList", { page: 1 }, config).then(handleContainerListResponse, cantLoadInitialData);
$scope.getFurtherContainersFromDB = function (pageNumber) {
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = {page: pageNumber};
dataurl = "/docker/getContainerList";
$http.post(dataurl, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
if (response.data.listContainerStatus === 1) {
var finalData = JSON.parse(response.data.data);
$scope.ContainerList = finalData;
$("#listFail").hide();
}
else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
console.log(response.data);
}
}
function cantLoadInitialData(response) {
console.log("not good");
}
var p = Math.max(1, parseInt(pageNumber, 10) || 1);
$http.post("/docker/getContainerList", { page: p }, config).then(handleContainerListResponse, cantLoadInitialData);
};
});
@@ -1906,7 +1917,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
text: 'Redirecting...',
type: 'success'
});
window.location.href = '/docker/listContainers';
window.location.href = '/docker/containers';
}
else {
new PNotify({
@@ -2703,257 +2714,3 @@ app.controller('manageImages', function ($scope, $http) {
}
});
// Container List Controller
app.controller('listContainers', function ($scope, $http, $timeout, $window) {
$scope.containers = [];
$scope.loading = false;
$scope.updateContainerName = '';
$scope.currentImage = '';
$scope.newImage = '';
$scope.newTag = 'latest';
// Load containers list
$scope.loadContainers = function() {
$scope.loading = true;
var url = '/docker/listContainers';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$scope.loading = false;
if (response.data.status === 1) {
$scope.containers = response.data.containers || [];
} else {
new PNotify({
title: 'Error Loading Containers',
text: response.data.error_message || 'Failed to load containers',
type: 'error'
});
}
}, function(error) {
$scope.loading = false;
new PNotify({
title: 'Connection Error',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Initialize containers on page load
$scope.loadContainers();
// Open update container modal
$scope.openUpdateModal = function(container) {
$scope.updateContainerName = container.name;
$scope.currentImage = container.image;
$scope.newImage = '';
$scope.newTag = 'latest';
$('#updateContainer').modal('show');
};
// Perform container update
$scope.performUpdate = function() {
if (!$scope.newImage && !$scope.newTag) {
new PNotify({
title: 'Missing Information',
text: 'Please enter a new image name or tag',
type: 'error'
});
return;
}
// If no new image specified, use current image with new tag
var imageToUse = $scope.newImage || $scope.currentImage.split(':')[0];
var tagToUse = $scope.newTag || 'latest';
var data = {
containerName: $scope.updateContainerName,
newImage: imageToUse,
newTag: tagToUse
};
var url = '/docker/updateContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
// Show loading
$('#updateContainer').modal('hide');
new PNotify({
title: 'Updating Container',
text: 'Please wait while the container is being updated...',
type: 'info',
hide: false
});
$http.post(url, data, config).then(function(response) {
if (response.data.updateContainerStatus === 1) {
new PNotify({
title: 'Container Updated Successfully',
text: response.data.message || 'Container has been updated successfully',
type: 'success'
});
// Reload containers list
$scope.loadContainers();
} else {
new PNotify({
title: 'Update Failed',
text: response.data.error_message || 'Failed to update container',
type: 'error'
});
}
}, function(error) {
new PNotify({
title: 'Update Failed',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Container actions
$scope.startContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/startContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.startContainerStatus === 1) {
new PNotify({
title: 'Container Started',
text: 'Container has been started successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Start Failed',
text: response.data.error_message || 'Failed to start container',
type: 'error'
});
}
});
};
$scope.stopContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/stopContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.stopContainerStatus === 1) {
new PNotify({
title: 'Container Stopped',
text: 'Container has been stopped successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Stop Failed',
text: response.data.error_message || 'Failed to stop container',
type: 'error'
});
}
});
};
$scope.restartContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/restartContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.restartContainerStatus === 1) {
new PNotify({
title: 'Container Restarted',
text: 'Container has been restarted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Restart Failed',
text: response.data.error_message || 'Failed to restart container',
type: 'error'
});
}
});
};
$scope.deleteContainerWithData = function(containerName) {
if (confirm('Are you sure you want to delete this container and all its data? This action cannot be undone.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerWithData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerWithDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container and all data have been deleted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
$scope.deleteContainerKeepData = function(containerName) {
if (confirm('Are you sure you want to delete this container but keep the data? The container will be removed but volumes will be preserved.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerKeepData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerKeepDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container has been deleted but data has been preserved',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
});

View File

@@ -36,7 +36,8 @@ from math import ceil
from plogical.alias import AliasManager
from plogical.applicationInstaller import ApplicationInstaller
from plogical import hashPassword, randomPassword
from emailMarketing.emACL import emACL
# emailMarketing removed from INSTALLED_APPS
# from emailMarketing.emACL import emACL
from plogical.processUtilities import ProcessUtilities
from managePHP.phpManager import PHPManager
from ApachController.ApacheVhosts import ApacheVhost
@@ -3729,7 +3730,9 @@ context /cyberpanel_suspension_page.html {
from plogical.processUtilities import ProcessUtilities
marketingStatus = emACL.checkIfEMEnabled(admin.userName)
# emailMarketing removed - always return False for marketing status
# marketingStatus = emACL.checkIfEMEnabled(admin.userName)
marketingStatus = False
Data['marketingStatus'] = marketingStatus
Data['ftpTotal'] = website.package.ftpAccounts