00d44256b4
Rebrand all 17 sub-extensions from mokowaas to mokosuite naming, including component, plugins, modules, task plugins, and webservices. Updates package manifest, workflows, docs, wiki, and issue templates. Adds new plg_system_mokosuite_license extension.
307 lines
14 KiB
PHP
307 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* @package MokoSuite
|
|
* @subpackage com_mokosuite
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\CMS\Session\Session;
|
|
|
|
$opts = $this->options;
|
|
$preview = $this->preview;
|
|
$nginx = $this->nginxPreview;
|
|
$current = $this->currentHtaccess;
|
|
$token = Session::getFormToken();
|
|
$saveUrl = Route::_('index.php?option=com_mokosuite&task=display.saveHtaccess&format=json');
|
|
$genUrl = Route::_('index.php?option=com_mokosuite&task=display.generateHtaccess&format=json');
|
|
|
|
// Helper for toggle switch
|
|
$sw = function($name, $label, $desc = '') use ($opts) {
|
|
$checked = !empty($opts[$name]) ? 'checked' : '';
|
|
echo '<div class="d-flex justify-content-between align-items-center py-2 border-bottom">';
|
|
echo '<div><strong>' . htmlspecialchars($label) . '</strong>';
|
|
if ($desc) echo '<br><small class="text-muted">' . htmlspecialchars($desc) . '</small>';
|
|
echo '</div>';
|
|
echo '<div class="form-check form-switch">';
|
|
echo '<input type="checkbox" class="form-check-input htaccess-opt" name="' . $name . '" id="htopt-' . $name . '" ' . $checked . '>';
|
|
echo '</div></div>';
|
|
};
|
|
?>
|
|
|
|
<div id="mokosuite-htaccess">
|
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
<li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-htaccess" role="tab">.htaccess</a></li>
|
|
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-nginx" role="tab">NginX</a></li>
|
|
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-current" role="tab">Current File</a></li>
|
|
</ul>
|
|
|
|
<div class="tab-content">
|
|
<!-- .htaccess Tab -->
|
|
<div class="tab-pane fade show active" id="tab-htaccess" role="tabpanel">
|
|
<div class="row">
|
|
<!-- Left: Options -->
|
|
<div class="col-12 col-xl-6">
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header"><strong><span class="icon-shield-alt"></span> Security</strong></div>
|
|
<div class="card-body">
|
|
<?php $sw('disable_directory_listing', 'Disable Directory Listing', 'Options -Indexes'); ?>
|
|
<?php $sw('block_sensitive_files', 'Block Sensitive Files', 'htaccess.txt, configuration.php-dist, etc.'); ?>
|
|
<?php $sw('block_php_in_uploads', 'Block PHP in Uploads', 'Prevent .php in images/, media/, tmp/'); ?>
|
|
<?php $sw('disable_server_signature', 'Hide Server Signature', 'ServerSignature Off, remove X-Powered-By'); ?>
|
|
<?php $sw('prevent_clickjacking', 'Clickjacking Protection', 'X-Frame-Options: SAMEORIGIN'); ?>
|
|
<?php $sw('prevent_mime_sniffing', 'MIME Sniffing Prevention', 'X-Content-Type-Options: nosniff'); ?>
|
|
<?php $sw('xss_protection', 'XSS Protection Header', 'X-XSS-Protection: 1; mode=block'); ?>
|
|
<?php $sw('disable_trace_track', 'Disable TRACE/TRACK', 'Block HTTP TRACE and TRACK methods'); ?>
|
|
|
|
<div class="py-2 border-bottom">
|
|
<label class="form-label fw-bold" for="htopt-referrer_policy">Referrer Policy</label>
|
|
<select class="form-select form-select-sm htaccess-opt" name="referrer_policy" id="htopt-referrer_policy">
|
|
<option value="off" <?php echo ($opts['referrer_policy'] ?? '') === 'off' ? 'selected' : ''; ?>>Off</option>
|
|
<option value="no-referrer" <?php echo ($opts['referrer_policy'] ?? '') === 'no-referrer' ? 'selected' : ''; ?>>no-referrer</option>
|
|
<option value="same-origin" <?php echo ($opts['referrer_policy'] ?? '') === 'same-origin' ? 'selected' : ''; ?>>same-origin</option>
|
|
<option value="strict-origin-when-cross-origin" <?php echo ($opts['referrer_policy'] ?? '') === 'strict-origin-when-cross-origin' ? 'selected' : ''; ?>>strict-origin-when-cross-origin</option>
|
|
</select>
|
|
</div>
|
|
|
|
<?php $sw('hsts_enabled', 'HSTS (Force HTTPS)', 'Strict-Transport-Security header'); ?>
|
|
<div class="ps-4 <?php echo empty($opts['hsts_enabled']) ? 'd-none' : ''; ?>" id="hsts-options">
|
|
<div class="row g-2 py-2">
|
|
<div class="col-6">
|
|
<label class="form-label small">Max Age (seconds)</label>
|
|
<input type="number" class="form-control form-control-sm htaccess-opt" name="hsts_max_age" value="<?php echo (int) ($opts['hsts_max_age'] ?? 31536000); ?>">
|
|
</div>
|
|
<div class="col-6 d-flex align-items-end">
|
|
<div class="form-check">
|
|
<input type="checkbox" class="form-check-input htaccess-opt" name="hsts_subdomains" id="htopt-hsts_sub" <?php echo !empty($opts['hsts_subdomains']) ? 'checked' : ''; ?>>
|
|
<label class="form-check-label small" for="htopt-hsts_sub">Include Subdomains</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php $sw('csp_enabled', 'Content Security Policy', 'CSP header'); ?>
|
|
<div class="ps-4 <?php echo empty($opts['csp_enabled']) ? 'd-none' : ''; ?>" id="csp-options">
|
|
<textarea class="form-control form-control-sm htaccess-opt mt-1" name="csp_value" rows="2" placeholder="default-src 'self'; script-src 'self' 'unsafe-inline'"><?php echo htmlspecialchars($opts['csp_value'] ?? ''); ?></textarea>
|
|
</div>
|
|
|
|
<?php $sw('permissions_policy', 'Permissions Policy', 'Camera, microphone, geolocation controls'); ?>
|
|
<div class="ps-4 <?php echo empty($opts['permissions_policy']) ? 'd-none' : ''; ?>" id="perms-options">
|
|
<textarea class="form-control form-control-sm htaccess-opt mt-1" name="permissions_value" rows="2" placeholder="camera=(), microphone=(), geolocation=()"><?php echo htmlspecialchars($opts['permissions_value'] ?? ''); ?></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header"><strong><span class="icon-bolt"></span> Performance</strong></div>
|
|
<div class="card-body">
|
|
<?php $sw('enable_gzip', 'GZip Compression', 'Compress CSS, JS, HTML, XML, JSON'); ?>
|
|
<?php $sw('enable_expires', 'Browser Caching', 'Set expiration headers for static files'); ?>
|
|
<div class="ps-4 <?php echo empty($opts['enable_expires']) ? 'd-none' : ''; ?>" id="expires-options">
|
|
<div class="row g-2 py-2">
|
|
<div class="col-4">
|
|
<label class="form-label small">HTML (sec)</label>
|
|
<input type="number" class="form-control form-control-sm htaccess-opt" name="expires_html" value="<?php echo (int) ($opts['expires_html'] ?? 3600); ?>">
|
|
</div>
|
|
<div class="col-4">
|
|
<label class="form-label small">CSS/JS (sec)</label>
|
|
<input type="number" class="form-control form-control-sm htaccess-opt" name="expires_css_js" value="<?php echo (int) ($opts['expires_css_js'] ?? 2592000); ?>">
|
|
</div>
|
|
<div class="col-4">
|
|
<label class="form-label small">Images (sec)</label>
|
|
<input type="number" class="form-control form-control-sm htaccess-opt" name="expires_images" value="<?php echo (int) ($opts['expires_images'] ?? 31536000); ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php $sw('etag_control', 'Disable ETags', 'For load-balanced environments'); ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header"><strong><span class="icon-search"></span> SEO / Redirects</strong></div>
|
|
<div class="card-body">
|
|
<div class="py-2 border-bottom">
|
|
<label class="form-label fw-bold">WWW Redirect</label>
|
|
<select class="form-select form-select-sm htaccess-opt" name="www_redirect">
|
|
<option value="off" <?php echo ($opts['www_redirect'] ?? 'off') === 'off' ? 'selected' : ''; ?>>Off</option>
|
|
<option value="www" <?php echo ($opts['www_redirect'] ?? '') === 'www' ? 'selected' : ''; ?>>Force www</option>
|
|
<option value="non-www" <?php echo ($opts['www_redirect'] ?? '') === 'non-www' ? 'selected' : ''; ?>>Force non-www</option>
|
|
</select>
|
|
</div>
|
|
<?php $sw('redirect_index_php', 'Redirect /index.php to /', 'SEO-friendly root redirect'); ?>
|
|
<?php $sw('force_trailing_slash', 'Force Trailing Slash', 'Append / to URLs without file extension'); ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header"><strong><span class="icon-code"></span> Custom Rules</strong></div>
|
|
<div class="card-body">
|
|
<textarea class="form-control htaccess-opt" name="custom_rules" rows="4" placeholder="# Add custom Apache directives here"><?php echo htmlspecialchars($opts['custom_rules'] ?? ''); ?></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: Preview -->
|
|
<div class="col-12 col-xl-6">
|
|
<div class="card mb-3 sticky-top" style="top:1rem">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<strong>Preview</strong>
|
|
<span class="badge bg-secondary" id="htaccess-line-count"><?php echo substr_count($preview, "\n"); ?> lines</span>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<textarea id="htaccess-preview" class="form-control font-monospace border-0" rows="30" readonly style="font-size:0.8rem;resize:none"><?php echo htmlspecialchars($preview); ?></textarea>
|
|
</div>
|
|
<div class="card-footer d-flex gap-2">
|
|
<button type="button" class="btn btn-primary" id="htaccess-save"
|
|
data-url="<?php echo $saveUrl; ?>" data-token="<?php echo $token; ?>">
|
|
<span class="icon-save"></span> Save to .htaccess
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="htaccess-download">
|
|
<span class="icon-download"></span> Download
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NginX Tab -->
|
|
<div class="tab-pane fade" id="tab-nginx" role="tabpanel">
|
|
<div class="card">
|
|
<div class="card-header"><strong>NginX Configuration Snippet</strong></div>
|
|
<div class="card-body p-0">
|
|
<textarea id="nginx-preview" class="form-control font-monospace border-0" rows="25" readonly style="font-size:0.8rem;resize:none"><?php echo htmlspecialchars($nginx); ?></textarea>
|
|
</div>
|
|
<div class="card-footer">
|
|
<button type="button" class="btn btn-outline-secondary" id="nginx-download">
|
|
<span class="icon-download"></span> Download NginX Config
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Current File Tab -->
|
|
<div class="tab-pane fade" id="tab-current" role="tabpanel">
|
|
<div class="card">
|
|
<div class="card-header"><strong>Current .htaccess on Disk</strong></div>
|
|
<div class="card-body p-0">
|
|
<textarea class="form-control font-monospace border-0" rows="25" readonly style="font-size:0.8rem;resize:none"><?php echo htmlspecialchars($current); ?></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var saveBtn = document.getElementById('htaccess-save');
|
|
var preview = document.getElementById('htaccess-preview');
|
|
var lineCount = document.getElementById('htaccess-line-count');
|
|
|
|
// Toggle sub-option visibility
|
|
document.getElementById('htopt-hsts_enabled').addEventListener('change', function() {
|
|
document.getElementById('hsts-options').classList.toggle('d-none', !this.checked);
|
|
});
|
|
document.getElementById('htopt-csp_enabled').addEventListener('change', function() {
|
|
document.getElementById('csp-options').classList.toggle('d-none', !this.checked);
|
|
});
|
|
document.getElementById('htopt-permissions_policy').addEventListener('change', function() {
|
|
document.getElementById('perms-options').classList.toggle('d-none', !this.checked);
|
|
});
|
|
document.getElementById('htopt-enable_expires') && document.getElementById('htopt-enable_expires').addEventListener('change', function() {
|
|
document.getElementById('expires-options').classList.toggle('d-none', !this.checked);
|
|
});
|
|
|
|
// Regenerate preview on any option change
|
|
document.querySelectorAll('.htaccess-opt').forEach(function(el) {
|
|
el.addEventListener('change', regeneratePreview);
|
|
el.addEventListener('input', regeneratePreview);
|
|
});
|
|
|
|
function collectOptions() {
|
|
var opts = {};
|
|
document.querySelectorAll('.htaccess-opt').forEach(function(el) {
|
|
if (el.type === 'checkbox') {
|
|
opts[el.name] = el.checked ? 1 : 0;
|
|
} else {
|
|
opts[el.name] = el.value;
|
|
}
|
|
});
|
|
return opts;
|
|
}
|
|
|
|
var debounceTimer;
|
|
function regeneratePreview() {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(function() {
|
|
var fd = new FormData();
|
|
var opts = collectOptions();
|
|
for (var k in opts) fd.append(k, opts[k]);
|
|
fd.append('<?php echo $token; ?>', '1');
|
|
|
|
fetch('<?php echo $genUrl; ?>', {
|
|
method: 'POST', body: fd,
|
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
if (d.htaccess) {
|
|
preview.value = d.htaccess;
|
|
lineCount.textContent = d.htaccess.split('\n').length + ' lines';
|
|
}
|
|
if (d.nginx) {
|
|
document.getElementById('nginx-preview').value = d.nginx;
|
|
}
|
|
});
|
|
}, 300);
|
|
}
|
|
|
|
// Save to disk
|
|
saveBtn.addEventListener('click', function() {
|
|
if (!confirm('This will overwrite your current .htaccess file. A backup will be created at .htaccess.mokosuite.bak. Continue?')) return;
|
|
var btn = this;
|
|
btn.disabled = true;
|
|
|
|
var fd = new FormData();
|
|
fd.append('content', preview.value);
|
|
var opts = collectOptions();
|
|
for (var k in opts) fd.append('opt_' + k, opts[k]);
|
|
fd.append('<?php echo $token; ?>', '1');
|
|
|
|
fetch(btn.dataset.url, {
|
|
method: 'POST', body: fd,
|
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
if (d.success) Joomla.renderMessages({message: [d.message]});
|
|
else Joomla.renderMessages({error: [d.message]});
|
|
})
|
|
.catch(function() { Joomla.renderMessages({error: ['Network error']}); })
|
|
.finally(function() { btn.disabled = false; });
|
|
});
|
|
|
|
// Download buttons
|
|
function downloadText(content, filename) {
|
|
var blob = new Blob([content], {type: 'text/plain'});
|
|
var a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
}
|
|
|
|
document.getElementById('htaccess-download').addEventListener('click', function() {
|
|
downloadText(preview.value, '.htaccess');
|
|
});
|
|
document.getElementById('nginx-download').addEventListener('click', function() {
|
|
downloadText(document.getElementById('nginx-preview').value, 'mokosuite-nginx.conf');
|
|
});
|
|
});
|
|
</script>
|