543 lines
14 KiB
PHP
543 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Akeeba Frontend Framework (FEF)
|
|
*
|
|
* @package fef
|
|
* @copyright (c) 2017-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU General Public License version 3, or later
|
|
*/
|
|
|
|
// Protect from unauthorized access
|
|
use Joomla\CMS\Document\PreloadManagerInterface;
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\HTML\HTMLHelper;
|
|
use Joomla\CMS\Uri\Uri;
|
|
use Joomla\Utilities\ArrayHelper;
|
|
|
|
defined('_JEXEC') || die();
|
|
|
|
if (@file_exists(__DIR__ . '/version.php') && @is_file(__DIR__ . '/version.php') && @is_readable(__DIR__ . '/version.php'))
|
|
{
|
|
@include_once(__DIR__ . '/version.php');
|
|
}
|
|
|
|
if (!defined('AKEEBAFEF_VERSION'))
|
|
{
|
|
define('AKEEBAFEF_VERSION', 'dev');
|
|
define('AKEEBAFEF_DATE', gmdate('Y-m-d'));
|
|
}
|
|
|
|
class AkeebaFEFHelper
|
|
{
|
|
/**
|
|
* Media versioning tag
|
|
*
|
|
* @var string
|
|
* @since 1.0.3
|
|
*/
|
|
public static $tag = null;
|
|
|
|
/**
|
|
* The Akeeba FEF Loader object
|
|
*
|
|
* @var AkeebaFEFLoader
|
|
* @since 2.0.0
|
|
*/
|
|
private static $loader;
|
|
|
|
/**
|
|
* Is this Joomla 4?
|
|
*
|
|
* @var bool|null
|
|
* @since 2.0.0
|
|
*/
|
|
private static $isJoomla4 = null;
|
|
|
|
/**
|
|
* Loads the Akeeba Frontend Framework, both CSS and JS
|
|
*
|
|
* @param bool $withReset Should I also load the CSS reset for the FEF container?
|
|
* @param bool $dark Include Dark Mode CSS?
|
|
*
|
|
* @return void
|
|
* @since 1.0.3
|
|
*/
|
|
public static function load(bool $withReset = true, bool $dark = false)
|
|
{
|
|
self::loadCSSFramework();
|
|
self::loadJSFramework();
|
|
}
|
|
|
|
/**
|
|
* Loads the Akeeba FEF CSS Framework
|
|
*
|
|
* @param bool $withReset Should I also load the CSS reset for the FEF container?
|
|
* @param bool $dark Include Dark Mode CSS?
|
|
*
|
|
* @return void
|
|
* @since 1.0.3
|
|
*/
|
|
public static function loadCSSFramework(bool $withReset = true, bool $dark = false)
|
|
{
|
|
self::getLoader()->loadCSSFramework($withReset, $dark);
|
|
}
|
|
|
|
/**
|
|
* Loads the Akeeba FEF JavaScript Framework
|
|
*
|
|
* @param bool $minimal Should I load the minimal framework (without optional features linked to FEF CSS?)
|
|
*
|
|
* @return void
|
|
* @since 2.0.0
|
|
*/
|
|
public static function loadJSFramework(bool $minimal = false)
|
|
{
|
|
self::getLoader()->loadJSFramework($minimal);
|
|
}
|
|
|
|
/**
|
|
* Legacy (FEF 1.1.x) alias to loadFEFScript.
|
|
*
|
|
* @param string $name
|
|
*
|
|
* @see self::loadFEFScript
|
|
* @deprecated 3.0
|
|
* @since 1.1.0
|
|
*/
|
|
public static function loadScript(string $name): void
|
|
{
|
|
self::loadFEFScript($name, true);
|
|
}
|
|
|
|
/**
|
|
* Load an Akeeba FEF JavaScript file and its dependencies.
|
|
*
|
|
* @param string $name The basename of the file, e.g. "Tabs"
|
|
* @param bool $defer Should I defer loading of the file?
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public static function loadFEFScript(string $name, bool $defer = true): void
|
|
{
|
|
self::getLoader()->loadFEFScript($name, $defer);
|
|
}
|
|
|
|
/**
|
|
* Is this Joomla 4 or later?
|
|
*
|
|
* @return bool
|
|
* @since 2.0.0
|
|
*/
|
|
private static function isJoomla4(): bool
|
|
{
|
|
if (!is_bool(self::$isJoomla4))
|
|
{
|
|
self::$isJoomla4 = version_compare(JVERSION, '3.999.999', 'gt');
|
|
}
|
|
|
|
return self::$isJoomla4;
|
|
}
|
|
|
|
/**
|
|
* Load a JavaScript file using the Joomla! API.
|
|
*
|
|
* Special considerations:
|
|
*
|
|
* We always load the minified version of the file. Joomla! will automatically use the non-minified one if Debug
|
|
* Site is enabled.
|
|
*
|
|
* You can have browser-specific files, e.g. foo_firefox.min.js, foo_firefox_57.min.js etc. These are loaded
|
|
* automatically instead of the foo.js file as needed.
|
|
*
|
|
* This method goes through Joomla's script loader, thus allowing template media overrides. The media overrides are
|
|
* supposed to be in the templates/YOUR_TEMPLATE/js/fef folder for FEF.
|
|
*
|
|
* @param string $name The Joomla!-coded path of the file, e.g. 'foo/bar.min.js' for the JavaScript file
|
|
* media/foo/js/bar.min.js
|
|
*
|
|
* @param bool $defer Should I load the script defered?
|
|
*
|
|
* @return void
|
|
* @since 1.0.3
|
|
*/
|
|
private static function loadJS(string $name, bool $defer = true): void
|
|
{
|
|
$options = [
|
|
'version' => self::getMediaVersion(),
|
|
'relative' => true,
|
|
'detectDebug' => false,
|
|
'framework' => false,
|
|
'pathOnly' => false,
|
|
'detectBrowser' => true,
|
|
];
|
|
HTMLHelper::_('script', 'fef/' . $name . '.min.js', $options, [
|
|
'defer' => $defer,
|
|
'async' => false,
|
|
]);
|
|
|
|
/**
|
|
* Preload the static resource we are definitely asking the browser to use for better performance.
|
|
*
|
|
* Yes, this is also useful for deferred scripts! On Joomla 4 this goes through Preload Manager which does an
|
|
* HTTP/2 Push for the script files. This is **far** faster than the browser making a number of HEAD requests
|
|
* to see if the scripts are cached then an equal (or smaller) number of GET requests to fetch the scripts.
|
|
*/
|
|
$options['pathOnly'] = 'true';
|
|
$path = HTMLHelper::_('script', 'fef/' . $name . '.min.js', $options);
|
|
|
|
if (empty($path))
|
|
{
|
|
return;
|
|
}
|
|
|
|
self::preloadResource($path . '?' . self::getMediaVersion(), ['as' => 'script']);
|
|
}
|
|
|
|
/**
|
|
* Load a CSS file using the Joomla! API.
|
|
*
|
|
* Special considerations:
|
|
*
|
|
* We always as Joomla to load the minified version of a file. Joomla! will automatically use the non-minified one
|
|
* if Debug Site is enabled.
|
|
*
|
|
* You can have browser-specific files, e.g. foo_firefox.min.css, foo_firefox_57.min.css etc. These are loaded
|
|
* automatically instead of the foo.css file as needed.
|
|
*
|
|
* This method goes through Joomla's script loader, thus allowing template media overrides. The media overrides are
|
|
* supposed to be in the templates/YOUR_TEMPLATE/css/fef folder for FEF.
|
|
*
|
|
* We are instructing the browser to preload the CSS file we are inserting in the HTML. This can cause a small
|
|
* performance increase. On Joomla 4 the increase is most noticeable because we go through the Preload Manager
|
|
* which can leverage HTTP/2 Push.
|
|
*
|
|
* When loading the main CSS file (joomla-fef) we preload the WOFF font files which will be most likely used by the
|
|
* browser consuming the CSS. This allows the browser to fetch the font files before fully parsing the stylesheet,
|
|
* saving some time.
|
|
*
|
|
* @param string $name The Joomla!-coded path of the file, e.g. 'foo/bar.min.css' for the stylesheet file
|
|
* media/foo/css/bar.min.css
|
|
*
|
|
* @return void
|
|
* @since 1.0.3
|
|
*/
|
|
private static function loadCSS(string $name): void
|
|
{
|
|
/**
|
|
* IMPORTANT! The $attribs (final parameter) MUST ALWAYS be non-empty. Otherwise Joomla! 3.x bugs out.
|
|
*/
|
|
$options = [
|
|
'version' => self::getMediaVersion(),
|
|
'relative' => true,
|
|
'detectDebug' => false,
|
|
'pathOnly' => false,
|
|
'detectBrowser' => true,
|
|
];
|
|
HTMLHelper::_('stylesheet', 'fef/' . $name . '.min.css', $options, [
|
|
'type' => 'text/css',
|
|
]);
|
|
|
|
// Preload the static resource we are definitely asking the browser to use for better performance
|
|
$options['pathOnly'] = 'true';
|
|
$path = HTMLHelper::_('stylesheet', 'fef/' . $name . '.min.css', $options, [
|
|
'type' => 'text/css',
|
|
]);
|
|
|
|
if (empty($path))
|
|
{
|
|
return;
|
|
}
|
|
|
|
self::preloadResource($path . '?' . self::getMediaVersion(), ['as' => 'style']);
|
|
|
|
// Special case: loading the fef-joomla stylesheet. Preload the font files as well
|
|
if ($name == 'fef-joomla')
|
|
{
|
|
$fontPath = dirname(dirname($path)) . '/fonts/akeeba/Akeeba-Products.woff';
|
|
self::preloadResource($fontPath, ['as' => 'font', 'crossorigin' => 'anonymous']);
|
|
|
|
$fontPath = dirname(dirname($path)) . '/fonts/Ionicon/ionicons.woff';
|
|
self::preloadResource($fontPath, ['as' => 'font', 'crossorigin' => 'anonymous']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Preload a resource.
|
|
*
|
|
* On Joomla 3 this adds a LINK tag to the HTML document, instructing the browser to preload the resource.
|
|
*
|
|
* On Joomla 4 we are using the document object's Preload Manager to leverage HTTP/2 Push if available.
|
|
*
|
|
* @param string $url The absolute or relative URL of the resource to preload.
|
|
* @param array $options Preload options. You need to specify 'as' as the bare minimum.
|
|
*
|
|
* @return void
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
private static function preloadResource(string $url, array $options): void
|
|
{
|
|
if (!self::isJoomla4() || !self::preloadResourceJoomla4($url, $options))
|
|
{
|
|
self::preloadResourceJoomla3($url, $options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Preload a resource on Joomla 3.
|
|
*
|
|
* This adds a LINK tag to the HTML document, instructing the browser to preload the resource.
|
|
*
|
|
* @param string $url The absolute or relative URL of the resource to preload.
|
|
* @param array $options Preload options. You need to specify 'as' as the bare minimum.
|
|
*
|
|
* @return bool True if successful; false if we can't get the document, or can't add a LINK tag e.g. this is not
|
|
* an HTMLDocument
|
|
* @since 2.0.0
|
|
*/
|
|
private static function preloadResourceJoomla3(string $url, array $options): bool
|
|
{
|
|
// Try to get Joomla's document object
|
|
$document = self::getDocument();
|
|
|
|
// Make sure the document object implements addCustomTag
|
|
if (!is_object($document) || !method_exists($document, 'addCustomTag'))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$options['rel'] = 'preload';
|
|
$options['href'] = self::relativeToAbsoluteURL($url);
|
|
$document->addCustomTag('<link ' . ArrayHelper::toString($options) . '>');
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Preload a resource on Joomla 4.
|
|
*
|
|
* We are using the document object's Preload Manager to leverage HTTP/2 Push if available.
|
|
*
|
|
* @param string $url The absolute or relative URL of the resource to preload.
|
|
* @param array $options Preload options. You need to specify 'as' as the bare minimum.
|
|
*
|
|
* @return bool True if successful; false if we can't get the document, or can't add a LINK tag e.g. this is not
|
|
* an HTMLDocument
|
|
* @since 2.0.0
|
|
*/
|
|
private static function preloadResourceJoomla4(string $url, array $options): bool
|
|
{
|
|
// Make sure we're in a version of Joomla which has support for the Preload Manager
|
|
if (!interface_exists('\\Joomla\CMS\Document\PreloadManagerInterface'))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Try to get Joomla's document object
|
|
$document = self::getDocument();
|
|
|
|
// Make sure the document object implements getPreloadManager
|
|
if (!is_object($document) || !method_exists($document, 'getPreloadManager'))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Try to get the preload manager
|
|
try
|
|
{
|
|
$preloadManager = $document->getPreloadManager();
|
|
}
|
|
catch (Throwable $e)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Make sure the preload manager is an object implementing the PreloadManagerInterface
|
|
if (!is_object($preloadManager) || !($preloadManager instanceof PreloadManagerInterface))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$absoluteUrl = self::relativeToAbsoluteURL($url);
|
|
$preloadManager->preload($absoluteUrl, $options);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get the Joomla document object
|
|
*
|
|
* @return JDocument|\Joomla\CMS\Document\Document|null NULL if we can't retrieve the document object.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
private static function getDocument()
|
|
{
|
|
// Get the CMS application
|
|
try
|
|
{
|
|
$app = Factory::getApplication();
|
|
}
|
|
catch (Throwable $e)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Make sure it's an object implementing getDocument
|
|
if (!is_object($app) || !method_exists($app, 'getDocument'))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Try to get the document
|
|
try
|
|
{
|
|
$document = $app->getDocument();
|
|
}
|
|
catch (Throwable $e)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Convert a relative URL to an absolute URL for the current site
|
|
*
|
|
* @param string $url The possibly relative URL.
|
|
*
|
|
* @return string The definitely absolute URL.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
private static function relativeToAbsoluteURL(string $url): string
|
|
{
|
|
static $baseUri;
|
|
static $basePath;
|
|
|
|
// Get the base URI, e.g. 'https://localhost/test'
|
|
if (empty($baseUri))
|
|
{
|
|
$baseUri = $baseUri ?? Uri::base();
|
|
|
|
if (substr($baseUri, -15) === '/administrator/')
|
|
{
|
|
$baseUri = substr($baseUri, 0, -15);
|
|
}
|
|
elseif (substr($baseUri, -14) === '/administrator')
|
|
{
|
|
$baseUri = substr($baseUri, 0, -14);
|
|
}
|
|
}
|
|
|
|
// Get the base path, e.g. 'test'
|
|
if (empty($basePath))
|
|
{
|
|
$basePath = $basePath ?? Uri::base(true);
|
|
$basePath = empty($basePath) ? '' : trim($basePath, '/');
|
|
|
|
if ($basePath === 'administrator')
|
|
{
|
|
$basePath = '';
|
|
}
|
|
elseif (substr($basePath, -14) == '/administrator')
|
|
{
|
|
$basePath = trim(substr($basePath, 0, -14), '/');
|
|
}
|
|
}
|
|
|
|
if ((substr($url, 0, 2) == '//') ||
|
|
(substr($url, 0, 7) == 'http://') ||
|
|
(substr($url, 0, 8) == 'https://') ||
|
|
(substr($url, 0, strlen($baseUri)) == $baseUri))
|
|
{
|
|
return $url;
|
|
}
|
|
|
|
$url = ltrim($url, '/');
|
|
|
|
if ((strlen($basePath) != 0) && (substr($url, 0, strlen($basePath)) === $basePath))
|
|
{
|
|
$url = ltrim(substr($url, strlen($basePath)), '/');
|
|
$url = ltrim($url, '/');
|
|
}
|
|
|
|
return rtrim($baseUri, '/') . '/' . $url;
|
|
}
|
|
|
|
/**
|
|
* Get the media versioning tag. If it's not set, create one first.
|
|
*
|
|
* @return string
|
|
* @since 1.1.0
|
|
*/
|
|
private static function getMediaVersion(): string
|
|
{
|
|
if (empty(self::$tag))
|
|
{
|
|
self::$tag = md5(AKEEBAFEF_VERSION . AKEEBAFEF_DATE . self::getApplicationSecret());
|
|
}
|
|
|
|
return self::$tag;
|
|
}
|
|
|
|
/**
|
|
* Return the secret key for the Joomla! installation. Falls back to an MD5 of our file mod time.
|
|
*
|
|
* @return string
|
|
* @since 1.1.0
|
|
*/
|
|
private static function getApplicationSecret(): string
|
|
{
|
|
$secret = md5(filemtime(__FILE__));
|
|
|
|
// Get the site's secret
|
|
try
|
|
{
|
|
$app = Factory::getApplication();
|
|
|
|
if (method_exists($app, 'get'))
|
|
{
|
|
return $app->get('secret', $secret);
|
|
}
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
}
|
|
|
|
return $secret;
|
|
}
|
|
|
|
/**
|
|
* Returns or creates the Akeeba FEF Loader object.
|
|
*
|
|
* IMPORTANT: DO NOT SET A RETURN VALUE TYPE HINT. The AkeebaFEFLoader class is undefined until we load it in this
|
|
* method. This causes a chicken and egg problem which results in a Fatal Error!
|
|
*
|
|
* @return AkeebaFEFLoader
|
|
* @since 2.0.0
|
|
*/
|
|
private static function getLoader()
|
|
{
|
|
if (!is_null(self::$loader))
|
|
{
|
|
return self::$loader;
|
|
}
|
|
|
|
if (!class_exists('AkeebaFEFLoader'))
|
|
{
|
|
require_once __DIR__ . '/php/AkeebaFEFLoader.php';
|
|
}
|
|
|
|
self::$loader = new AkeebaFEFLoader(function (string $name) {
|
|
self::loadCSS($name);
|
|
}, function (string $name, bool $defer) {
|
|
self::loadJS($name, $defer);
|
|
}, 'joomla');
|
|
|
|
return self::$loader;
|
|
}
|
|
}
|