ef31713029
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Add content snapshot system for lightweight article/category/module versioning independent of full backups. Snapshots store as JSON files with replace or merge restore modes, wrapped in DB transactions. - SnapshotEngine: dumps articles, categories, modules + related tables (workflow_associations, tag maps, frontpage) to JSON - SnapshotRestoreEngine: replace (clean slate) or merge (upsert) mode - Full MVC: controller, models, view, template with create/restore modals - New ACL permission: mokosuitebackup.snapshot.manage - Submenu entry with camera icon, upgrade SQL for snapshots table Improve full-site restore UI with confirmation modal offering options for files, database, preserve config, and encryption password. Config improvements: - WebcronSecretField: CSPRNG generator, strength meter, rejects weak patterns (password, admin, secret), enforces min 16 chars - IpWhitelistField: table-based management, current IP detection with one-click "Add my IP" button - Default profile shows "Title (#ID)" format - Default backup dir uses [DEFAULT_DIR] placeholder - Install script generates random 32-char webcron secret - Dashboard quick actions: full-width dropdown with button below
185 lines
5.3 KiB
PHP
185 lines
5.3 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteBackup
|
|
* @subpackage com_mokosuitebackup
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*
|
|
* Custom field for the webcron secret word.
|
|
* Generates a random default and validates minimum strength.
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Form\FormField;
|
|
use Joomla\CMS\Language\Text;
|
|
|
|
class WebcronSecretField extends FormField
|
|
{
|
|
protected $type = 'WebcronSecret';
|
|
|
|
private const MIN_LENGTH = 16;
|
|
private const WEAK_PATTERNS = [
|
|
'password', 'secret', '123456', 'admin', 'backup',
|
|
'test', 'webcron', 'qwerty', 'letmein', 'welcome',
|
|
];
|
|
|
|
protected function getInput(): string
|
|
{
|
|
$value = $this->value ?? '';
|
|
$id = $this->id;
|
|
$name = $this->name;
|
|
$maxLength = (int) ($this->element['maxlength'] ?? 64);
|
|
|
|
$strengthHtml = '';
|
|
$strengthClass = 'text-muted';
|
|
$strengthText = Text::_('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_NONE');
|
|
|
|
if (!empty($value)) {
|
|
$strength = $this->evaluateStrength($value);
|
|
$strengthClass = $strength['class'];
|
|
$strengthText = $strength['label'];
|
|
}
|
|
|
|
$html = '<div class="input-group">';
|
|
$html .= '<input type="text" name="' . htmlspecialchars($name) . '" id="' . htmlspecialchars($id) . '"'
|
|
. ' value="' . htmlspecialchars($value) . '"'
|
|
. ' class="form-control" maxlength="' . $maxLength . '"'
|
|
. ' autocomplete="off" spellcheck="false"'
|
|
. ' onchange="mokoWebcronCheckStrength(this)"'
|
|
. ' onkeyup="mokoWebcronCheckStrength(this)">';
|
|
$html .= '<button type="button" class="btn btn-outline-secondary" onclick="mokoWebcronGenerate(\'' . htmlspecialchars($id) . '\')"'
|
|
. ' title="' . Text::_('COM_MOKOJOOMBACKUP_WEBCRON_GENERATE') . '">'
|
|
. '<span class="icon-refresh" aria-hidden="true"></span> '
|
|
. Text::_('COM_MOKOJOOMBACKUP_WEBCRON_GENERATE')
|
|
. '</button>';
|
|
$html .= '</div>';
|
|
$html .= '<div id="' . htmlspecialchars($id) . '-strength" class="small mt-1 ' . $strengthClass . '">'
|
|
. $strengthText . '</div>';
|
|
|
|
$html .= $this->getScript();
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function evaluateStrength(string $value): array
|
|
{
|
|
$len = strlen($value);
|
|
|
|
// Check weak patterns
|
|
$lower = strtolower($value);
|
|
foreach (self::WEAK_PATTERNS as $weak) {
|
|
if (str_contains($lower, $weak)) {
|
|
return [
|
|
'class' => 'text-danger fw-bold',
|
|
'label' => Text::_('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_WEAK'),
|
|
];
|
|
}
|
|
}
|
|
|
|
if ($len < self::MIN_LENGTH) {
|
|
return [
|
|
'class' => 'text-danger',
|
|
'label' => Text::sprintf('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_SHORT', self::MIN_LENGTH),
|
|
];
|
|
}
|
|
|
|
$hasUpper = preg_match('/[A-Z]/', $value);
|
|
$hasLower = preg_match('/[a-z]/', $value);
|
|
$hasDigit = preg_match('/[0-9]/', $value);
|
|
|
|
if ($hasUpper && $hasLower && $hasDigit && $len >= 32) {
|
|
return [
|
|
'class' => 'text-success',
|
|
'label' => Text::_('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_STRONG'),
|
|
];
|
|
}
|
|
|
|
if ($hasUpper && $hasLower && $hasDigit) {
|
|
return [
|
|
'class' => 'text-warning',
|
|
'label' => Text::_('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_OK'),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'class' => 'text-danger',
|
|
'label' => Text::_('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_WEAK'),
|
|
];
|
|
}
|
|
|
|
private function getScript(): string
|
|
{
|
|
$minLen = self::MIN_LENGTH;
|
|
$weakJson = json_encode(self::WEAK_PATTERNS);
|
|
$labelNone = Text::_('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_NONE');
|
|
$labelShort = json_encode(Text::sprintf('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_SHORT', $minLen));
|
|
$labelWeak = json_encode(Text::_('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_WEAK'));
|
|
$labelOk = json_encode(Text::_('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_OK'));
|
|
$labelStrong = json_encode(Text::_('COM_MOKOJOOMBACKUP_WEBCRON_STRENGTH_STRONG'));
|
|
|
|
return <<<JS
|
|
<script>
|
|
function mokoWebcronGenerate(fieldId) {
|
|
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
var result = '';
|
|
var arr = new Uint8Array(32);
|
|
(window.crypto || window.msCrypto).getRandomValues(arr);
|
|
for (var i = 0; i < 32; i++) {
|
|
result += chars.charAt(arr[i] % chars.length);
|
|
}
|
|
var field = document.getElementById(fieldId);
|
|
field.value = result;
|
|
mokoWebcronCheckStrength(field);
|
|
}
|
|
|
|
function mokoWebcronCheckStrength(field) {
|
|
var val = field.value;
|
|
var el = document.getElementById(field.id + '-strength');
|
|
var weak = {$weakJson};
|
|
var lower = val.toLowerCase();
|
|
|
|
if (!val) {
|
|
el.className = 'small mt-1 text-muted';
|
|
el.textContent = '{$labelNone}';
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < weak.length; i++) {
|
|
if (lower.indexOf(weak[i]) !== -1) {
|
|
el.className = 'small mt-1 text-danger fw-bold';
|
|
el.textContent = {$labelWeak};
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (val.length < {$minLen}) {
|
|
el.className = 'small mt-1 text-danger';
|
|
el.textContent = {$labelShort};
|
|
return;
|
|
}
|
|
|
|
var hasUpper = /[A-Z]/.test(val);
|
|
var hasLower = /[a-z]/.test(val);
|
|
var hasDigit = /[0-9]/.test(val);
|
|
|
|
if (hasUpper && hasLower && hasDigit && val.length >= 32) {
|
|
el.className = 'small mt-1 text-success';
|
|
el.textContent = {$labelStrong};
|
|
} else if (hasUpper && hasLower && hasDigit) {
|
|
el.className = 'small mt-1 text-warning';
|
|
el.textContent = {$labelOk};
|
|
} else {
|
|
el.className = 'small mt-1 text-danger';
|
|
el.textContent = {$labelWeak};
|
|
}
|
|
}
|
|
</script>
|
|
JS;
|
|
}
|
|
}
|