Redesign cPanel restore plugin to match WHM workflow

Replace the 8-card category grid with a unified restore workflow:
- index.live.cgi: now serves as Step 1 (remote + snapshot selection)
- restore.live.cgi: Step 2 (Full Account/Selective mode toggle with
  type checkboxes, exclude paths, file browser), Step 3 (multi-type
  confirmation), Step 4 (multi-type execution via AdminBin)

Also update cPanel plugin icon from gniza.svg source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shuki
2026-03-05 03:13:53 +02:00
parent 9dc427a1e6
commit 4408917aec
4 changed files with 735 additions and 432 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 685 B

View File

@@ -1,5 +1,5 @@
#!/usr/local/cpanel/3rdparty/bin/perl #!/usr/local/cpanel/3rdparty/bin/perl
# gniza cPanel Plugin — Restore Category Grid # gniza cPanel Plugin — Step 1: Select Remote + Snapshot
use strict; use strict;
use warnings; use warnings;
@@ -27,6 +27,7 @@ my $remotes_raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_
my @remotes = grep { $_ ne '' } split /\n/, $remotes_raw; my @remotes = grep { $_ ne '' } split /\n/, $remotes_raw;
print GnizaCPanel::UI::page_header('GNIZA Backups'); print GnizaCPanel::UI::page_header('GNIZA Backups');
print GnizaCPanel::UI::render_flash();
if (!@remotes) { if (!@remotes) {
print qq{<div class="alert alert-info mb-4">No backup remotes are available for restore. Please contact your server administrator.</div>\n}; print qq{<div class="alert alert-info mb-4">No backup remotes are available for restore. Please contact your server administrator.</div>\n};
@@ -36,76 +37,111 @@ if (!@remotes) {
exit; exit;
} }
# Category cards print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
my @categories = ( print qq{<h2 class="card-title text-sm">Select Backup Source</h2>\n};
{
type => 'account',
label => 'Full Backup',
desc => 'Restore entire account from backup',
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/></svg>',
},
{
type => 'files',
label => 'Home Directory',
desc => 'Restore files and folders',
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>',
},
{
type => 'database',
label => 'Databases',
desc => 'Restore MySQL databases',
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/></svg>',
},
{
type => 'dbusers',
label => 'Database Users',
desc => 'Restore database users and grants',
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/></svg>',
},
{
type => 'cron',
label => 'Cron Jobs',
desc => 'Restore scheduled tasks',
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
},
{
type => 'domains',
label => 'Domains',
desc => 'Restore domain and DNS configuration',
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"/></svg>',
},
{
type => 'ssl',
label => 'SSL Certificates',
desc => 'Restore SSL certificates',
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"/></svg>',
},
{
type => 'mailbox',
label => 'Email Accounts',
desc => 'Restore mailboxes and email',
icon => '<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"/></svg>',
},
);
print qq{<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">\n}; # Remote dropdown
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
for my $cat (@categories) { print qq{ <label class="w-36 font-medium text-sm" for="remote">Remote</label>\n};
my $type = GnizaCPanel::UI::esc($cat->{type}); print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="remote" name="remote" onchange="gnizaLoadSnapshots()">\n};
my $label = GnizaCPanel::UI::esc($cat->{label}); print qq{ <option value="">-- Select remote --</option>\n};
my $desc = GnizaCPanel::UI::esc($cat->{desc}); for my $r (@remotes) {
my $icon = $cat->{icon}; # Already safe SVG my $esc = GnizaCPanel::UI::esc($r);
print qq{ <option value="$esc">$esc</option>\n};
print qq{<a href="restore.live.cgi?type=$type" class="card bg-white shadow-sm border border-base-300 hover:shadow-md transition-shadow no-underline">\n}; }
print qq{<div class="card-body items-center text-center">\n}; print qq{ </select>\n};
print qq{ <div class="text-primary mb-2">$icon</div>\n};
print qq{ <h2 class="card-title text-sm">$label</h2>\n};
print qq{ <p class="text-xs text-base-content/60">$desc</p>\n};
print qq{</div>\n}; print qq{</div>\n};
print qq{</a>\n};
# Snapshot dropdown (populated via AJAX)
print qq{<div class="flex items-center gap-3 mb-2.5">\n};
print qq{ <label class="w-36 font-medium text-sm" for="timestamp">Snapshot</label>\n};
print qq{ <select class="select select-bordered select-sm w-full max-w-xs" id="timestamp" name="timestamp" disabled>\n};
print qq{ <option value="">-- Select remote first --</option>\n};
print qq{ </select>\n};
print qq{</div>\n};
print qq{</div>\n</div>\n};
print qq{<div class="flex items-center gap-2">\n};
print qq{ <button type="button" class="btn btn-primary btn-sm" id="next-btn" disabled onclick="gnizaGoNext()">Next</button>\n};
print qq{</div>\n};
# JavaScript for snapshot loading and navigation
print <<"END_JS";
<script>
function gnizaLoadSnapshots() {
var remote = document.getElementById('remote').value;
var sel = document.getElementById('timestamp');
var btn = document.getElementById('next-btn');
if (!remote) {
_setSelectPlaceholder(sel, '-- Select remote first --');
sel.disabled = true;
btn.disabled = true;
return;
} }
print qq{</div>\n}; _setSelectPlaceholder(sel, 'Loading...');
sel.disabled = true;
btn.disabled = true;
var url = 'restore.live.cgi?step=fetch_snapshots&remote=' + encodeURIComponent(remote);
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.error) {
_setSelectPlaceholder(sel, 'Error: ' + data.error);
} else if (data.snapshots && data.snapshots.length > 0) {
_populateSelect(sel, data.snapshots);
sel.disabled = false;
btn.disabled = false;
} else {
_setSelectPlaceholder(sel, 'No snapshots found');
}
} catch(e) {
_setSelectPlaceholder(sel, 'Failed to parse response');
}
} else {
_setSelectPlaceholder(sel, 'Request failed');
}
};
xhr.send();
}
function _setSelectPlaceholder(sel, text) {
while (sel.options.length) sel.remove(0);
var opt = document.createElement('option');
opt.value = '';
opt.textContent = text;
sel.appendChild(opt);
}
function _populateSelect(sel, values) {
while (sel.options.length) sel.remove(0);
for (var i = 0; i < values.length; i++) {
var opt = document.createElement('option');
opt.value = values[i];
opt.textContent = values[i];
sel.appendChild(opt);
}
}
function gnizaGoNext() {
var remote = document.getElementById('remote').value;
var timestamp = document.getElementById('timestamp').value;
if (!remote || !timestamp) return;
var url = 'restore.live.cgi?step=2'
+ '&remote=' + encodeURIComponent(remote)
+ '&timestamp=' + encodeURIComponent(timestamp);
window.location.href = url;
}
</script>
END_JS
print GnizaCPanel::UI::page_footer(); print GnizaCPanel::UI::page_footer();
print $cpanel->footer(); print $cpanel->footer();

File diff suppressed because it is too large Load Diff