8258ed804a
Standards Compliance / Secret Scanning (push) Successful in 3s
Standards Compliance / License Header Validation (push) Successful in 4s
Standards Compliance / Repository Structure Validation (push) Successful in 5s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Version Consistency Check (push) Successful in 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 2s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 2s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 4s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 3s
Standards Compliance / Code Duplication Detection (push) Successful in 4s
Standards Compliance / Dead Code Detection (push) Successful in 3s
Standards Compliance / File Size Limits (push) Successful in 2s
CodeQL Security Scanning / Analyze (javascript) (push) Failing after 1m9s
Standards Compliance / Binary File Detection (push) Successful in 4s
CodeQL Security Scanning / Analyze (actions) (push) Failing after 1m11s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 5s
Standards Compliance / Broken Link Detection (push) Successful in 5s
Standards Compliance / Unused Dependencies Check (push) Successful in 7s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Enterprise Readiness Check (push) Successful in 3s
Standards Compliance / Repository Health Check (push) Successful in 4s
Standards Compliance / Terraform Configuration Validation (push) Successful in 6s
CodeQL Security Scanning / Security Scan Summary (push) Successful in 1s
Standards Compliance / Compliance Summary (push) Successful in 1s
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s
Auto-Update SHA Hash / Update SHA-256 Hash in updates.xml (release) Failing after 5s
All files renamed from mokocassiopeia to mokoonyx. Update server points to MokoOnyx repo. Bridge migration removed (clean standalone template). Version reset to 01.00.00. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
415 lines
16 KiB
PHP
415 lines
16 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
|
|
This file is part of a Moko Consulting project.
|
|
|
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Component\ComponentHelper;
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\HTML\HTMLHelper;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\CMS\Uri\Uri;
|
|
|
|
/**
|
|
* @var \Joomla\CMS\Document\HtmlDocument $this
|
|
* @var \Joomla\Registry\Registry $this->params
|
|
* @var string $this->language
|
|
* @var string $this->direction
|
|
*/
|
|
|
|
$app = Factory::getApplication();
|
|
$doc = Factory::getDocument();
|
|
$wa = $doc->getWebAssetManager();
|
|
$params = $this->params ?: $app->getTemplate(true)->params;
|
|
$direction = $this->direction ?: 'ltr';
|
|
|
|
// Register the template's asset manifest (not auto-loaded in offline context)
|
|
$manifestPath = JPATH_ROOT . '/media/templates/site/' . $this->template . '/joomla.asset.json';
|
|
if (is_file($manifestPath)) {
|
|
$wa->getRegistry()->addRegistryFile($manifestPath);
|
|
}
|
|
|
|
// Load language files (not auto-loaded in offline context)
|
|
$lang = Factory::getLanguage();
|
|
$lang->load('tpl_' . $this->template, JPATH_ROOT . '/templates/' . $this->template);
|
|
$lang->load('tpl_' . $this->template, JPATH_ROOT);
|
|
$lang->load('com_users', JPATH_ROOT);
|
|
$lang->load('com_users', JPATH_ROOT . '/components/com_users');
|
|
$lang->load('', JPATH_ROOT);
|
|
|
|
/* -----------------------
|
|
Load assets via WebAssetManager (matches index.php pattern)
|
|
------------------------ */
|
|
$params_developmentmode = (bool) $params->get('developmentmode', false) || (bool) $app->get('debug', false);
|
|
$suffix = $params_developmentmode ? '' : '.min';
|
|
|
|
// Core template CSS + offline overlay CSS
|
|
$wa->useStyle('template.base' . $suffix);
|
|
$wa->useStyle('template.offline' . $suffix);
|
|
|
|
// Osaka font
|
|
$wa->useStyle('template.font.osaka');
|
|
|
|
// Font Awesome 7 Free
|
|
$wa->useStyle('vendor.fa7free.all' . $suffix);
|
|
|
|
// Theme palettes
|
|
$wa->useStyle('template.light.standard' . $suffix);
|
|
$wa->useStyle('template.dark.standard' . $suffix);
|
|
|
|
// Custom palettes (if selected and files exist)
|
|
$params_LightColorName = (string) $params->get('colorLightName', 'standard');
|
|
$params_DarkColorName = (string) $params->get('colorDarkName', 'standard');
|
|
if ($params_LightColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokoonyx/css/theme/light.custom.css'))
|
|
{
|
|
$wa->useStyle('template.light.custom' . $suffix);
|
|
}
|
|
if ($params_DarkColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokoonyx/css/theme/dark.custom.css'))
|
|
{
|
|
$wa->useStyle('template.dark.custom' . $suffix);
|
|
}
|
|
|
|
// User overrides (loaded last)
|
|
$wa->useStyle('template.user');
|
|
|
|
// Accessibility high-contrast stylesheet
|
|
$wa->useStyle('template.a11y-high-contrast');
|
|
|
|
// Template JS (theme switcher, a11y toolbar, var-copy, etc.)
|
|
if ($params_developmentmode) {
|
|
$wa->useScript('template.js');
|
|
} else {
|
|
$wa->useScript('template.js.min');
|
|
}
|
|
$wa->useScript('user.js');
|
|
|
|
// Bootstrap CSS + JS (accordion, responsive grid, utilities)
|
|
try {
|
|
$wa->useStyle('bootstrap.css');
|
|
} catch (\Exception $e) {
|
|
// Fallback: load via HTMLHelper
|
|
HTMLHelper::_('bootstrap.loadCss', true, $doc);
|
|
}
|
|
HTMLHelper::_('bootstrap.framework');
|
|
|
|
/* -----------------------
|
|
Title + Meta
|
|
------------------------ */
|
|
$sitename = (string) $app->get('sitename');
|
|
$baseTitle = Text::_('JGLOBAL_OFFLINE') ?: 'Offline';
|
|
$snSetting = (int) $app->get('sitename_pagetitles', 0);
|
|
|
|
if ($snSetting === 1) {
|
|
$doc->setTitle(Text::sprintf('JPAGETITLE', $sitename, $baseTitle));
|
|
} elseif ($snSetting === 2) {
|
|
$doc->setTitle(Text::sprintf('JPAGETITLE', $baseTitle, $sitename));
|
|
} else {
|
|
$doc->setTitle($baseTitle);
|
|
}
|
|
$doc->setMetaData('robots', 'noindex, nofollow');
|
|
|
|
/* -----------------------
|
|
Offline content from Global Config
|
|
------------------------ */
|
|
$displayOfflineMessage = (int) $app->get('display_offline_message', 1);
|
|
$offlineMessage = trim((string) $app->get('offline_message', ''));
|
|
|
|
/* -----------------------
|
|
Offline image from Joomla Global Config (System > Global Configuration > Site > Offline Image)
|
|
Used as the full-viewport background image.
|
|
------------------------ */
|
|
$offlineImage = trim((string) $app->get('offline_image', ''));
|
|
$bgStyle = '';
|
|
if ($offlineImage !== '') {
|
|
$bgStyle = 'background-image: url(\'' . htmlspecialchars(Uri::root(false) . $offlineImage, ENT_QUOTES, 'UTF-8') . '\');';
|
|
}
|
|
|
|
/* -----------------------
|
|
Brand: logo from template params OR siteTitle
|
|
------------------------ */
|
|
$brandHtml = '';
|
|
$logoFile = (string) $params->get('logoFile');
|
|
|
|
if ($logoFile !== '') {
|
|
$brandHtml = HTMLHelper::_(
|
|
'image',
|
|
Uri::root(false) . htmlspecialchars($logoFile, ENT_QUOTES, 'UTF-8'),
|
|
$sitename,
|
|
['class' => 'logo d-inline-block', 'loading' => 'eager', 'decoding' => 'async'],
|
|
false,
|
|
0
|
|
);
|
|
} else {
|
|
$siteTitle = $params->get('siteTitle', 'MokoOnyx');
|
|
$brandHtml = '<span class="site-title" title="' . htmlspecialchars($sitename, ENT_QUOTES, 'UTF-8') . '">'
|
|
. htmlspecialchars($siteTitle, ENT_COMPAT, 'UTF-8')
|
|
. '</span>';
|
|
}
|
|
|
|
$brandTagline = (string) ($params->get('brand_tagline') ?: $params->get('siteDescription') ?: '');
|
|
$showTagline = (int) $params->get('show_brand_tagline', 0);
|
|
|
|
// Favicon
|
|
$params_favicon_source = (string) $params->get('favicon_source', '');
|
|
$faviconHeadTags = '';
|
|
if ($params_favicon_source) {
|
|
require_once JPATH_ROOT . '/templates/' . $this->template . '/helper/favicon.php';
|
|
$faviconSourceAbs = JPATH_ROOT . '/' . ltrim($params_favicon_source, '/');
|
|
$faviconOutputDir = JPATH_ROOT . '/images/favicons';
|
|
$faviconUrlBase = Uri::root(true) . '/images/favicons';
|
|
|
|
if (MokoFaviconHelper::generate($faviconSourceAbs, $faviconOutputDir)) {
|
|
$faviconHeadTags = MokoFaviconHelper::getHeadTags($faviconUrlBase);
|
|
}
|
|
}
|
|
|
|
// Theme params
|
|
$params_theme_enabled = (int) $params->get('theme_enabled', 1);
|
|
$params_theme_fab_enabled = (int) $params->get('theme_fab_enabled', 1);
|
|
$params_theme_fab_pos = 'br';
|
|
|
|
// Accessibility params
|
|
$params_a11y_toolbar = (int) $params->get('a11y_toolbar_enabled', 1);
|
|
$params_a11y_resize = (int) $params->get('a11y_text_resize', 1);
|
|
$params_a11y_invert = (int) $params->get('a11y_color_inversion', 1);
|
|
$params_a11y_contrast = (int) $params->get('a11y_high_contrast', 1);
|
|
$params_a11y_links = (int) $params->get('a11y_highlight_links', 1);
|
|
$params_a11y_font = (int) $params->get('a11y_readable_font', 1);
|
|
$params_a11y_animations = (int) $params->get('a11y_pause_animations', 1);
|
|
$params_a11y_pos = 'br';
|
|
|
|
// Analytics params
|
|
$params_googletagmanager = $params->get('googletagmanager', false);
|
|
$params_googletagmanagerid = $params->get('googletagmanagerid', null);
|
|
$params_googleanalytics = $params->get('googleanalytics', false);
|
|
$params_googleanalyticsid = $params->get('googleanalyticsid', null);
|
|
$params_googlesitekey = $params->get('googlesitekey', null);
|
|
|
|
if (!empty($params_googlesitekey)) {
|
|
$doc->setMetaData('google-site-verification', htmlspecialchars($params_googlesitekey, ENT_QUOTES, 'UTF-8'));
|
|
}
|
|
|
|
/* -----------------------
|
|
Login routes
|
|
------------------------ */
|
|
$action = Route::_('index.php', true);
|
|
$return = base64_encode(Uri::base());
|
|
$allowRegistration = (bool) ComponentHelper::getParams('com_users')->get('allowUserRegistration', 0);
|
|
|
|
if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
|
|
$resetUrl = \Joomla\Component\Users\Site\Helper\RouteHelper::getResetRoute();
|
|
$remindUrl = \Joomla\Component\Users\Site\Helper\RouteHelper::getRemindRoute();
|
|
$registrationUrl = \Joomla\Component\Users\Site\Helper\RouteHelper::getRegistrationRoute();
|
|
} else {
|
|
$resetUrl = Route::_('index.php?option=com_users&view=reset');
|
|
$remindUrl = Route::_('index.php?option=com_users&view=remind');
|
|
$registrationUrl = Route::_('index.php?option=com_users&view=registration');
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="<?php echo htmlspecialchars($this->language ?? 'en', ENT_QUOTES, 'UTF-8'); ?>" dir="<?php echo htmlspecialchars($direction, ENT_QUOTES, 'UTF-8'); ?>">
|
|
<head>
|
|
<jdoc:include type="head" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<?php if ($faviconHeadTags) : ?>
|
|
<?php echo $faviconHeadTags; ?>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($params_theme_enabled) : ?>
|
|
<script>
|
|
(function () {
|
|
try {
|
|
var stored = localStorage.getItem('theme');
|
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
var theme = stored ? stored : (prefersDark ? 'dark' : 'light');
|
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
document.documentElement.setAttribute('data-aria-theme', theme);
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
</head>
|
|
<body class="site moko-offline-wrap"
|
|
data-theme-fab-enabled="<?php echo $params_theme_fab_enabled ? '1' : '0'; ?>"
|
|
data-theme-fab-pos="<?php echo htmlspecialchars($params_theme_fab_pos, ENT_QUOTES, 'UTF-8'); ?>"
|
|
data-a11y-toolbar="<?php echo $params_a11y_toolbar ? '1' : '0'; ?>"
|
|
data-a11y-resize="<?php echo $params_a11y_resize ? '1' : '0'; ?>"
|
|
data-a11y-invert="<?php echo $params_a11y_invert ? '1' : '0'; ?>"
|
|
data-a11y-contrast="<?php echo $params_a11y_contrast ? '1' : '0'; ?>"
|
|
data-a11y-links="<?php echo $params_a11y_links ? '1' : '0'; ?>"
|
|
data-a11y-font="<?php echo $params_a11y_font ? '1' : '0'; ?>"
|
|
data-a11y-animations="<?php echo $params_a11y_animations ? '1' : '0'; ?>"
|
|
data-a11y-pos="<?php echo htmlspecialchars($params_a11y_pos, ENT_QUOTES, 'UTF-8'); ?>"
|
|
<?php if ($bgStyle) : ?>style="<?php echo $bgStyle; ?>"<?php endif; ?>>
|
|
<?php if (!empty($params_googletagmanager) && !empty($params_googletagmanagerid)) :
|
|
$gtmID = htmlspecialchars($params_googletagmanagerid, ENT_QUOTES, 'UTF-8'); ?>
|
|
<script>
|
|
(function(w,d,s,l,i){
|
|
w[l]=w[l]||[];
|
|
w[l].push({'gtm.start': new Date().getTime(), event:'gtm.js'});
|
|
var f=d.getElementsByTagName(s)[0],
|
|
j=d.createElement(s),
|
|
dl=l!='dataLayer'?'&l='+l:'';
|
|
j.async=true;
|
|
j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;
|
|
f.parentNode.insertBefore(j,f);
|
|
})(window,document,'script','dataLayer','<?php echo $gtmID; ?>');
|
|
</script>
|
|
<noscript>
|
|
<iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo $gtmID; ?>"
|
|
height="0" width="0" style="display:none;visibility:hidden"></iframe>
|
|
</noscript>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($params_googleanalytics) && !empty($params_googleanalyticsid)) :
|
|
$gaId = htmlspecialchars($params_googleanalyticsid, ENT_QUOTES, 'UTF-8'); ?>
|
|
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo $gaId; ?>"></script>
|
|
<script>
|
|
window.dataLayer = window.dataLayer || [];
|
|
function gtag(){dataLayer.push(arguments);}
|
|
gtag('js', new Date());
|
|
gtag('consent', 'default', {
|
|
'ad_storage': 'denied',
|
|
'analytics_storage': 'granted',
|
|
'ad_user_data': 'denied',
|
|
'ad_personalization': 'denied'
|
|
});
|
|
(function(id){
|
|
if (/^G-/.test(id)) {
|
|
gtag('config', id, { 'anonymize_ip': true });
|
|
} else if (/^UA-/.test(id)) {
|
|
gtag('config', id, { 'anonymize_ip': true });
|
|
} else {
|
|
console.warn('Unrecognized Google Analytics ID format:', id);
|
|
}
|
|
})('<?php echo $gaId; ?>');
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
<a class="skip-link" href="#maincontent"><?php echo Text::_('JSKIP_TO_CONTENT') ?: 'Skip to content'; ?></a>
|
|
|
|
<!-- Centered overlay card -->
|
|
<main id="maincontent">
|
|
<div class="moko-offline-card">
|
|
|
|
<!-- Logo -->
|
|
<a class="moko-offline-brand" href="<?php echo htmlspecialchars(Uri::base(), ENT_QUOTES, 'UTF-8'); ?>" aria-label="<?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?>">
|
|
<?php echo $brandHtml; ?>
|
|
<?php if ($showTagline && $brandTagline): ?>
|
|
<small class="brand-tagline"><?php echo htmlspecialchars($brandTagline, ENT_COMPAT, 'UTF-8'); ?></small>
|
|
<?php endif; ?>
|
|
</a>
|
|
|
|
<!-- Offline message: 0=hidden, 1=custom message, 2=system language string -->
|
|
<?php if ($displayOfflineMessage === 1 && $offlineMessage !== '') : ?>
|
|
<div class="moko-offline-message">
|
|
<p><?php echo $offlineMessage; ?></p>
|
|
</div>
|
|
<?php elseif ($displayOfflineMessage === 2) : ?>
|
|
<div class="moko-offline-message">
|
|
<p><?php echo Text::_('JOFFLINE_MESSAGE') ?: 'This site is down for maintenance.'; ?></p>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Offline module position -->
|
|
<?php if ($this->countModules('offline')) : ?>
|
|
<div class="moko-offline-modules">
|
|
<jdoc:include type="modules" name="offline" style="none" />
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Login accordion -->
|
|
<div class="accordion" id="offlineAccordion">
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header" id="headingLogin">
|
|
<button class="accordion-button collapsed" type="button"
|
|
data-bs-toggle="collapse" data-bs-target="#collapseLogin"
|
|
aria-expanded="false" aria-controls="collapseLogin">
|
|
<?php echo Text::_('JLOGIN'); ?>
|
|
</button>
|
|
</h2>
|
|
<div id="collapseLogin" class="accordion-collapse collapse" aria-labelledby="headingLogin" data-bs-parent="#offlineAccordion">
|
|
<div class="accordion-body">
|
|
<form action="<?php echo $action; ?>" method="post" class="form-validate">
|
|
<fieldset>
|
|
<legend class="visually-hidden"><?php echo Text::_('JLOGIN'); ?></legend>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label" for="username"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label>
|
|
<input class="form-control" type="text" name="username" id="username" autocomplete="username" required aria-required="true">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label" for="password"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
|
|
<input class="form-control" type="password" name="password" id="password" autocomplete="current-password" required aria-required="true">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label" for="secretkey"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label>
|
|
<input class="form-control" type="text" name="secretkey" id="secretkey" autocomplete="one-time-code" placeholder="<?php echo Text::_('JGLOBAL_SECRETKEY'); ?>">
|
|
</div>
|
|
|
|
<div class="form-check mb-4">
|
|
<input class="form-check-input" type="checkbox" name="remember" id="remember">
|
|
<label class="form-check-label" for="remember"><?php echo Text::_('JGLOBAL_REMEMBER_ME'); ?></label>
|
|
</div>
|
|
|
|
<div class="d-grid">
|
|
<button type="submit" class="btn btn-primary"><?php echo Text::_('JLOGIN'); ?></button>
|
|
</div>
|
|
|
|
<input type="hidden" name="option" value="com_users">
|
|
<input type="hidden" name="task" value="user.login">
|
|
<input type="hidden" name="return" value="<?php echo $return; ?>">
|
|
<?php echo HTMLHelper::_('form.token'); ?>
|
|
</fieldset>
|
|
|
|
<nav class="mt-3 small" aria-label="<?php echo Text::_('COM_USERS'); ?>">
|
|
<ul class="list-inline m-0">
|
|
<li class="list-inline-item">
|
|
<a href="<?php echo $resetUrl; ?>"><?php echo Text::_('COM_USERS_LOGIN_RESET'); ?></a>
|
|
</li>
|
|
<li class="list-inline-item">
|
|
<a href="<?php echo $remindUrl; ?>"><?php echo Text::_('COM_USERS_LOGIN_REMIND'); ?></a>
|
|
</li>
|
|
<?php if ($allowRegistration) : ?>
|
|
<li class="list-inline-item">
|
|
<a href="<?php echo $registrationUrl; ?>"><?php echo Text::_('COM_USERS_REGISTER'); ?></a>
|
|
</li>
|
|
<?php endif; ?>
|
|
</ul>
|
|
</nav>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Copyright -->
|
|
<div class="moko-offline-copyright">
|
|
<div>© <?php echo date('Y'); ?> <?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?></div>
|
|
<div><?php echo Text::_('MOD_FOOTER_LINE2'); ?></div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Offline footer module position -->
|
|
<?php if ($this->countModules('offline-footer')) : ?>
|
|
<div class="moko-offline-messages mt-3">
|
|
<jdoc:include type="modules" name="offline-footer" style="none" />
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<jdoc:include type="modules" name="debug" style="none" />
|
|
</body>
|
|
</html>
|