first commit
This commit is contained in:
7
administrator/components/com_jchoptimize/access.xml
Normal file
7
administrator/components/com_jchoptimize/access.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<access component="com_cache">
|
||||
<section name="component">
|
||||
<action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" />
|
||||
<action name="core.manage" title="JACTION_MANAGE" description="JACTION_MANAGE_COMPONENT_DESC" />
|
||||
</section>
|
||||
</access>
|
||||
30
administrator/components/com_jchoptimize/autoload.php
Normal file
30
administrator/components/com_jchoptimize/autoload.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die('Restricted access');
|
||||
|
||||
if (! defined('_JCH_EXEC')) {
|
||||
define('_JCH_EXEC', 1);
|
||||
}
|
||||
|
||||
if (! defined('_JCH_BASE_DIR')) {
|
||||
define('_JCH_BASE_DIR', __DIR__);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/version.php';
|
||||
|
||||
if (JCH_DEVELOP) {
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
} else {
|
||||
require_once __DIR__ . '/lib/vendor/scoper-autoload.php';
|
||||
}
|
||||
1298
administrator/components/com_jchoptimize/config.xml
Normal file
1298
administrator/components/com_jchoptimize/config.xml
Normal file
File diff suppressed because it is too large
Load Diff
80
administrator/components/com_jchoptimize/fields/ajax.php
Normal file
80
administrator/components/com_jchoptimize/fields/ajax.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use JchOptimize\ContainerFactory;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
|
||||
include_once JPATH_ADMINISTRATOR . '/components/com_jchoptimize/autoload.php';
|
||||
|
||||
class JFormFieldAjax extends JFormField
|
||||
{
|
||||
protected $type = 'ajax';
|
||||
|
||||
|
||||
public function setup(SimpleXMLElement $element, $value, $group = null)
|
||||
{
|
||||
$params = ContainerFactory::getContainer()->get('params');
|
||||
|
||||
if (!defined('JCH_DEBUG')) {
|
||||
define('JCH_DEBUG', ($params->get('debug', 0) && JDEBUG));
|
||||
}
|
||||
|
||||
$script_options = ['framework' => false, 'relative' => true];
|
||||
|
||||
HTMLHelper::_('jquery.framework', true, null, false);
|
||||
|
||||
$document = JFactory::getDocument();
|
||||
$script = '';
|
||||
|
||||
$options = ['version' => JCH_VERSION];
|
||||
$document->addStyleSheet(JUri::root(true) . '/media/com_jchoptimize/core/css/admin.css', $options);
|
||||
$document->addScript(JUri::root(true) . '/media/com_jchoptimize/core/js/admin-utility.js', $options);
|
||||
$document->addScript(JUri::root(true) . '/media/com_jchoptimize/js/platform-joomla.js', $options);
|
||||
$document->addScript(JUri::root(true) . '/media/com_jchoptimize/core/js/multiselect.js', $options);
|
||||
$document->addScript(JUri::root(true) . '/media/com_jchoptimize/core/js/smart-combine.js', $options);
|
||||
|
||||
if (version_compare(JVERSION, '3.99.99', '>')) {
|
||||
$document->addStyleSheet(JUri::root(true) . '/media/vendor/chosen/css/chosen.css');
|
||||
$document->addStyleSheet(JUri::root(true) . '/media/com_jchoptimize/css/js-excludes-J4.css', $options);
|
||||
$document->addScript(JUri::root(true) . '/media/vendor/chosen/js/chosen.jquery.js');
|
||||
$document->addScriptDeclaration(
|
||||
'jQuery(document).ready(function() {
|
||||
jQuery(\'.jch-multiselect\').chosen({
|
||||
width: "80%"
|
||||
});
|
||||
});'
|
||||
);
|
||||
} else {
|
||||
$document->addStyleSheet(JUri::root(true) . '/media/com_jchoptimize/css/js-excludes-J3.css', $options);
|
||||
}
|
||||
|
||||
$ajax_url = JRoute::_('index.php?option=com_jchoptimize&view=Ajax', false, JRoute::TLS_IGNORE, true);
|
||||
|
||||
$script .= <<<JS
|
||||
var jch_observers = [];
|
||||
var jch_ajax_url = '$ajax_url';
|
||||
|
||||
JS;
|
||||
|
||||
$document->addScriptDeclaration($script);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/core
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined( '_JEXEC' ) or die;
|
||||
|
||||
include_once dirname( __FILE__ ) . '/exclude.php';
|
||||
|
||||
class JFormFieldCssbgimages extends JFormFieldExclude
|
||||
{
|
||||
public $type = 'cssbgimages';
|
||||
public string $filetype = 'css';
|
||||
public string $filegroup = 'class';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldCustomextension extends JFormFieldExclude {
|
||||
|
||||
public $type = 'customextension';
|
||||
public string $filetype = 'url';
|
||||
public string $filegroup = 'file';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldDynamicselectors extends JFormFieldExclude
|
||||
{
|
||||
public $type = 'dynamicselectors';
|
||||
public string $filetype = 'selectors';
|
||||
public string $filegroup = 'style';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
106
administrator/components/com_jchoptimize/fields/exclude.php
Normal file
106
administrator/components/com_jchoptimize/fields/exclude.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use JchOptimize\ContainerFactory;
|
||||
use JchOptimize\Core\Admin\MultiSelectItems;
|
||||
use JchOptimize\Core\Helper;
|
||||
use Joomla\CMS\Uri\Uri as JUri;
|
||||
|
||||
include_once JPATH_ADMINISTRATOR . '/components/com_jchoptimize/autoload.php';
|
||||
include_once JPATH_ADMINISTRATOR . '/components/com_jchoptimize/version.php';
|
||||
|
||||
JFormHelper::loadFieldClass('textarea');
|
||||
|
||||
abstract class JFormFieldExclude extends JFormFieldTextarea
|
||||
{
|
||||
|
||||
protected bool $first_field = false;
|
||||
protected string $filegroup = 'file';
|
||||
protected string $filetype = '';
|
||||
/**
|
||||
* @var MultiSelectItems
|
||||
*/
|
||||
protected $multiSelect;
|
||||
|
||||
public function __construct($form = null)
|
||||
{
|
||||
parent::__construct($form);
|
||||
|
||||
$container = ContainerFactory::getContainer();
|
||||
$this->multiSelect = $container->buildObject(MultiSelectItems::class);
|
||||
}
|
||||
|
||||
public function setup(SimpleXMLElement $element, $value, $group = null): bool
|
||||
{
|
||||
$value = $this->castValue($value);
|
||||
|
||||
return parent::setup($element, $value, $group);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function castValue($value)
|
||||
{
|
||||
if ( ! is_array($value)) {
|
||||
$value = Helper::getArray($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getInput()
|
||||
{
|
||||
$attributes = 'class="inputbox chzn-custom-value input-xlarge jch-multiselect" multiple size="5" data-jch_type="' . $this->filetype . '" data-jch_param="' . $this->fieldname . '" data-jch_group="' . $this->filegroup . '"';
|
||||
$select = JHTML::_(
|
||||
'select.genericlist',
|
||||
$this->getOptions(),
|
||||
'jform[' . $this->fieldname . '][]',
|
||||
$attributes,
|
||||
'id',
|
||||
'name',
|
||||
$this->value,
|
||||
$this->id
|
||||
);
|
||||
$uriRoot = JUri::root();
|
||||
|
||||
return <<<HTML
|
||||
<div id="div-{$this->fieldname}">{$select}
|
||||
<img id="img-{$this->fieldname}" class="jch-multiselect-loading-image" src="{$uriRoot}media/com_jchoptimize/core/images/exclude-loader.gif" />
|
||||
<button type="button" class="btn btn-sm btn-secondary jch-multiselect-add-button" onmousedown="jchMultiselect.addJchOption('jform_{$this->fieldname}')" style="display: none;">Add item</button>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
protected function getOptions(): array
|
||||
{
|
||||
$options = [];
|
||||
|
||||
foreach ($this->value as $excludeValue) {
|
||||
$options[$excludeValue] = $this->multiSelect->{'prepare' . ucfirst($this->filegroup) . 'Values'}(
|
||||
$excludeValue
|
||||
);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\ContainerFactory;
|
||||
use Joomla\Filesystem\Folder;
|
||||
|
||||
defined( '_JEXEC' ) or die;
|
||||
|
||||
include_once dirname( __FILE__ ) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludecomponent extends JFormFieldExclude
|
||||
{
|
||||
public $type = 'excludecomponent';
|
||||
|
||||
protected function getOptions(): array
|
||||
{
|
||||
$options = [];
|
||||
|
||||
$params = ContainerFactory::getContainer()->get( 'params' );
|
||||
|
||||
$installedComponents = Folder::folders( JPATH_SITE . '/components' );
|
||||
$excludedComponents = $params->get( 'cache_exclude_component', [ 'com_ajax' ] );
|
||||
|
||||
$components = array_unique( array_merge( $installedComponents, $excludedComponents ) );
|
||||
|
||||
foreach ( $components as $component )
|
||||
{
|
||||
$options[ $component ] = $component;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined( '_JEXEC' ) or die;
|
||||
|
||||
use JchOptimize\Platform\Plugin;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
|
||||
include_once dirname( __FILE__ ) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludecss extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludecss';
|
||||
public string $filetype = 'css';
|
||||
public string $filegroup = 'file';
|
||||
|
||||
/**
|
||||
*
|
||||
* @return type
|
||||
*/
|
||||
protected function getInput()
|
||||
{
|
||||
$this->first_field = true;
|
||||
|
||||
return parent::getInput();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludecssextns extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludecssextns';
|
||||
public string $filetype = 'css';
|
||||
public string $filegroup = 'extension';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludehttp2 extends JFormFieldExclude
|
||||
{
|
||||
public $type = 'excludehttp2';
|
||||
public string $filegroup = 'file';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludeimages extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludeimages';
|
||||
public string $filetype = 'images';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludejs extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludejs';
|
||||
public string $filetype = 'js';
|
||||
public string $filegroup = 'file';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludejsextns extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludejsextns';
|
||||
public string $filetype = 'js';
|
||||
public string $filegroup = 'extension';
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/excludejspei.php';
|
||||
|
||||
class JFormFieldExcludejsextnspei extends JFormFieldExcludejspei
|
||||
{
|
||||
|
||||
public $type = 'excludejsextnspei';
|
||||
public string $filetype = 'js';
|
||||
public string $filegroup = 'extension';
|
||||
}
|
||||
105
administrator/components/com_jchoptimize/fields/excludejspei.php
Normal file
105
administrator/components/com_jchoptimize/fields/excludejspei.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludejspei extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludejspei';
|
||||
public string $filetype = 'js';
|
||||
public string $filegroup = 'file';
|
||||
protected string $valueType = 'url';
|
||||
|
||||
protected function getInput(): string
|
||||
{
|
||||
$this->value = array_values($this->value);
|
||||
$i = 0;
|
||||
$nextIndex = count($this->value);
|
||||
|
||||
$field = <<<HTML
|
||||
<fieldset id="fieldset-{$this->fieldname}" data-index="{$nextIndex}">
|
||||
<div class="jch-js-fieldset-children jch-js-excludes-header">
|
||||
<span class="jch-js-ieo-header"> Ignore execution order </span>
|
||||
<span class="jch-js-dontmove-header"> Don't move to bottom </span>
|
||||
</div>
|
||||
HTML;
|
||||
foreach ($this->value as $value) {
|
||||
//Sanity check
|
||||
if (!isset($value[$this->valueType])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ieoChecked = isset($value['ieo']) ? 'checked' : '';
|
||||
$dontMoveChecked = isset($value['dontmove']) ? 'checked' : '';
|
||||
$dataValue = $this->multiSelect->{'prepare' . ucfirst($this->filegroup) . 'Values'}(
|
||||
$value[$this->valueType]
|
||||
);
|
||||
$size = strlen($value[$this->valueType]);
|
||||
|
||||
$field .= <<<HTML
|
||||
<div id="div-{$this->fieldname}-{$i}" class="jch-js-fieldset-children jch-js-excludes-container">
|
||||
<span class="jch-js-excludes"><span><input type="text" readonly size="{$size}" value="{$value[$this->valueType]}" name="jform[{$this->fieldname}][$i][{$this->valueType}]">
|
||||
{$dataValue}
|
||||
<button type="button" class="jch-multiselect-remove-button" onmouseup="jchMultiselect.removeJchJsOption('div-{$this->fieldname}-{$i}', 'jform_{$this->fieldname}')"></button>
|
||||
</span></span>
|
||||
<span class="jch-js-ieo">
|
||||
<input type="checkbox" name="jform[{$this->fieldname}][$i][ieo]" {$ieoChecked}/>
|
||||
</span>
|
||||
<span class="jch-js-dontmove">
|
||||
<input type="checkbox" name="jform[{$this->fieldname}][$i][dontmove]" {$dontMoveChecked} />
|
||||
</span>
|
||||
</div>
|
||||
HTML;
|
||||
$i++;
|
||||
}
|
||||
$attributes = 'class="inputbox chzn-custom-value input-large jch-multiselect" multiple data-jch_type="' . $this->filetype . '" data-jch_param="' . $this->fieldname . '" data-jch_group="' . $this->filegroup . '"';
|
||||
$select = HTMLHelper::_(
|
||||
'select.genericlist',
|
||||
$this->getOptions(),
|
||||
'jform[' . $this->fieldname . '][][' . $this->valueType . ']',
|
||||
$attributes,
|
||||
'id',
|
||||
'name',
|
||||
$this->value,
|
||||
$this->id
|
||||
);
|
||||
|
||||
$uriRoot = Uri::root();
|
||||
|
||||
$field .= <<<HTML
|
||||
</fieldset>
|
||||
<div id="div-{$this->fieldname}">{$select}
|
||||
<img id="img-{$this->fieldname}" class="jch-multiselect-loading-image" src="{$uriRoot}media/com_jchoptimize/core/images/exclude-loader.gif" />
|
||||
<button type="button" class="btn btn-sm btn-secondary jch-multiselect-add-button" onmousedown="jchMultiselect.addJchJsOption('jform_{$this->fieldname}', '{$this->fieldname}', '{$this->valueType}')" style="display: none;">Add item</button>
|
||||
</div>
|
||||
<script>
|
||||
jQuery('#jform_{$this->fieldname}').on('change', function(evt, params){
|
||||
jchMultiselect.appendJchJsOption('jform_{$this->fieldname}', '{$this->fieldname}', params, '{$this->valueType}');
|
||||
});
|
||||
</script>
|
||||
HTML;
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludejsurls extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludejsurls';
|
||||
public string $filetype = 'js';
|
||||
public string $filegroup = 'file';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludelazyload extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludelazyload';
|
||||
public string $filetype = 'lazyload';
|
||||
public string $filegroup = 'file';
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludelazyloadclass extends JFormFieldExclude
|
||||
{
|
||||
public $type = 'excludelazyloadclass';
|
||||
public string $filetype = 'lazyload';
|
||||
public string $filegroup = 'class';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludelazyloadfolders extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludelazyloadfolders';
|
||||
public string $filetype = 'lazyload';
|
||||
public string $filegroup = 'folder';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludescripts extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludescripts';
|
||||
public string $filetype = 'js';
|
||||
public string $filegroup = 'script';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/excludejspei.php';
|
||||
|
||||
class JFormFieldExcludescriptspei extends JFormFieldExcludejspei
|
||||
{
|
||||
|
||||
public $type = 'excludescriptspei';
|
||||
public string $filetype = 'js';
|
||||
public string $filegroup = 'script';
|
||||
protected string $valueType = 'script';
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludestyles extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludestyles';
|
||||
public string $filetype = 'css';
|
||||
public string $filegroup = 'style';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludeurl extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludeurl';
|
||||
public string $filetype = 'url';
|
||||
public string $filegroup = 'file';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldExcludeurlwithquery extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'excludeurlwithquery';
|
||||
public string $filetype = 'url';
|
||||
public string $filegroup = 'class';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldIncludehttp2 extends JFormFieldExclude
|
||||
{
|
||||
public $type = 'includehttp2';
|
||||
public string $filegroup = 'file';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldIncludeimages extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'includeimages';
|
||||
public string $filetype = 'images';
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
78
administrator/components/com_jchoptimize/fields/jchgroup.php
Normal file
78
administrator/components/com_jchoptimize/fields/jchgroup.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die('No direct access');
|
||||
|
||||
class JFormFieldJchgroup extends JFormField
|
||||
{
|
||||
|
||||
public $type = 'jchgroup';
|
||||
|
||||
protected function getLabel()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
$attributes = $this->element->attributes();
|
||||
|
||||
$html = '';
|
||||
|
||||
$header = isset($attributes['label']) ? '<h4>' . JText::_($attributes['label']) . '<span class="fa"></span></h4>' : '';
|
||||
$description = isset($attributes['description']) ? '<p><em>' . JText::_($attributes['description']) . '</em></p>' : '';
|
||||
$section = $attributes['section'];
|
||||
$name = $attributes['name'];
|
||||
|
||||
$class = isset($attributes['class']) !== false ? 'class="' . $attributes['class'] . '" ' : '';
|
||||
|
||||
$collapsible = '<div ' . $class . '>';
|
||||
$collapsible .= $header;
|
||||
$collapsible .= $description . '<br>';
|
||||
$collapsible .= '</div><div>';
|
||||
|
||||
if (version_compare(JVERSION, '3.0', '>='))
|
||||
{
|
||||
|
||||
$html .= '</div></div>';
|
||||
|
||||
if ($section == 'start')
|
||||
{
|
||||
$html .= '<div class="well well-small">';
|
||||
$html .= $collapsible;
|
||||
}
|
||||
else
|
||||
{
|
||||
$html .= '</div></div>';
|
||||
}
|
||||
|
||||
$html .= '<div><div>';
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($section == 'start')
|
||||
{
|
||||
$html .= '<div class="jchgroup">';
|
||||
$html .= $collapsible;
|
||||
$html .= '<ul class="adminformlist">';
|
||||
}
|
||||
else
|
||||
{
|
||||
$html .= '</ul></div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldLoadfilesasync extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'loadfilesasync';
|
||||
protected $jch_params = 'pro_loadFilesAsync';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
$this->setAjaxParams('js', $this->jch_params, 'file');
|
||||
|
||||
return parent::getInput();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined( '_JEXEC' ) or die( 'No direct access' );
|
||||
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
|
||||
require_once JPATH_ADMINISTRATOR . '/components/com_menus/helpers/menus.php';
|
||||
|
||||
class JFormFieldMenuselection extends JFormField
|
||||
{
|
||||
|
||||
public $type = 'menuselection';
|
||||
|
||||
|
||||
public function setup( SimpleXMLElement $element, $value, $group = null )
|
||||
{
|
||||
$script_options = array( 'framework' => false, 'relative' => true );
|
||||
|
||||
if ( version_compare( JVERSION, '3.99.99', '<' ) )
|
||||
{
|
||||
JHtml::script( 'jui/jquery.min.js' );
|
||||
JHtml::_( 'script', 'jui/treeselectmenu.jquery.min.js', false, true );
|
||||
}
|
||||
else
|
||||
{
|
||||
HTMLHelper::_( 'script', 'vendor/jquery/jquery.min.js', $script_options );
|
||||
$oDocument = JFactory::getDocument();
|
||||
$oDocument->addScript( JUri::root( true ) . '/media/legacy/js/treeselectmenu.js' );
|
||||
}
|
||||
|
||||
if ( ! is_array( $value ) )
|
||||
{
|
||||
$value = array();
|
||||
}
|
||||
|
||||
return parent::setup( $element, $value, $group );
|
||||
}
|
||||
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
$menuTypes = MenusHelper::getMenuLinks();
|
||||
|
||||
$html = '';
|
||||
|
||||
if ( ! empty( $menuTypes ) ) :
|
||||
$id = 'jform_menuselect';
|
||||
|
||||
$html .= '<div>
|
||||
<div class="form-inline">
|
||||
<span class="small">' . JText::_( 'JSELECT' ) . ':
|
||||
<a id="treeCheckAll" href="javascript://">' . JText::_( 'JALL' ) . '</a>,
|
||||
<a id="treeUncheckAll" href="javascript://">' . JText::_( 'JNONE' ) . '</a>
|
||||
</span>
|
||||
<span class="width-20">|</span>
|
||||
<span class="small">' . JText::_( 'JCH_EXPAND' ) . ':
|
||||
<a id="treeExpandAll" href="javascript://">' . JText::_( 'JALL' ) . '</a>,
|
||||
<a id="treeCollapseAll" href="javascript://">' . JText::_( 'JNONE' ) . '</a>
|
||||
</span>
|
||||
<input type="text" id="treeselectfilter" name="treeselectfilter" class="input-medium search-query pull-right" size="16"
|
||||
autocomplete="off" placeholder="' . JText::_( 'JSEARCH_FILTER' ) . '" aria-invalid="false" tabindex="-1">
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<hr class="hr-condensed" />
|
||||
|
||||
<ul class="treeselect">';
|
||||
foreach ( $menuTypes as &$type ) :
|
||||
if ( count( $type->links ) ) :
|
||||
$prevlevel = 0;
|
||||
$html .= '<li>
|
||||
<div class="treeselect-item pull-left">
|
||||
<label class="pull-left nav-header">' . $type->title . '</label></div>';
|
||||
foreach ( $type->links as $i => $link ) :
|
||||
if ( $prevlevel < $link->level )
|
||||
{
|
||||
$html .= '<ul class="treeselect-sub">';
|
||||
}
|
||||
elseif ( $prevlevel > $link->level )
|
||||
{
|
||||
$html .= str_repeat( '</li></ul>', $prevlevel - $link->level );
|
||||
}
|
||||
else
|
||||
{
|
||||
$html .= '</li>';
|
||||
}
|
||||
// $selected = 0;
|
||||
// if ($pluginassignment == 0)
|
||||
// {
|
||||
// $selected = 1;
|
||||
// } elseif ($pluginassingment < 0)
|
||||
// {
|
||||
// $selected = in_array(-$link->value, $this->value);
|
||||
// } elseif ($pluginassignment > 0)
|
||||
// {
|
||||
$selected = in_array( $link->value, $this->value );
|
||||
// }
|
||||
|
||||
$html .= '<li>
|
||||
<div class="treeselect-item pull-left">
|
||||
<input type="checkbox" class="pull-left" name="jform[menuexcluded][]" id="' . $id . $link->value . '" value="' . (int)$link->value . '"' . ( $selected ? ' checked="checked"' : '' ) . ' />
|
||||
<label for="' . $id . $link->value . '" class="pull-left">
|
||||
' . $link->text . ' <span class="small">' . JText::sprintf( 'JGLOBAL_LIST_ALIAS', htmlentities( $link->alias ) ) . '</span>
|
||||
';
|
||||
if ( JLanguageMultilang::isEnabled() && $link->language != '' && $link->language != '*' )
|
||||
{
|
||||
$html .= JHtml::_( 'image', 'mod_languages/' . $link->language_image . '.gif', $link->language_title, array( 'title' => $link->language_title ), true );
|
||||
}
|
||||
if ( $link->published == 0 )
|
||||
{
|
||||
$html .= ' <span class="label">' . JText::_( 'JUNPUBLISHED' ) . '</span>';
|
||||
}
|
||||
|
||||
$html .= ' </label>
|
||||
</div>';
|
||||
|
||||
$menuitems_limit = 50;
|
||||
if ( ! isset( $type->links[ $i + 1 ] ) || $i > $menuitems_limit )
|
||||
{
|
||||
$html .= str_repeat( '</li></ul>', $link->level );
|
||||
}
|
||||
$prevlevel = $link->level;
|
||||
|
||||
if ( $i > $menuitems_limit )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
endforeach;
|
||||
$html .= ' </li>';
|
||||
endif;
|
||||
endforeach;
|
||||
$html .= ' </ul>
|
||||
<div id="noresultsfound" style="display:none;" class="alert alert-no-items">
|
||||
' . JText::_( 'JGLOBAL_NO_MATCHING_RESULTS' ) . '
|
||||
</div>
|
||||
<div style="display:none;" id="treeselectmenu">
|
||||
<div class="pull-left nav-hover treeselect-menu">
|
||||
<div class="btn-group">
|
||||
<a href="#" data-toggle="dropdown" class="dropdown-toggle btn btn-micro">
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="nav-header">' . JText::_( 'JCH_SUBITEMS' ) . '</li> <li class="divider"></li>
|
||||
<li class=""><a class="checkall" href="javascript://"><span class="icon-checkbox"></span> ' . JText::_( 'JSELECT' ) . '</a>
|
||||
</li>
|
||||
<li><a class="uncheckall" href="javascript://"><span class="icon-checkbox-unchecked"></span> ' . JText::_( 'JCH_DESELECT' ) . '</a>
|
||||
</li>
|
||||
<div class="treeselect-menu-expand">
|
||||
<li class="divider"></li>
|
||||
<li><a class="expandall" href="javascript://"><span class="icon-plus"></span>' . JText::_( 'JCH_EXPAND' ) . '</a></li>
|
||||
<li><a class="collapseall" href="javascript://"><span class="icon-minus"></span>' . JText::_( 'JCH_COLLAPSE' ) . '</a></li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>';
|
||||
endif;
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die('No direct access');
|
||||
|
||||
require_once(JPATH_ADMINISTRATOR . '/components/com_modules/helpers/modules.php');
|
||||
|
||||
class JFormFieldPluginassignment extends JFormField
|
||||
{
|
||||
|
||||
public $type = 'pluginassignment';
|
||||
|
||||
|
||||
public function setup(SimpleXMLElement $element, $value, $group = NULL)
|
||||
{
|
||||
$script = "
|
||||
jQuery(document).ready(function()
|
||||
{
|
||||
menuHide(jQuery('#jform_assignment').val());
|
||||
jQuery('#jform_assignment').change(function()
|
||||
{
|
||||
menuHide(jQuery(this).val());
|
||||
})
|
||||
});
|
||||
function menuHide(val)
|
||||
{
|
||||
if (val == 0 || val == '-')
|
||||
{
|
||||
jQuery('#menuselect-group').hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
jQuery('#menuselect-group').show();
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
// Add the script to the document head
|
||||
JFactory::getDocument()->addScriptDeclaration($script);
|
||||
|
||||
return parent::setup($element, $value, $group);
|
||||
}
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
$html = '<select name="jform[assignment]" id="jform_assignment"> ' .
|
||||
JHtml::_('select.options', ModulesHelper::getAssignmentOptions(0), 'value', 'text', $this->value, true) .
|
||||
'</select>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/core
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2023 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldProcriticaljs extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'procriticaljs';
|
||||
public string $filetype = 'criticaljs';
|
||||
public string $filegroup = 'file';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO) {
|
||||
return Helper::proOnlyField();
|
||||
} else {
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/core
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined( '_JEXEC' ) or die;
|
||||
|
||||
include_once dirname( __FILE__ ) . '/excludecss.php';
|
||||
|
||||
class JFormFieldProexcludecss extends JFormFieldExcludecss
|
||||
{
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/excludejsurls.php';
|
||||
|
||||
class JFormFieldProexcludejsurls extends JFormFieldExcludejsurls
|
||||
{
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/excludescripts.php';
|
||||
|
||||
class JFormFieldProexcludescripts extends JFormFieldExcludescripts
|
||||
{
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
include_once dirname(__FILE__) . '/exclude.php';
|
||||
|
||||
class JFormFieldPromodules extends JFormFieldExclude
|
||||
{
|
||||
|
||||
public $type = 'promodules';
|
||||
public string $filetype = 'modules';
|
||||
public string $filegroup = 'file';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO) {
|
||||
return Helper::proOnlyField();
|
||||
} else {
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
|
||||
JFormHelper::loadFieldClass('checkboxes');
|
||||
|
||||
class JFormFieldProonlycheckboxes extends JFormFieldCheckboxes
|
||||
{
|
||||
|
||||
public $type = 'proonlycheckboxes';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/core
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2023 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
use Joomla\CMS\Cache\Cache;
|
||||
use Joomla\CMS\Form\FormHelper;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
defined('_JEXEC') or die('Restricted Access');
|
||||
|
||||
FormHelper::loadFieldClass('list');
|
||||
|
||||
class JFormFieldProonlyjchcachehandler extends JFormFieldList
|
||||
{
|
||||
public $type = 'proonlyjchcachehandler';
|
||||
|
||||
protected function getOptions()
|
||||
{
|
||||
$optionsMap = [
|
||||
'file' => 'filesystem',
|
||||
'redis' => 'redis',
|
||||
'apcu' => 'apcu',
|
||||
'memcached' => 'memcached',
|
||||
'wincache' => 'wincache'
|
||||
];
|
||||
|
||||
$availableStores = Cache::getStores();
|
||||
|
||||
foreach ($optionsMap as $joomlaStorage => $laminasStorage) {
|
||||
if (JCH_PRO || $laminasStorage == 'filesystem') {
|
||||
if (in_array($joomlaStorage, $availableStores)) {
|
||||
$options[] = HTMLHelper::_(
|
||||
'select.option',
|
||||
$laminasStorage,
|
||||
Text::_('COM_JCHOPTIMIZE_STORAGE_' . strtoupper($laminasStorage)),
|
||||
'value',
|
||||
'text',
|
||||
false
|
||||
);
|
||||
} else {
|
||||
$options[] = HTMLHelper::_(
|
||||
'select.option',
|
||||
$laminasStorage,
|
||||
Text::_('COM_JCHOPTIMIZE_STORAGE_' . strtoupper($laminasStorage)),
|
||||
'value',
|
||||
'text',
|
||||
true
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$options[] = HTMLHelper::_(
|
||||
'select.option',
|
||||
$laminasStorage,
|
||||
Text::_('COM_JCHOPTIMIZE_STORAGE_' . strtoupper($laminasStorage)) . ' (Pro Only)',
|
||||
[
|
||||
'disable' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$options = array_merge(parent::getOptions(), $options);
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/core
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2023 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
use JchOptimize\ContainerFactory;
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
use JchOptimize\Model\ModeSwitcher;
|
||||
use Joomla\CMS\Form\FormHelper;
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
defined('_JEXEC') or die('Restricted Access');
|
||||
|
||||
FormHelper::loadFieldClass('list');
|
||||
|
||||
class JFormFieldProonlyjchpagecache extends JFormFieldList
|
||||
{
|
||||
public $type = 'proonlyjchpagecache';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if (!JCH_PRO) {
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
|
||||
return parent::getInput();
|
||||
}
|
||||
|
||||
protected function getOptions(): array
|
||||
{
|
||||
/** @var ModeSwitcher $modeSwitcher */
|
||||
$modeSwitcher = ContainerFactory::getContainer()->get(ModeSwitcher::class);
|
||||
$availablePlugins = $modeSwitcher->getAvailablePageCachePlugins();
|
||||
|
||||
foreach ($modeSwitcher->pageCachePlugins as $pageCache => $title) {
|
||||
if (in_array($pageCache, $availablePlugins)) {
|
||||
$options[] = HTMLHelper::_(
|
||||
'select.option',
|
||||
$pageCache,
|
||||
Text::_($title),
|
||||
'value',
|
||||
'text',
|
||||
false
|
||||
);
|
||||
} else {
|
||||
$options[] = HTMLHelper::_(
|
||||
'select.option',
|
||||
$pageCache,
|
||||
Text::_($title),
|
||||
'value',
|
||||
'text',
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$options = array_merge(parent::getOptions(), $options);
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
use Joomla\CMS\Form\FormHelper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
|
||||
FormHelper::loadFieldClass('list');
|
||||
|
||||
class JFormFieldProonlylist extends JFormFieldList
|
||||
{
|
||||
|
||||
public $type = 'proonlylist';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
|
||||
JFormHelper::loadFieldClass('radio');
|
||||
|
||||
class JFormFieldProonlyradio extends JFormFieldRadio
|
||||
{
|
||||
|
||||
public $type = 'proonlyradio';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
|
||||
JFormHelper::loadFieldClass('text');
|
||||
|
||||
class JFormFieldProonlytext extends JFormFieldText
|
||||
{
|
||||
|
||||
public $type = 'proonlytext';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::getInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\Core\Admin\Helper;
|
||||
|
||||
defined( '_JEXEC' ) or die( 'Restricted access' );
|
||||
|
||||
|
||||
class JFormFieldSmartCombine extends JFormFieldRadio
|
||||
{
|
||||
protected $type = 'SmartCombine';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
if ( ! JCH_PRO )
|
||||
{
|
||||
return Helper::proOnlyField();
|
||||
}
|
||||
else
|
||||
{
|
||||
return '<div id="div-' . $this->fieldname . '">' . parent::getInput() . '<img id="img-' . $this->fieldname . '" src="' . JUri::root() . 'media/com_jchoptimize/core/images/exclude-loader.gif" style="display: none;"/> <button id="btn-' . $this->fieldname . '" type="button" class="btn btn-sm btn-secondary" style="display: none;">Reprocess Smart Combine</button>
|
||||
</div>';
|
||||
}
|
||||
//</div>
|
||||
//</div>
|
||||
//<div class="control-group" style="display: none;">
|
||||
//<div class="control-label"></div>
|
||||
//<div class="controls">
|
||||
//<select id="jform_params_pro_smart_combine_values" name="jform[params][pro_smart_combine_values][]" style="display: none;" multiple="multiple" //></select>
|
||||
//</div>
|
||||
//</div>
|
||||
//<div class="control-group" style="display: none;">
|
||||
//<div class="control-label">
|
||||
//';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
defined( '_JEXEC' ) or die( 'Restricted access' );
|
||||
|
||||
|
||||
class JFormFieldSmartCombineValues extends JFormFieldList
|
||||
{
|
||||
protected $type = 'SmartCombineValues';
|
||||
|
||||
protected function getOptions()
|
||||
{
|
||||
$aOptions = array();
|
||||
|
||||
$aValueArray = $this->value;
|
||||
|
||||
if ( ! empty( $aValueArray ) )
|
||||
{
|
||||
foreach ( $aValueArray as $sValue )
|
||||
{
|
||||
$tmp = new stdClass();
|
||||
$tmp->value = $sValue;
|
||||
$tmp->text = '';
|
||||
$tmp->disable = '';
|
||||
$tmp->class = '';
|
||||
$tmp->selected = true;
|
||||
$tmp->checked = '';
|
||||
$tmp->onclick = '';
|
||||
$tmp->onchange = '';
|
||||
|
||||
$aOptions[] = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return $aOptions;
|
||||
}
|
||||
}
|
||||
1
administrator/components/com_jchoptimize/index.html
Normal file
1
administrator/components/com_jchoptimize/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<html><body style="background: #fff;"></body></html>
|
||||
38
administrator/components/com_jchoptimize/jchoptimize.php
Normal file
38
administrator/components/com_jchoptimize/jchoptimize.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads
|
||||
*
|
||||
* @package jchoptimize/joomla-platform
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use JchOptimize\ContainerFactory;
|
||||
use JchOptimize\ControllerResolver;
|
||||
use Joomla\CMS\Access\Exception\NotAllowed;
|
||||
use Joomla\CMS\Factory;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
require_once __DIR__ . '/autoload.php';
|
||||
|
||||
$app = Factory::getApplication();
|
||||
|
||||
if ($app->isClient('administrator') && ! $app->getIdentity()->authorise('core.manage', 'com_jchoptimize')) {
|
||||
throw new NotAllowed($app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$container = ContainerFactory::getContainer();
|
||||
$container->get(ControllerResolver::class)->resolve();
|
||||
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
|
||||
echo '<p> Error resolving controller: ' . $e->getMessage() . '</p>';
|
||||
} catch (\Exception $e) {
|
||||
echo '<p> Failed initializing component: ' . $e->getMessage() . '</p>';
|
||||
}
|
||||
68
administrator/components/com_jchoptimize/jchoptimize.xml
Normal file
68
administrator/components/com_jchoptimize/jchoptimize.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="component" method="upgrade" version="3.10">
|
||||
<name>com_jchoptimize</name>
|
||||
<!-- The following elements are optional and free of formatting constraints -->
|
||||
<creationDate>2023-08-09</creationDate>
|
||||
<author>Samuel Marshall</author>
|
||||
<authorEmail>samuel@jch-optimize.net</authorEmail>
|
||||
<authorUrl>https://www.jch-optimize.net</authorUrl>
|
||||
<copyright>(c) 2022 JCH Optimize Inc. All rights reserved</copyright>
|
||||
<license>GNU General Public License version 2 or later</license>
|
||||
<!-- The version string is recorded in the components table -->
|
||||
<version>8.0.6</version>
|
||||
<!-- The description is optional and defaults to the name -->
|
||||
<description>Performs several front end optimizations for fast site downloads</description>
|
||||
<variant>FREE</variant>
|
||||
|
||||
<files folder="frontend"/>
|
||||
|
||||
<media destination="com_jchoptimize" folder="media">
|
||||
<folder>assets</folder>
|
||||
<folder>assets2</folder>
|
||||
<folder>assets3</folder>
|
||||
<folder>cache</folder>
|
||||
<folder>css</folder>
|
||||
<folder>js</folder>
|
||||
<folder>lazysizes</folder>
|
||||
<folder>jquery-ui</folder>
|
||||
<folder>icons</folder>
|
||||
<folder>filetree</folder>
|
||||
<folder>core</folder>
|
||||
<folder>bootstrap</folder>
|
||||
</media>
|
||||
|
||||
<administration>
|
||||
<menu>COM_JCHOPTIMIZE</menu>
|
||||
<submenu>
|
||||
<menu view="ControlPanel">
|
||||
COM_JCHOPTIMIZE_CONTROLPANEL
|
||||
</menu>
|
||||
<menu view="OptimizeImages">
|
||||
COM_JCHOPTIMIZE_OPTIMIZEIMAGES
|
||||
</menu>
|
||||
<menu view="PageCache">
|
||||
COM_JCHOPTIMIZE_PAGECACHE
|
||||
</menu>
|
||||
</submenu>
|
||||
<files folder="backend">
|
||||
<!-- Admin Main File Copy Section -->
|
||||
<filename>index.html</filename>
|
||||
<filename>jchoptimize.php</filename>
|
||||
<filename>version.php</filename>
|
||||
<filename>autoload.php</filename>
|
||||
<filename>config.xml</filename>
|
||||
<filename>access.xml</filename>
|
||||
|
||||
<folder>fields</folder>
|
||||
<folder>lib</folder>
|
||||
</files>
|
||||
|
||||
<languages folder="language/backend">
|
||||
<language tag="en-GB">en-GB/en-GB.com_jchoptimize.ini</language>
|
||||
<language tag="en-GB">en-GB/en-GB.com_jchoptimize.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
</administration>
|
||||
|
||||
<scriptfile>script.com_jchoptimize.php</scriptfile>
|
||||
</extension>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2021 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize;
|
||||
|
||||
use JchOptimize\Core\Container\Container;
|
||||
use JchOptimize\Service\ConfigurationProvider;
|
||||
use JchOptimize\Service\DatabaseProvider;
|
||||
use JchOptimize\Service\LoggerProvider;
|
||||
use JchOptimize\Service\ModeSwitcherProvider;
|
||||
use JchOptimize\Service\MvcProvider;
|
||||
use JchOptimize\Service\ReCacheProvider;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted access');
|
||||
|
||||
/**
|
||||
* A class to easily fetch a Joomla\DI\Container with all dependencies registered.
|
||||
*/
|
||||
class ContainerFactory extends \JchOptimize\Core\Container\AbstractContainerFactory
|
||||
{
|
||||
protected function registerPlatformProviders(Container $container): void
|
||||
{
|
||||
$container->registerServiceProvider(new DatabaseProvider())->registerServiceProvider(new ConfigurationProvider())->registerServiceProvider(new LoggerProvider())->registerServiceProvider(new MvcProvider());
|
||||
if (\JCH_PRO) {
|
||||
$container->registerServiceProvider(new ReCacheProvider());
|
||||
$container->registerServiceProvider(new ModeSwitcherProvider());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Controller;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\Controller\AbstractController;
|
||||
use JchOptimize\Core\Admin\Ajax\Ajax as AdminAjax;
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted Access');
|
||||
class Ajax extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $taskMap;
|
||||
|
||||
public function __construct(Input $input, AdministratorApplication $app)
|
||||
{
|
||||
parent::__construct($input, $app);
|
||||
$this->taskMap = ['filetree' => 'doFileTree', 'multiselect' => 'doMultiSelect', 'optimizeimage' => 'doOptimizeImage', 'smartcombine' => 'doSmartCombine', 'garbagecron' => 'doGarbageCron'];
|
||||
}
|
||||
|
||||
public function execute(): bool
|
||||
{
|
||||
/** @var Input $input */
|
||||
$input = $this->getInput();
|
||||
|
||||
/** @var string $task */
|
||||
$task = $input->get('task');
|
||||
// @see self::doFileTree()
|
||||
// @see self::doMultiSelect()
|
||||
// @see self::doOptimizeImage()
|
||||
// @see self::doSmartCombine()
|
||||
// @see self::doGarbageCron()
|
||||
$this->{$this->taskMap[$task]}();
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
$app->close();
|
||||
|
||||
return \true;
|
||||
}
|
||||
|
||||
private function doFileTree(): void
|
||||
{
|
||||
echo AdminAjax::getInstance('FileTree')->run();
|
||||
}
|
||||
|
||||
private function doMultiSelect(): void
|
||||
{
|
||||
echo AdminAjax::getInstance('MultiSelect')->run();
|
||||
}
|
||||
|
||||
private function doOptimizeImage(): void
|
||||
{
|
||||
echo AdminAjax::getInstance('OptimizeImage')->run();
|
||||
}
|
||||
|
||||
private function doSmartCombine(): void
|
||||
{
|
||||
echo AdminAjax::getInstance('SmartCombine')->run();
|
||||
}
|
||||
|
||||
private function doGarbageCron(): void
|
||||
{
|
||||
echo AdminAjax::getInstance('GarbageCron')->run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2021 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Controller;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\Controller\AbstractController;
|
||||
use JchOptimize\Core\Exception\ExceptionInterface;
|
||||
use JchOptimize\Model\Configure;
|
||||
use Joomla\Application\AbstractApplication;
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted Access');
|
||||
class ApplyAutoSetting extends AbstractController
|
||||
{
|
||||
private Configure $model;
|
||||
|
||||
public function __construct(Configure $model, ?Input $input = null, ?AbstractApplication $app = null)
|
||||
{
|
||||
$this->model = $model;
|
||||
parent::__construct($input, $app);
|
||||
}
|
||||
|
||||
public function execute(): bool
|
||||
{
|
||||
/** @var Input $input */
|
||||
$input = $this->getInput();
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
|
||||
try {
|
||||
$this->model->applyAutoSettings((string) $input->get('autosetting', 's1'));
|
||||
} catch (ExceptionInterface $e) {
|
||||
}
|
||||
$body = \json_encode(['success' => \true]);
|
||||
$app->clearHeaders();
|
||||
$app->setHeader('Content-Type', 'application/json');
|
||||
$app->setHeader('Content-Length', (string) \strlen($body));
|
||||
$app->setBody($body);
|
||||
$app->allowCache(\false);
|
||||
echo $app->toString();
|
||||
$app->close();
|
||||
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2023 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Controller;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\Controller\AbstractController;
|
||||
use JchOptimize\Model\Cache;
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
class CacheInfo extends AbstractController
|
||||
{
|
||||
private Cache $cacheModel;
|
||||
|
||||
public function __construct(Cache $cacheModel, Input $input = null, AdministratorApplication $app = null)
|
||||
{
|
||||
$this->cacheModel = $cacheModel;
|
||||
parent::__construct($input, $app);
|
||||
}
|
||||
|
||||
public function execute(): bool
|
||||
{
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
[$size, $numFiles] = $this->cacheModel->getCacheSize();
|
||||
$body = \json_encode(['size' => $size, 'numFiles' => $numFiles]);
|
||||
$app->clearHeaders();
|
||||
$app->setHeader('Content-Type', 'application/json');
|
||||
$app->setHeader('Content-Length', (string) \strlen($body));
|
||||
$app->setBody($body);
|
||||
$app->allowCache(\false);
|
||||
echo $app->toString();
|
||||
$app->close();
|
||||
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Controller;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\Controller\AbstractController;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use JchOptimize\Core\Admin\Icons;
|
||||
use JchOptimize\Core\Cdn;
|
||||
use JchOptimize\Core\PageCache\CaptureCache;
|
||||
use JchOptimize\Joomla\Plugin\PluginHelper;
|
||||
use JchOptimize\Model\Updates;
|
||||
use JchOptimize\View\ControlPanelHtml;
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted Access');
|
||||
class ControlPanel extends AbstractController implements ContainerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
|
||||
private ControlPanelHtml $view;
|
||||
|
||||
private Updates $updatesModel;
|
||||
|
||||
private Icons $icons;
|
||||
private Cdn $cdn;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(Updates $updatesModel, ControlPanelHtml $view, Icons $icons, Cdn $cdn, Input $input = null, AdministratorApplication $app = null)
|
||||
{
|
||||
$this->updatesModel = $updatesModel;
|
||||
$this->view = $view;
|
||||
$this->icons = $icons;
|
||||
$this->cdn = $cdn;
|
||||
parent::__construct($input, $app);
|
||||
}
|
||||
|
||||
public function execute(): bool
|
||||
{
|
||||
$this->manageUpdates();
|
||||
if (\JCH_PRO) {
|
||||
/** @var CaptureCache $captureCache */
|
||||
$captureCache = $this->container->get(CaptureCache::class);
|
||||
$captureCache->updateHtaccess();
|
||||
}
|
||||
$this->cdn->updateHtaccess();
|
||||
$this->view->setData(['view' => 'ControlPanel', 'icons' => $this->icons]);
|
||||
$this->view->loadResources();
|
||||
$this->view->loadToolBar();
|
||||
if (!PluginHelper::isEnabled('system', 'jchoptimize')) {
|
||||
if (\JCH_PRO) {
|
||||
$editUrl = Route::_('index.php?option=com_jchoptimize&view=ModeSwitcher&task=setProduction&return='.\base64_encode((string) Uri::getInstance()), \false);
|
||||
} else {
|
||||
$editUrl = Route::_('index.php?option=com_plugins&filter[search]=JCH Optimize&filter[folder]=system');
|
||||
}
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
$app->enqueueMessage(Text::sprintf('COM_JCHOPTIMIZE_PLUGIN_NOT_ENABLED', $editUrl), 'warning');
|
||||
}
|
||||
echo $this->view->render();
|
||||
|
||||
return \true;
|
||||
}
|
||||
|
||||
private function manageUpdates(): void
|
||||
{
|
||||
$this->updatesModel->upgradeLicenseKey();
|
||||
$this->updatesModel->refreshUpdateSite();
|
||||
$this->updatesModel->removeObsoleteUpdateSites();
|
||||
if (\JCH_PRO) {
|
||||
if ('' == $this->updatesModel->getLicenseKey()) {
|
||||
if (\version_compare(JVERSION, '4.0', 'lt')) {
|
||||
$dlidEditUrl = Route::_('index.php?option=com_config&view=component&component=com_jchoptimize');
|
||||
} else {
|
||||
$dlidEditUrl = Route::_('index.php?option=com_installer&view=updatesites&filter[search]=JCH Optimize&filter[supported]=1');
|
||||
}
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
$app->enqueueMessage(Text::sprintf('COM_JCHOPTIMIZE_DOWNLOADID_MISSING', $dlidEditUrl), 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2021 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Controller;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\Controller\AbstractController;
|
||||
use JchOptimize\Core\Admin\Ajax\Ajax as AdminAjax;
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Language\Text as JText;
|
||||
use Joomla\CMS\Router\Route as JRoute;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted Access');
|
||||
class OptimizeImage extends AbstractController
|
||||
{
|
||||
public function execute(): bool
|
||||
{
|
||||
/** @var Input $input */
|
||||
$input = $this->getInput();
|
||||
|
||||
/** @var null|string $status */
|
||||
$status = $input->get('status', null);
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
if (\is_null($status)) {
|
||||
echo AdminAjax::getInstance('OptimizeImage')->run();
|
||||
$app->close();
|
||||
} else {
|
||||
if ('success' == $status) {
|
||||
$dir = \rtrim((string) $input->get('dir', ''), '/').'/';
|
||||
$cnt = (int) $input->get('cnt', 0);
|
||||
$app->enqueueMessage(\sprintf(JText::_('%1$d images optimized in %2$s'), $cnt, $dir));
|
||||
} else {
|
||||
$msg = (string) $input->get('msg', '');
|
||||
$app->enqueueMessage(JText::_('The Optimize Image function failed with message "'.$msg), 'error');
|
||||
}
|
||||
$app->redirect(JRoute::_('index.php?option=com_jchoptimize&view=OptimizeImages', \false));
|
||||
}
|
||||
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Controller;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\Controller\AbstractController;
|
||||
use JchOptimize\Core\Admin\Icons;
|
||||
use JchOptimize\Model\ApiParams;
|
||||
use JchOptimize\View\OptimizeImagesHtml;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted Access');
|
||||
class OptimizeImages extends AbstractController
|
||||
{
|
||||
private OptimizeImagesHtml $view;
|
||||
|
||||
private ApiParams $model;
|
||||
|
||||
private Icons $icons;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(ApiParams $model, OptimizeImagesHtml $view, Icons $icons)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->view = $view;
|
||||
$this->icons = $icons;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function execute(): bool
|
||||
{
|
||||
$this->view->setData(['view' => 'OptimizeImages', 'apiParams' => \json_encode($this->model->getCompParams()), 'icons' => $this->icons]);
|
||||
$this->view->loadResources();
|
||||
$this->view->loadToolBar();
|
||||
echo $this->view->render();
|
||||
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Controller;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\Controller\AbstractController;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use _JchOptimizeVendor\Laminas\Paginator\Adapter\ArrayAdapter;
|
||||
use _JchOptimizeVendor\Laminas\Paginator\Paginator;
|
||||
use JchOptimize\Joomla\Plugin\PluginHelper;
|
||||
use JchOptimize\Model\ModeSwitcher;
|
||||
use JchOptimize\Model\PageCache as PageCacheModel;
|
||||
use JchOptimize\Model\ReCache;
|
||||
use JchOptimize\View\PageCacheHtml;
|
||||
use Joomla\Application\AbstractApplication;
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted Access');
|
||||
class PageCache extends AbstractController implements ContainerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
private PageCacheHtml $view;
|
||||
private PageCacheModel $pageCacheModel;
|
||||
|
||||
public function __construct(PageCacheModel $pageCacheModel, PageCacheHtml $view, ?Input $input = null, ?AbstractApplication $app = null)
|
||||
{
|
||||
$this->pageCacheModel = $pageCacheModel;
|
||||
$this->view = $view;
|
||||
parent::__construct($input, $app);
|
||||
}
|
||||
|
||||
public function execute()
|
||||
{
|
||||
/** @var Input $input */
|
||||
$input = $this->getInput();
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
if ('remove' == $input->get('task')) {
|
||||
$success = $this->pageCacheModel->delete((array) $input->get('cid', []));
|
||||
}
|
||||
if ('deleteAll' == $input->get('task')) {
|
||||
$success = $this->pageCacheModel->deleteAll();
|
||||
}
|
||||
if (\JCH_PRO && 'recache' == $input->get('task')) {
|
||||
/** @var ReCache $reCacheModel */
|
||||
$reCacheModel = $this->container->get(ReCache::class);
|
||||
$redirectUrl = Route::_('index.php?option=com_jchoptimize&view=PageCache', \false, 0, \true);
|
||||
$reCacheModel->reCache($redirectUrl);
|
||||
}
|
||||
if (isset($success)) {
|
||||
if ($success) {
|
||||
$message = Text::_('COM_JCHOPTIMIZE_PAGECACHE_DELETED_SUCCESSFULLY');
|
||||
$messageType = 'success';
|
||||
} else {
|
||||
$message = Text::_('COM_JCHOPTIMIZE_PAGECACHE_DELETE_ERROR');
|
||||
$messageType = 'error';
|
||||
}
|
||||
$app->enqueueMessage($message, $messageType);
|
||||
$app->redirect(Route::_('index.php?option=com_jchoptimize&view=PageCache', \false));
|
||||
}
|
||||
$integratedPageCache = 'jchoptimizepagecache';
|
||||
if (\JCH_PRO) {
|
||||
/** @var ModeSwitcher $modeSwitcher */
|
||||
$modeSwitcher = $this->container->get(ModeSwitcher::class);
|
||||
$integratedPageCache = $modeSwitcher->getIntegratedPageCachePlugin();
|
||||
}
|
||||
if ('jchoptimizepagecache' == $integratedPageCache) {
|
||||
if (!PluginHelper::isEnabled('system', 'jchoptimizepagecache')) {
|
||||
if (\JCH_PRO === '1') {
|
||||
$editUrl = Route::_('index.php?option=com_jchoptimize&view=Utility&task=togglepagecache&return='.\base64_encode((string) Uri::getInstance()), \false);
|
||||
} else {
|
||||
$editUrl = Route::_('index.php?option=com_plugins&filter[search]=JCH Optimize Page Cache&filter[folder]=system');
|
||||
}
|
||||
$app->enqueueMessage(Text::sprintf('COM_JCHOPTIMIZE_PAGECACHE_NOT_ENABLED', $editUrl), 'warning');
|
||||
}
|
||||
} elseif (\JCH_PRO) {
|
||||
/** @var ModeSwitcher $modeSwitcher */
|
||||
$modeSwitcher = $this->container->get(ModeSwitcher::class);
|
||||
$app->enqueueMessage(Text::sprintf('COM_JCHOPTIMIZE_INTEGRATED_PAGE_CACHE_NOT_JCHOPTIMIZE', Text::_($modeSwitcher->pageCachePlugins[$integratedPageCache])), 'info');
|
||||
}
|
||||
|
||||
/** @var int $defaultListLimit */
|
||||
$defaultListLimit = $app->get('list_limit');
|
||||
$paginator = new Paginator(new ArrayAdapter($this->pageCacheModel->getItems()));
|
||||
$paginator->setCurrentPageNumber((int) $input->get('list_page', '1'))->setItemCountPerPage((int) $this->pageCacheModel->getState()->get('list_limit', $defaultListLimit));
|
||||
$this->view->setData(['items' => $paginator, 'view' => 'PageCache', 'paginator' => $paginator->getPages(), 'pageLink' => 'index.php?option=com_jchoptimize&view=PageCache', 'adapter' => $this->pageCacheModel->getAdaptorName(), 'httpRequest' => $this->pageCacheModel->isCaptureCacheEnabled()]);
|
||||
$this->view->renderStatefulElements($this->pageCacheModel->getState());
|
||||
$this->view->loadResources();
|
||||
$this->view->loadToolBar();
|
||||
echo $this->view->render();
|
||||
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Controller;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\Controller\AbstractController;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use JchOptimize\Core\Admin\Icons;
|
||||
use JchOptimize\Core\Exception\ExceptionInterface;
|
||||
use JchOptimize\Model\Configure;
|
||||
use JchOptimize\Model\ModeSwitcher;
|
||||
use JchOptimize\Platform\Cache;
|
||||
use Joomla\Application\AbstractApplication;
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted Access');
|
||||
class ToggleSetting extends AbstractController implements ContainerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
|
||||
private Configure $model;
|
||||
|
||||
public function __construct(Configure $model, ?Input $input = null, ?AbstractApplication $application = null)
|
||||
{
|
||||
$this->model = $model;
|
||||
parent::__construct($input, $application);
|
||||
}
|
||||
|
||||
public function execute(): bool
|
||||
{
|
||||
/** @var Input $input */
|
||||
$input = $this->getInput();
|
||||
|
||||
/** @var string $setting */
|
||||
$setting = $input->get('setting');
|
||||
|
||||
try {
|
||||
$this->model->toggleSetting($setting);
|
||||
} catch (ExceptionInterface $e) {
|
||||
}
|
||||
$currentSettingValue = (string) $this->model->getState()->get($setting);
|
||||
if ('integrated_page_cache_enable' == $setting) {
|
||||
$currentSettingValue = Cache::isPageCacheEnabled($this->model->getState());
|
||||
}
|
||||
$class = $currentSettingValue ? 'enabled' : 'disabled';
|
||||
$class2 = '';
|
||||
$auto = \false;
|
||||
$pageCacheStatus = '';
|
||||
$statusClass = '';
|
||||
if ('pro_reduce_unused_css' == $setting) {
|
||||
$class2 = $this->model->getState()->get('optimizeCssDelivery_enable') ? 'enabled' : 'disabled';
|
||||
}
|
||||
if ('optimizeCssDelivery_enable' == $setting) {
|
||||
$class2 = $this->model->getState()->get('pro_reduce_unused_css') ? 'enabled' : 'disabled';
|
||||
}
|
||||
if ('combine_files_enable' == $setting && $currentSettingValue) {
|
||||
$auto = $this->getEnabledAutoSetting();
|
||||
}
|
||||
if (JCH_PRO && 'integrated_page_cache_enable' == $setting) {
|
||||
/** @var ModeSwitcher $modeSwitcher */
|
||||
$modeSwitcher = $this->container->get(ModeSwitcher::class);
|
||||
[, , $pageCacheStatus, $statusClass] = $modeSwitcher->getIndicators();
|
||||
$pageCacheStatus = Text::sprintf('MOD_JCHMODESWITCHER_PAGECACHE_STATUS', $pageCacheStatus);
|
||||
}
|
||||
$body = \json_encode(['class' => $class, 'class2' => $class2, 'auto' => $auto, 'page_cache_status' => $pageCacheStatus, 'status_class' => $statusClass]);
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
$app->clearHeaders();
|
||||
$app->setHeader('Content-Type', 'application/json');
|
||||
$app->setHeader('Content-Length', (string) \strlen($body));
|
||||
$app->setBody($body);
|
||||
$app->allowCache(\false);
|
||||
echo $app->toString();
|
||||
$app->close();
|
||||
|
||||
return \true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|string
|
||||
*/
|
||||
private function getEnabledAutoSetting()
|
||||
{
|
||||
$autoSettingsMap = Icons::autoSettingsArrayMap();
|
||||
$autoSettingsInitialized = \array_map(function ($a) {
|
||||
return '0';
|
||||
}, $autoSettingsMap);
|
||||
$currentAutoSettings = \array_intersect_key($this->model->getState()->toArray(), $autoSettingsInitialized);
|
||||
// order array
|
||||
$orderedCurrentAutoSettings = \array_merge($autoSettingsInitialized, $currentAutoSettings);
|
||||
$autoSettings = ['minimum', 'intermediate', 'average', 'deluxe', 'premium', 'optimum'];
|
||||
for ($j = 0; $j < 6; ++$j) {
|
||||
if (\array_values($orderedCurrentAutoSettings) === \array_column($autoSettingsMap, 's'.($j + 1))) {
|
||||
return $autoSettings[$j];
|
||||
}
|
||||
}
|
||||
// No auto setting configured
|
||||
return \false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2020 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Controller;
|
||||
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\UploadedFile;
|
||||
use _JchOptimizeVendor\Joomla\Controller\AbstractController;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use JchOptimize\Core\Admin\Tasks;
|
||||
use JchOptimize\Model\BulkSettings;
|
||||
use JchOptimize\Model\Cache;
|
||||
use JchOptimize\Model\ModeSwitcher;
|
||||
use JchOptimize\Model\OrderPlugins;
|
||||
use JchOptimize\Model\ReCache;
|
||||
use JchOptimize\Model\TogglePlugins;
|
||||
use Joomla\CMS\Application\AdministratorApplication;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\Filesystem\File;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted Access');
|
||||
class Utility extends AbstractController implements ContainerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
|
||||
/**
|
||||
* Message to enqueue by application.
|
||||
*/
|
||||
private string $message = '';
|
||||
|
||||
/**
|
||||
* Message type.
|
||||
*/
|
||||
private string $messageType = 'success';
|
||||
|
||||
/**
|
||||
* Url to redirect to.
|
||||
*/
|
||||
private string $redirectUrl;
|
||||
|
||||
private OrderPlugins $orderPluginsModel;
|
||||
|
||||
private Cache $cacheModel;
|
||||
private TogglePlugins $togglePluginsModel;
|
||||
private BulkSettings $bulkSettings;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(OrderPlugins $orderPluginsModel, Cache $cacheModel, TogglePlugins $togglePluginsModel, BulkSettings $bulkSettings, ?Input $input = null, ?AdministratorApplication $app = null)
|
||||
{
|
||||
$this->orderPluginsModel = $orderPluginsModel;
|
||||
$this->cacheModel = $cacheModel;
|
||||
$this->togglePluginsModel = $togglePluginsModel;
|
||||
$this->bulkSettings = $bulkSettings;
|
||||
$this->redirectUrl = Route::_('index.php?option=com_jchoptimize', \false);
|
||||
parent::__construct($input, $app);
|
||||
}
|
||||
|
||||
public function execute()
|
||||
{
|
||||
/** @var Input $input */
|
||||
$input = $this->getInput();
|
||||
$this->{$input->get('task', 'default')}();
|
||||
|
||||
/** @var string $return */
|
||||
$return = $input->get('return', '', 'base64');
|
||||
if ($return) {
|
||||
$this->redirectUrl = Route::_(\base64_decode($return));
|
||||
}
|
||||
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
$app->enqueueMessage($this->message, $this->messageType);
|
||||
$app->redirect($this->redirectUrl);
|
||||
|
||||
return \true;
|
||||
}
|
||||
|
||||
private function browsercaching(): void
|
||||
{
|
||||
$success = null;
|
||||
$expires = Tasks::leverageBrowserCaching($success);
|
||||
if (\false === $success) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_LEVERAGEBROWSERCACHE_FAILED');
|
||||
$this->messageType = 'error';
|
||||
} elseif ('FILEDOESNTEXIST' === $expires) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_LEVERAGEBROWSERCACHE_FILEDOESNTEXIST');
|
||||
$this->messageType = 'warning';
|
||||
} elseif ('CODEUPDATEDSUCCESS' === $expires) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_LEVERAGEBROWSERCACHE_CODEUPDATEDSUCCESS');
|
||||
} elseif ('CODEUPDATEDFAIL' === $expires) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_LEVERAGEBROWSERCACHE_CODEUPDATEDFAIL');
|
||||
$this->messageType = 'notice';
|
||||
} else {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_LEVERAGEBROWSERCACHE_SUCCESS');
|
||||
}
|
||||
}
|
||||
|
||||
private function cleancache(): void
|
||||
{
|
||||
$deleted = $this->cacheModel->cleanCache();
|
||||
if (!$deleted) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_CACHECLEAN_FAILED');
|
||||
$this->messageType = 'error';
|
||||
} else {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_CACHECLEAN_SUCCESS');
|
||||
}
|
||||
}
|
||||
|
||||
private function togglepagecache(): void
|
||||
{
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_TOGGLE_PAGE_CACHE_FAILURE');
|
||||
$this->messageType = 'error';
|
||||
if (JCH_PRO === '1') {
|
||||
/** @var ModeSwitcher $modeSwitcher */
|
||||
$modeSwitcher = $this->container->get(ModeSwitcher::class);
|
||||
$result = $modeSwitcher->togglePageCacheState();
|
||||
} else {
|
||||
$result = $this->togglePluginsModel->togglePageCacheState('jchoptimizepagecache');
|
||||
}
|
||||
if ($result) {
|
||||
$this->message = Text::sprintf('COM_JCHOPTIMIZE_TOGGLE_PAGE_CACHE_SUCCESS', 'enabled');
|
||||
$this->messageType = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
private function keycache(): void
|
||||
{
|
||||
Tasks::generateNewCacheKey();
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_CACHE_KEY_GENERATED');
|
||||
}
|
||||
|
||||
private function orderplugins(): void
|
||||
{
|
||||
$saved = $this->orderPluginsModel->orderPlugins();
|
||||
if (\false === $saved) {
|
||||
$this->message = Text::_('JLIB_APPLICATION_ERROR_REORDER_FAILED');
|
||||
$this->messageType = 'error';
|
||||
} else {
|
||||
$this->message = Text::_('JLIB_APPLICATION_SUCCESS_ORDERING_SAVED');
|
||||
}
|
||||
}
|
||||
|
||||
private function restoreimages(): void
|
||||
{
|
||||
$mResult = Tasks::restoreBackupImages();
|
||||
if ('SOMEIMAGESDIDNTRESTORE' === $mResult) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_SOMERESTOREIMAGE_FAILED');
|
||||
$this->messageType = 'warning';
|
||||
} elseif ('BACKUPPATHDOESNTEXIST' === $mResult) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_BACKUPPATH_DOESNT_EXIST');
|
||||
$this->messageType = 'warning';
|
||||
} else {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_RESTOREIMAGE_SUCCESS');
|
||||
}
|
||||
$this->redirectUrl = Route::_('index.php?option=com_jchoptimize&view=OptimizeImages', \false);
|
||||
}
|
||||
|
||||
private function deletebackups(): void
|
||||
{
|
||||
$mResult = Tasks::deleteBackupImages();
|
||||
if (\false === $mResult) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_DELETEBACKUPS_FAILED');
|
||||
$this->messageType = 'error';
|
||||
} elseif ('BACKUPPATHDOESNTEXIST' === $mResult) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_BACKUPPATH_DOESNT_EXIST');
|
||||
$this->messageType = 'warning';
|
||||
} else {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_DELETEBACKUPS_SUCCESS');
|
||||
}
|
||||
$this->redirectUrl = Route::_('index.php?option=com_jchoptimize&view=OptimizeImages', \false);
|
||||
}
|
||||
|
||||
private function recache(): void
|
||||
{
|
||||
if (JCH_PRO === '1') {
|
||||
/** @var ReCache $reCacheModel */
|
||||
$reCacheModel = $this->container->get(ReCache::class);
|
||||
$redirectUrl = Route::_('index.php?option=com_jchoptimize', \false, 0, \true);
|
||||
$reCacheModel->reCache($redirectUrl);
|
||||
}
|
||||
$this->redirectUrl = Route::_('index.php?options=');
|
||||
}
|
||||
|
||||
private function importsettings()
|
||||
{
|
||||
$input = $this->getInput();
|
||||
\assert($input instanceof Input);
|
||||
|
||||
/** @psalm-var array{tmp_name:string, size:int, error:int, name:string|null, type:string|null}|null $file */
|
||||
$file = $input->files->get('file');
|
||||
if (empty($file)) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_UPLOAD_ERR_NO_FILE');
|
||||
$this->messageType = 'error';
|
||||
|
||||
return;
|
||||
}
|
||||
$uploadErrorMap = [\UPLOAD_ERR_OK => Text::_('COM_JCHOPTIMIZE_UPLOAD_ERR_OK'), \UPLOAD_ERR_INI_SIZE => Text::_('COM_JCHOPTIMIZE_UPLOAD_ERR_INI_SIZE'), \UPLOAD_ERR_FORM_SIZE => Text::_('COM_JCHOPTIMIZE_UPLOAD_ERR_FORM_SIZE'), \UPLOAD_ERR_PARTIAL => Text::_('COM_JCHOPTIMIZE_UPLOAD_ERR_PARTIAL'), \UPLOAD_ERR_NO_FILE => Text::_('COM_JCHOPTIMIZE_UPLOAD_ERR_NO_FILE'), \UPLOAD_ERR_NO_TMP_DIR => Text::_('COM_JCHOPTIMIZE_UPLOAD_ERR_NO_TMP_DIR'), \UPLOAD_ERR_CANT_WRITE => Text::_('COM_JCHOPTIMIZE_UPLOAD_ERR_CANT_WRITE'), \UPLOAD_ERR_EXTENSION => Text::_('COM_JCHOPTIMIZE_UPLOAD_ERR_EXTENSION')];
|
||||
|
||||
try {
|
||||
$uploadedFile = new UploadedFile($file['tmp_name'], $file['size'], $file['error'], $file['name'], $file['type']);
|
||||
if (\UPLOAD_ERR_OK !== $uploadedFile->getError()) {
|
||||
throw new \Exception(Text::_($uploadErrorMap[$uploadedFile->getError()]));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->message = Text::sprintf('COM_JCHOPTIMIZE_UPLOADED_FILE_ERROR', $e->getMessage());
|
||||
$this->messageType = 'error';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->bulkSettings->importSettings($uploadedFile);
|
||||
} catch (\Exception $e) {
|
||||
$this->message = Text::sprintf('COM_JCHOPTIMIZE_IMPORT_SETTINGS_ERROR', $e->getMessage());
|
||||
$this->messageType = 'error';
|
||||
|
||||
return;
|
||||
}
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_SUCCESSFULLY_IMPORTED_SETTINGS');
|
||||
}
|
||||
|
||||
private function exportsettings(): void
|
||||
{
|
||||
$file = $this->bulkSettings->exportSettings();
|
||||
if (\file_exists($file)) {
|
||||
/** @var AdministratorApplication $app */
|
||||
$app = $this->getApplication();
|
||||
$app->setHeader('Content-Description', 'FileTransfer');
|
||||
$app->setHeader('Content-Type', 'application/json');
|
||||
$app->setHeader('Content-Disposition', 'attachment; filename="'.\basename($file).'"');
|
||||
$app->setHeader('Expires', '0');
|
||||
$app->setHeader('Cache-Control', 'must-revalidate');
|
||||
$app->setHeader('Pragma', 'public');
|
||||
$app->setHeader('Content-Length', (string) \filesize($file));
|
||||
$app->sendHeaders();
|
||||
\ob_clean();
|
||||
\flush();
|
||||
\readfile($file);
|
||||
File::delete($file);
|
||||
$app->close();
|
||||
}
|
||||
}
|
||||
|
||||
private function setdefaultsettings()
|
||||
{
|
||||
try {
|
||||
$this->bulkSettings->setDefaultSettings();
|
||||
} catch (\Exception $e) {
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_RESTORE_DEFAULT_SETTINGS_FAILED');
|
||||
$this->messageType = 'error';
|
||||
|
||||
return;
|
||||
}
|
||||
$this->message = Text::_('COM_JCHOPTIMIZE_DEFAULT_SETTINGS_RESTORED');
|
||||
}
|
||||
|
||||
private function default(): void
|
||||
{
|
||||
$this->redirectUrl = Route::_('index.php?option=com_jchoptimize', \false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2021 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize;
|
||||
|
||||
use _JchOptimizeVendor\Psr\Container\ContainerExceptionInterface;
|
||||
use _JchOptimizeVendor\Psr\Container\ContainerInterface;
|
||||
use _JchOptimizeVendor\Psr\Container\NotFoundExceptionInterface;
|
||||
use Joomla\Input\Input;
|
||||
|
||||
\defined('_JEXEC') or exit('Restricted access');
|
||||
class ControllerResolver
|
||||
{
|
||||
private ContainerInterface $container;
|
||||
|
||||
private Input $input;
|
||||
|
||||
public function __construct(ContainerInterface $container, Input $input)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->input = $input;
|
||||
}
|
||||
|
||||
public function resolve()
|
||||
{
|
||||
$controller = $this->getController();
|
||||
if ($this->container->has($controller)) {
|
||||
try {
|
||||
\call_user_func([$this->container->get($controller), 'execute']);
|
||||
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
|
||||
throw new \InvalidArgumentException(\sprintf('Controller %s not found', $controller));
|
||||
}
|
||||
} else {
|
||||
throw new \InvalidArgumentException(\sprintf('Cannot resolve controller: %s', $controller));
|
||||
}
|
||||
}
|
||||
|
||||
private function getController(): string
|
||||
{
|
||||
// @var string
|
||||
return $this->input->get('view', 'ControlPanel');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Admin;
|
||||
|
||||
use _JchOptimizeVendor\GuzzleHttp\Client;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\Uri;
|
||||
use _JchOptimizeVendor\GuzzleHttp\RequestOptions;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use _JchOptimizeVendor\Psr\Http\Client\ClientInterface;
|
||||
use _JchOptimizeVendor\Spatie\Crawler\Crawler;
|
||||
use _JchOptimizeVendor\Spatie\Crawler\CrawlProfiles\CrawlInternalUrls;
|
||||
use JchOptimize\Core\Interfaces\Html;
|
||||
use JchOptimize\Core\Spatie\Crawlers\HtmlCollector;
|
||||
use JchOptimize\Core\Spatie\CrawlQueues\NonOptimizedCacheCrawlQueue;
|
||||
use JchOptimize\Core\SystemUri;
|
||||
use JchOptimize\Core\Uri\UriComparator;
|
||||
use Joomla\Registry\Registry;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
abstract class AbstractHtml implements Html, LoggerAwareInterface, ContainerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
use ContainerAwareTrait;
|
||||
|
||||
/**
|
||||
* JCH Optimize settings.
|
||||
*/
|
||||
protected Registry $params;
|
||||
|
||||
/**
|
||||
* Http client transporter object.
|
||||
*
|
||||
* @var Client&ClientInterface
|
||||
*/
|
||||
protected $http;
|
||||
private bool $logging = \false;
|
||||
|
||||
/**
|
||||
* @param Client&ClientInterface $http
|
||||
*/
|
||||
public function __construct(Registry $params, $http)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->http = $http;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{base_url?:string, crawl_limit?:int} $options
|
||||
*
|
||||
* @return array{list<array{url:string, html:string}>, list<Json>}
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getCrawledHtmls(array $options = []): array
|
||||
{
|
||||
$defaultOptions = ['crawl_limit' => 10, 'base_url' => SystemUri::currentBaseFull()];
|
||||
$options = \array_merge($defaultOptions, $options);
|
||||
if (UriComparator::isCrossOrigin(new Uri($options['base_url']))) {
|
||||
throw new \Exception('Cross origin URLs not allowed');
|
||||
}
|
||||
$htmlCollector = new HtmlCollector();
|
||||
$this->logger = $this->logger ?? new NullLogger();
|
||||
$logger = $this->logging ? $this->logger : new NullLogger();
|
||||
$htmlCollector->setLogger($logger);
|
||||
$clientOptions = [RequestOptions::COOKIES => \false, RequestOptions::CONNECT_TIMEOUT => 10, RequestOptions::TIMEOUT => 10, RequestOptions::ALLOW_REDIRECTS => \true, RequestOptions::HEADERS => ['User-Agent' => $_SERVER['HTTP_USER_AGENT'] ?? '*']];
|
||||
Crawler::create($clientOptions)->setCrawlObserver($htmlCollector)->setParseableMimeTypes(['text/html'])->ignoreRobots()->setTotalCrawlLimit($options['crawl_limit'])->setCrawlQueue($this->container->get(NonOptimizedCacheCrawlQueue::class))->setCrawlProfile(new CrawlInternalUrls($options['base_url']))->startCrawling($options['base_url']);
|
||||
|
||||
return [$htmlCollector->getHtmls(), $htmlCollector->getMessages()];
|
||||
}
|
||||
|
||||
public function setLogging(bool $state = \true): void
|
||||
{
|
||||
$this->logging = $state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Admin\Ajax;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use JchOptimize\ContainerFactory;
|
||||
use JchOptimize\Core\Admin\Json;
|
||||
use Joomla\Input\Input;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
abstract class Ajax implements ContainerAwareInterface, LoggerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
use LoggerAwareTrait;
|
||||
|
||||
protected Input $input;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
\ini_set('pcre.backtrack_limit', '1000000');
|
||||
\ini_set('pcre.recursion_limit', '1000000');
|
||||
if (!\JCH_DEVELOP) {
|
||||
\error_reporting(0);
|
||||
}
|
||||
if (\version_compare(\PHP_VERSION, '7.0.0', '>=')) {
|
||||
\ini_set('pcre.jit', '0');
|
||||
}
|
||||
$this->container = ContainerFactory::getContainer();
|
||||
$this->logger = $this->container->get(LoggerInterface::class);
|
||||
$this->input = $this->container->get(Input::class);
|
||||
}
|
||||
|
||||
public static function getInstance(string $sClass): Ajax
|
||||
{
|
||||
$sFullClass = '\\JchOptimize\\Core\\Admin\\Ajax\\'.$sClass;
|
||||
// @var Ajax
|
||||
return new $sFullClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Json|string
|
||||
*/
|
||||
abstract public function run();
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Admin\Ajax;
|
||||
|
||||
use JchOptimize\Core\Admin\Helper as AdminHelper;
|
||||
use JchOptimize\Platform\Paths;
|
||||
use JchOptimize\Platform\Utility;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class FileTree extends \JchOptimize\Core\Admin\Ajax\Ajax
|
||||
{
|
||||
public function run(): string
|
||||
{
|
||||
// Website document root
|
||||
$root = Paths::rootPath();
|
||||
// The expanded directory in the folder tree
|
||||
$dir = \urldecode($this->input->getString('dir', '')).'/';
|
||||
// Which side of the Explorer view are we rendering? Folder tree or subdirectories and files
|
||||
$view = \urldecode($this->input->getWord('jchview', ''));
|
||||
// Will be set to 1 if this is the root directory
|
||||
$initial = \urldecode($this->input->getString('initial', '0'));
|
||||
$files = \array_diff(\scandir($root.$dir), ['..', '.']);
|
||||
$directories = [];
|
||||
$imageFiles = [];
|
||||
$i = 0;
|
||||
$j = 0;
|
||||
foreach ($files as $file) {
|
||||
if (\is_dir($root.$dir.$file) && 'jch_optimize_backup_images' != $file && '.jch' != $file) {
|
||||
/*if ($i > 500) {
|
||||
if ($j > 1000) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}*/
|
||||
$directories[$i]['name'] = $file;
|
||||
$directories[$i]['file_path'] = $dir.$file;
|
||||
++$i;
|
||||
} elseif ('tree' != $view && \preg_match('#\\.(?:gif|jpe?g|png)$#i', $file) && @\file_exists($root.$dir.$file)) {
|
||||
/* if ($j > 1000) {
|
||||
if ($i > 500) {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
} */
|
||||
$imageFiles[$j]['ext'] = \preg_replace('/^.*\\./', '', $file);
|
||||
$imageFiles[$j]['name'] = $file;
|
||||
$imageFiles[$j]['file_path'] = $dir.$file;
|
||||
$imageFiles[$j]['optimized'] = AdminHelper::isAlreadyOptimized($root.$dir.$file);
|
||||
++$j;
|
||||
}
|
||||
}
|
||||
$items = function (string $view, array $directories, array $imageFiles): string {
|
||||
$item = '<ul class="jqueryFileTree">';
|
||||
foreach ($directories as $directory) {
|
||||
$item .= '<li class="directory collapsed">';
|
||||
if ('tree' != $view) {
|
||||
$item .= '<input type="checkbox" value="'.$directory['file_path'].'">';
|
||||
}
|
||||
$item .= '<a href="#" data-url="'.$directory['file_path'].'">'.\htmlentities($directory['name']).'</a>';
|
||||
$item .= '</li>';
|
||||
}
|
||||
if ('tree' != $view) {
|
||||
foreach ($imageFiles as $image) {
|
||||
$style = $image['optimized'] ? ' class="already-optimized"' : '';
|
||||
$file_name = \htmlentities($image['name']);
|
||||
$item .= <<<HTML
|
||||
<li class="file ext_{$image['ext']}">
|
||||
\t<input type="checkbox" value="{$image['file_path']}">
|
||||
\t<span{$style}><a href="#" data-url="{$image['file_path']}">{$file_name}</a> </span>\t
|
||||
\t<span><input type="text" size="10" maxlength="5" name="width"></span>
|
||||
\t<span><input type="text" size="10" maxlength="5" name="height"></span>
|
||||
</li>\t\t
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
$item .= '</ul>';
|
||||
|
||||
return $item;
|
||||
};
|
||||
// generate the response
|
||||
$response = '';
|
||||
if ('tree' != $view) {
|
||||
$width = Utility::translate('Width');
|
||||
$height = Utility::translate('Height');
|
||||
$response .= <<<HTML
|
||||
<div id="files-container-header">
|
||||
<ul class="jqueryFileTree">
|
||||
<li class="check-all">
|
||||
<input type="checkbox"><span><em>Check all</em></span>
|
||||
<span><em>{$width}</em></span>
|
||||
<span><em>{$height}</em></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
if ($initial && 'tree' == $view) {
|
||||
$response .= <<<HTML
|
||||
<div class="files-content">
|
||||
<ul class="jqueryFileTree">
|
||||
<li class="directory expanded root"><a href="#" data-root="{$root}" data-url=""><root></a>
|
||||
|
||||
{$items($view, $directories, $imageFiles)}
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
HTML;
|
||||
} elseif ('tree' != $view) {
|
||||
$response .= <<<HTML
|
||||
\t<div class="files-content">
|
||||
\t
|
||||
\t{$items($view, $directories, $imageFiles)}
|
||||
\t
|
||||
\t</div>
|
||||
HTML;
|
||||
} else {
|
||||
$response .= $items($view, $directories, $imageFiles);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dir
|
||||
* @param string $view
|
||||
* @param string $path
|
||||
*/
|
||||
private function item(string $file, $dir, $view, $path): string
|
||||
{
|
||||
$file_path = $dir.$file;
|
||||
$root = Paths::rootPath();
|
||||
$anchor = '<a href="#" data-url="'.$file_path.'">'.\htmlentities($file).'</a>';
|
||||
$html = '';
|
||||
if ('tree' == $view) {
|
||||
$html .= $anchor;
|
||||
} else {
|
||||
$html .= '<input type="checkbox" value="'.$file_path.'">';
|
||||
if ('dir' == $path) {
|
||||
$html .= $anchor;
|
||||
} else {
|
||||
$html .= '<span';
|
||||
if (AdminHelper::isAlreadyOptimized($root.$dir.$file)) {
|
||||
$html .= ' class="already-optimized"';
|
||||
}
|
||||
$html .= '>'.\htmlentities($file).'</span><span><input type="text" size="10" maxlength="5" name="width" ></span><span><input type="text" size="10" maxlength="5" name="height" ></span>';
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Admin\Ajax;
|
||||
|
||||
use JchOptimize\Core\Admin\Json;
|
||||
use JchOptimize\Core\Admin\MultiSelectItems;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class MultiSelect extends \JchOptimize\Core\Admin\Ajax\Ajax
|
||||
{
|
||||
public function run(): Json
|
||||
{
|
||||
$aData = $this->input->get('data', [], 'array');
|
||||
$container = $this->getContainer();
|
||||
|
||||
/** @var MultiSelectItems $oAdmin */
|
||||
$oAdmin = $container->buildObject(MultiSelectItems::class);
|
||||
|
||||
try {
|
||||
$oAdmin->getAdminLinks();
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
$response = [];
|
||||
foreach ($aData as $sData) {
|
||||
$options = $oAdmin->prepareFieldOptions($sData['type'], $sData['param'], $sData['group'], \false);
|
||||
$response[$sData['id']] = new Json($options);
|
||||
}
|
||||
|
||||
return new Json($response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Admin;
|
||||
|
||||
use _JchOptimizeVendor\Composer\CaBundle\CaBundle;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Client;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\Utils as GuzzleUtils;
|
||||
use _JchOptimizeVendor\GuzzleHttp\RequestOptions;
|
||||
use JchOptimize\Core\SystemUri;
|
||||
use JchOptimize\Core\Uri\Utils;
|
||||
use JchOptimize\Platform\Paths;
|
||||
use Joomla\Filesystem\Exception\FilesystemException;
|
||||
use Joomla\Filesystem\File;
|
||||
use Joomla\Filesystem\Folder;
|
||||
|
||||
use function file;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Helper
|
||||
{
|
||||
/**
|
||||
* @return null|array|string|string[]
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public static function expandFileNameLegacy($sFile)
|
||||
{
|
||||
$sSanitizedFile = \str_replace('//', '/', $sFile);
|
||||
$aPathParts = \pathinfo($sSanitizedFile);
|
||||
$sRelFile = \str_replace(['_', '//'], ['/', '_'], $aPathParts['basename']);
|
||||
|
||||
return \preg_replace('#^'.\preg_quote(\ltrim(SystemUri::basePath(), \DIRECTORY_SEPARATOR)).'#', Paths::rootPath().\DIRECTORY_SEPARATOR, $sRelFile);
|
||||
}
|
||||
|
||||
public static function expandFileName(string $file): string
|
||||
{
|
||||
$sanitizedFile = \str_replace('//', '/', $file);
|
||||
$aPathParts = \pathinfo($sanitizedFile);
|
||||
$expandedBasename = \str_replace(['_', '//'], [\DIRECTORY_SEPARATOR, '_'], $aPathParts['basename']);
|
||||
|
||||
return Paths::rootPath().\DIRECTORY_SEPARATOR.\ltrim($expandedBasename, \DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|(mixed|string)[]|string $dest
|
||||
*
|
||||
* @psalm-param array<mixed|string>|null|string $dest
|
||||
*/
|
||||
public static function copyImage(string $src, $dest): bool
|
||||
{
|
||||
try {
|
||||
$client = new Client([RequestOptions::VERIFY => CaBundle::getBundledCaBundlePath()]);
|
||||
$uri = Utils::uriFor($src);
|
||||
if (0 === \strpos($uri->getScheme(), 'http')) {
|
||||
$response = $client->get($uri);
|
||||
$srcStream = $response->getBody();
|
||||
} else {
|
||||
$srcStream = GuzzleUtils::streamFor(GuzzleUtils::tryFopen($src, 'rb'));
|
||||
}
|
||||
// Let's ensure parent directory for dest exists
|
||||
if (!\file_exists(\dirname($dest))) {
|
||||
Folder::create(\dirname($dest));
|
||||
}
|
||||
GuzzleUtils::copyToStream(GuzzleUtils::streamFor($srcStream), GuzzleUtils::streamFor(GuzzleUtils::tryFopen($dest, 'wb')));
|
||||
} catch (\Exception $e) {
|
||||
return \false;
|
||||
}
|
||||
|
||||
return \true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public static function contractFileNameLegacy(string $fileName): string
|
||||
{
|
||||
return \str_replace([Paths::rootPath().\DIRECTORY_SEPARATOR, '_', \DIRECTORY_SEPARATOR], [\ltrim(SystemUri::basePath(), \DIRECTORY_SEPARATOR), '__', '_'], $fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 'contracted' path of the file relative to the Uri base as opposed to the web root as in legacy.
|
||||
*/
|
||||
public static function contractFileName(string $filePath): string
|
||||
{
|
||||
$difference = self::subtractPath($filePath, Paths::rootPath().'/');
|
||||
|
||||
return \str_replace(['_', '\\', '/'], ['__', '_', '_'], $difference);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{path:string}
|
||||
*/
|
||||
public static function prepareImageUrl(string $image): array
|
||||
{
|
||||
// return array('path' => Utility::encrypt($image));
|
||||
return ['path' => $image];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param false|string $sValue
|
||||
*
|
||||
* @return float|int
|
||||
*/
|
||||
public static function stringToBytes($sValue)
|
||||
{
|
||||
$sUnit = \strtolower(\substr($sValue, -1, 1));
|
||||
|
||||
return (int) $sValue * \pow(1024, \array_search($sUnit, [1 => 'k', 'm', 'g']));
|
||||
}
|
||||
|
||||
public static function markOptimized(string $file): void
|
||||
{
|
||||
$metafile = self::getMetaFile();
|
||||
$metafileDir = \dirname($metafile);
|
||||
|
||||
try {
|
||||
if (!\file_exists($metafileDir.'/index.html') || !\file_exists($metafileDir.'/.htaccess')) {
|
||||
$html = <<<'HTML'
|
||||
<html><head><title></title></head><body></body></html>
|
||||
HTML;
|
||||
File::write($metafileDir.'/index.html', $html);
|
||||
$htaccess = <<<APACHECONFIG
|
||||
order deny,allow
|
||||
deny from all
|
||||
|
||||
<IfModule mod_autoindex.c>
|
||||
\tOptions -Indexes
|
||||
</IfModule>
|
||||
APACHECONFIG;
|
||||
File::write($metafileDir.'/.htaccess', $htaccess);
|
||||
}
|
||||
} catch (FilesystemException $e) {
|
||||
}
|
||||
if (\is_dir($metafileDir)) {
|
||||
$file = self::normalizePath($file);
|
||||
$file = '[ROOT]/'.\ltrim(self::subtractPath($file, Paths::rootPath()), '/').\PHP_EOL;
|
||||
if (!\in_array($file, self::getOptimizedFiles())) {
|
||||
File::write($metafile, $file, \false, \true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getMetaFile(): string
|
||||
{
|
||||
return Paths::rootPath().\DIRECTORY_SEPARATOR.'.jch'.\DIRECTORY_SEPARATOR.'jch-api2.txt';
|
||||
}
|
||||
|
||||
public static function getOptimizedFiles(): array
|
||||
{
|
||||
static $optimizeds = null;
|
||||
if (\is_null($optimizeds)) {
|
||||
$optimizeds = self::getCurrentOptimizedFiles();
|
||||
}
|
||||
|
||||
return $optimizeds;
|
||||
}
|
||||
|
||||
public static function filterOptimizedFiles(array $images): array
|
||||
{
|
||||
$normalizedImages = \array_map(function ($image) {
|
||||
return self::normalizePath($image);
|
||||
}, $images);
|
||||
|
||||
return \array_diff($normalizedImages, self::getOptimizedFiles());
|
||||
}
|
||||
|
||||
public static function isAlreadyOptimized(string $image): bool
|
||||
{
|
||||
return \in_array(self::normalizePath($image), self::getOptimizedFiles());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|(mixed|string)[]|string $file
|
||||
*
|
||||
* @psalm-param array<mixed|string>|null|string $file
|
||||
*/
|
||||
public static function unmarkOptimized($file)
|
||||
{
|
||||
$metafile = self::getMetaFile();
|
||||
if (!@\file_exists($metafile)) {
|
||||
return;
|
||||
}
|
||||
$aOptimizedFile = self::getCurrentOptimizedFiles();
|
||||
if (($key = \array_search($file, $aOptimizedFile)) !== \false) {
|
||||
unset($aOptimizedFile[$key]);
|
||||
}
|
||||
$sContents = \implode(\PHP_EOL, $aOptimizedFile).\PHP_EOL;
|
||||
\file_put_contents($metafile, $sContents);
|
||||
}
|
||||
|
||||
public static function proOnlyField(): string
|
||||
{
|
||||
return '<fieldset style="padding: 5px 5px 0 0; color:darkred"><em>Only available in Pro Version!</em></fieldset>';
|
||||
}
|
||||
|
||||
public static function subtractPath(string $minuend, string $subtrahend): string
|
||||
{
|
||||
$minuendNormalized = self::normalizePath($minuend);
|
||||
$subtrahendNormalized = self::normalizePath($subtrahend);
|
||||
if (0 === \strpos($minuendNormalized, $subtrahendNormalized)) {
|
||||
return \substr($minuend, \strlen($subtrahend));
|
||||
}
|
||||
|
||||
return $minuend;
|
||||
}
|
||||
|
||||
public static function normalizePath(string $path): string
|
||||
{
|
||||
return \rawurldecode((string) Utils::uriFor($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
protected static function getCurrentOptimizedFiles(): array
|
||||
{
|
||||
$metafile = self::getMetaFile();
|
||||
if (!\file_exists($metafile)) {
|
||||
return [];
|
||||
}
|
||||
$optimizeds = \file($metafile, \FILE_IGNORE_NEW_LINES);
|
||||
if (\false === $optimizeds) {
|
||||
$optimizeds = [];
|
||||
} else {
|
||||
$optimizeds = \array_map(function (string $value) {
|
||||
return \str_replace('[ROOT]', Paths::rootPath(), $value);
|
||||
}, $optimizeds);
|
||||
}
|
||||
|
||||
return $optimizeds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Admin;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use JchOptimize\Model\ModeSwitcher;
|
||||
use JchOptimize\Platform\Cache;
|
||||
use JchOptimize\Platform\Paths;
|
||||
use JchOptimize\Platform\Utility;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Icons implements ContainerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
private Registry $params;
|
||||
|
||||
public function __construct(Registry $params)
|
||||
{
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
public static function getAutoSettingsArray(): array
|
||||
{
|
||||
return [['name' => 'Minimum', 'icon' => 'minimum.png', 'setting' => 1], ['name' => 'Intermediate', 'icon' => 'intermediate.png', 'setting' => 2], ['name' => 'Average', 'icon' => 'average.png', 'setting' => 3], ['name' => 'Deluxe', 'icon' => 'deluxe.png', 'setting' => 4], ['name' => 'Premium', 'icon' => 'premium.png', 'setting' => 5], ['name' => 'Optimum', 'icon' => 'optimum.png', 'setting' => 6]];
|
||||
}
|
||||
|
||||
public function printIconsHTML($buttons): string
|
||||
{
|
||||
$sIconsHTML = '';
|
||||
foreach ($buttons as $button) {
|
||||
$sContentAttr = Utility::bsTooltipContentAttribute();
|
||||
$sTooltip = @$button['tooltip'] ? " class=\"hasPopover fig-caption\" title=\"{$button['name']}\" {$sContentAttr}=\"{$button['tooltip']}\" " : ' class="fig-caption"';
|
||||
$sIconSrc = Paths::iconsUrl().'/'.$button['icon'];
|
||||
$sToggle = '<span class="toggle-wrapper" ><i class="toggle fa"></i></span>';
|
||||
$onClickFalse = '';
|
||||
if (!JCH_PRO && !empty($button['proonly'])) {
|
||||
$button['link'] = '';
|
||||
$button['script'] = '';
|
||||
$button['class'] = 'disabled proonly';
|
||||
$sToggle = '<span id="proonly-span"><em>(Pro Only)</em></span>';
|
||||
$onClickFalse = ' onclick="return false;"';
|
||||
}
|
||||
$sIconsHTML .= <<<HTML
|
||||
<figure id="{$button['id']}" class="icon {$button['class']}">
|
||||
\t<a href="{$button['link']}" class="btn" {$button['script']}{$onClickFalse}>
|
||||
\t\t<img src="{$sIconSrc}" alt="" width="50" height="50" />
|
||||
\t\t<span{$sTooltip}>{$button['name']}</span>
|
||||
\t\t{$sToggle}
|
||||
\t</a>
|
||||
</figure>
|
||||
|
||||
HTML;
|
||||
}
|
||||
|
||||
return $sIconsHTML;
|
||||
}
|
||||
|
||||
public function compileAutoSettingsIcons($settings): array
|
||||
{
|
||||
$buttons = [];
|
||||
for ($i = 0; $i < \count($settings); ++$i) {
|
||||
$id = $this->generateIdFromName($settings[$i]['name']);
|
||||
$buttons[$i]['link'] = '';
|
||||
$buttons[$i]['icon'] = $settings[$i]['icon'];
|
||||
$buttons[$i]['name'] = $settings[$i]['name'];
|
||||
$buttons[$i]['script'] = "onclick=\"jchPlatform.applyAutoSettings('{$settings[$i]['setting']}', '{$id}', '".Utility::getNonce('s'.$settings[$i]['setting'])."'); return false;\"";
|
||||
$buttons[$i]['id'] = $id;
|
||||
$buttons[$i]['class'] = 'auto-setting disabled';
|
||||
$buttons[$i]['tooltip'] = \htmlspecialchars(self::generateAutoSettingTooltip($settings[$i]['setting']));
|
||||
}
|
||||
$sCombineFilesEnable = $this->params->get('combine_files_enable', '0');
|
||||
$aParamsArray = $this->params->toArray();
|
||||
$aAutoSettings = self::autoSettingsArrayMap();
|
||||
$aAutoSettingsInit = \array_map(function ($a) {
|
||||
return '0';
|
||||
}, $aAutoSettings);
|
||||
$aCurrentAutoSettings = \array_intersect_key($aParamsArray, $aAutoSettingsInit);
|
||||
// order array
|
||||
$aCurrentAutoSettings = \array_merge($aAutoSettingsInit, $aCurrentAutoSettings);
|
||||
if ($sCombineFilesEnable) {
|
||||
for ($j = 0; $j < 6; ++$j) {
|
||||
if (\array_values($aCurrentAutoSettings) === \array_column($aAutoSettings, 's'.($j + 1))) {
|
||||
$buttons[$j]['class'] = 'auto-setting enabled';
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
public static function autoSettingsArrayMap(): array
|
||||
{
|
||||
return ['css' => ['s1' => '1', 's2' => '1', 's3' => '1', 's4' => '1', 's5' => '1', 's6' => '1'], 'javascript' => ['s1' => '1', 's2' => '1', 's3' => '1', 's4' => '1', 's5' => '1', 's6' => '1'], 'gzip' => ['s1' => '0', 's2' => '1', 's3' => '1', 's4' => '1', 's5' => '1', 's6' => '1'], 'css_minify' => ['s1' => '0', 's2' => '1', 's3' => '1', 's4' => '1', 's5' => '1', 's6' => '1'], 'js_minify' => ['s1' => '0', 's2' => '1', 's3' => '1', 's4' => '1', 's5' => '1', 's6' => '1'], 'html_minify' => ['s1' => '0', 's2' => '1', 's3' => '1', 's4' => '1', 's5' => '1', 's6' => '1'], 'includeAllExtensions' => ['s1' => '0', 's2' => '0', 's3' => '1', 's4' => '1', 's5' => '1', 's6' => '1'], 'replaceImports' => ['s1' => '0', 's2' => '0', 's3' => '0', 's4' => '1', 's5' => '1', 's6' => '1'], 'phpAndExternal' => ['s1' => '0', 's2' => '0', 's3' => '0', 's4' => '1', 's5' => '1', 's6' => '1'], 'inlineStyle' => ['s1' => '0', 's2' => '0', 's3' => '0', 's4' => '1', 's5' => '1', 's6' => '1'], 'inlineScripts' => ['s1' => '0', 's2' => '0', 's3' => '0', 's4' => '1', 's5' => '1', 's6' => '1'], 'bottom_js' => ['s1' => '0', 's2' => '0', 's3' => '0', 's4' => '0', 's5' => '1', 's6' => '1'], 'loadAsynchronous' => ['s1' => '0', 's2' => '0', 's3' => '0', 's4' => '0', 's5' => '0', 's6' => '1']];
|
||||
}
|
||||
|
||||
public function getApi2UtilityArray(): array
|
||||
{
|
||||
return self::getUtilityArray(['restoreimages', 'deletebackups']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $actions
|
||||
*
|
||||
* @psalm-param list{0?: 'restoreimages', 1?: 'deletebackups'} $actions
|
||||
*/
|
||||
public function getUtilityArray(array $actions = []): array
|
||||
{
|
||||
$aUtilities = [$action = 'browsercaching' => ['action' => $action, 'icon' => 'browser_caching.png', 'name' => 'Optimize .htaccess', 'tooltip' => Utility::translate('Use this button to add codes to your htaccess file to enable leverage browser caching and gzip compression.')], $action = 'filepermissions' => ['action' => $action, 'icon' => 'file_permissions.png', 'name' => 'Fix file permissions', 'tooltip' => Utility::translate("If your site has lost CSS formatting after enabling the plugin, the problem could be that the plugin files were installed with incorrect file permissions so the browser cannot access the cached combined file. Click here to correct the plugin's file permissions.")], $action = 'cleancache' => ['action' => $action, 'icon' => 'clean_cache.png', 'name' => 'Clean Cache', 'tooltip' => Utility::translate("Click this button to clean the plugin's cache and page cache. If you have edited any CSS or JavaScript files you need to clean the cache so the changes can be visible.")], $action = 'orderplugins' => ['action' => $action, 'icon' => 'order_plugin.png', 'name' => 'Order Plugin', 'tooltip' => Utility::translate('The published order of the plugin is important! When you click on this icon, it will attempt to order the plugin correctly.')], $action = 'keycache' => ['action' => $action, 'icon' => 'keycache.png', 'name' => 'Generate new cache key', 'tooltip' => Utility::translate("If you've made any changes to your files generate a new cache key to counter browser caching of the old content.")], $action = 'recache' => ['action' => $action, 'icon' => 'recache.png', 'name' => 'Recache', 'proonly' => \true, 'tooltip' => Utility::translate('Rebuild the cache for all the pages of the site.')], $action = 'bulksettings' => ['action' => $action, 'icon' => 'bulk_settings.png', 'name' => 'Bulk Setting Operations', 'tooltip' => Utility::translate('Opens a modal that provides options to import/export settings, or restore to default values.'), 'script' => 'onclick="loadBulkSettingsModal(); return false;"'], $action = 'restoreimages' => ['action' => $action, 'icon' => 'restoreimages.png', 'name' => 'Restore Original Images,', 'tooltip' => Utility::translate("If you're not satisfied with the images that were optimized you can restore the original ones by clicking this button if they were not deleted. This will also remove any webp image created from the restored file."), 'proonly' => \true], $action = 'deletebackups' => ['action' => $action, 'icon' => 'deletebackups.png', 'name' => 'Delete Backup Images', 'tooltip' => Utility::translate("This will permanently delete the images that were backed up. There's no way to undo this so be sure you're satisfied with the ones that were optimized before clicking this button."), 'proonly' => \true, 'script' => 'onclick="return confirm(\'Are you sure? This cannot be undone!\');"']];
|
||||
if (empty($actions)) {
|
||||
return $aUtilities;
|
||||
}
|
||||
|
||||
return \array_intersect_key($aUtilities, \array_flip($actions));
|
||||
}
|
||||
|
||||
public function compileUtilityIcons($utilities): array
|
||||
{
|
||||
$icons = [];
|
||||
$i = 0;
|
||||
foreach ($utilities as $utility) {
|
||||
$icons[$i]['link'] = Paths::adminController($utility['action']);
|
||||
$icons[$i]['icon'] = $utility['icon'];
|
||||
$icons[$i]['name'] = Utility::translate($utility['name']);
|
||||
$icons[$i]['id'] = $this->generateIdFromName($utility['name']);
|
||||
$icons[$i]['tooltip'] = @$utility['tooltip'] ?: \false;
|
||||
$icons[$i]['script'] = @$utility['script'] ?: '';
|
||||
$icons[$i]['class'] = '';
|
||||
$icons[$i]['proonly'] = @$utility['proonly'] ?: \false;
|
||||
++$i;
|
||||
}
|
||||
|
||||
return $icons;
|
||||
}
|
||||
|
||||
public function getToggleSettings(): array
|
||||
{
|
||||
$pageCacheTooltip = '';
|
||||
if (\JCH_PLATFORM == 'Joomla!') {
|
||||
$pageCacheTooltip = '<strong>[';
|
||||
if (JCH_PRO) {
|
||||
$modeSwitcher = $this->container->get(ModeSwitcher::class);
|
||||
$integratedPageCache = $modeSwitcher->getIntegratedPageCachePlugin();
|
||||
$pageCacheTooltip .= Text::_($modeSwitcher->pageCachePlugins[$integratedPageCache]);
|
||||
} else {
|
||||
$pageCacheTooltip .= Text::_('COM_JCHOPTIMIZE_SYSTEM_PAGE_CACHE');
|
||||
}
|
||||
$pageCacheTooltip .= ']</strong><br><br>';
|
||||
}
|
||||
$pageCacheTooltip .= Utility::translate('Toggles on/off the Page Cache feature.');
|
||||
|
||||
return [['name' => 'Add Image Attributes', 'setting' => $setting = 'img_attributes_enable', 'icon' => 'img_attributes.png', 'enabled' => $this->params->get($setting, '0'), 'tooltip' => Utility::translate('Adds \'height\' and/or \'width\' attributes to <:img>\'s, if missing, to reduce CLS.')], ['name' => 'Sprite Generator', 'setting' => $setting = 'csg_enable', 'icon' => 'sprite_gen.png', 'enabled' => $this->params->get($setting, '0'), 'tooltip' => Utility::translate('Combines select background images into a sprite.')], ['name' => 'Http/2 Push', 'setting' => $setting = 'http2_push_enable', 'icon' => 'http2_push.png', 'enabled' => $this->params->get($setting, '0'), 'tooltip' => Utility::translate('Preloads critical assets using the http/2 protocol to improve LCP.')], ['name' => 'Lazy Load Images', 'setting' => $setting = 'lazyload_enable', 'icon' => 'lazyload.png', 'enabled' => $this->params->get($setting, '0'), 'tooltip' => Utility::translate('Defer images that fall below the fold.')], ['name' => 'Optimize CSS Delivery', 'setting' => $setting = 'optimizeCssDelivery_enable', 'icon' => 'optimize_css_delivery.png', 'enabled' => $this->params->get($setting, '0'), 'tooltip' => Utility::translate('Eliminates CSS render-blocking')], ['name' => 'Optimize Fonts', 'setting' => $setting = 'pro_optimizeFonts_enable', 'icon' => 'optimize_gfont.png', 'enabled' => $this->params->get($setting, '0'), 'proonly' => \true, 'tooltip' => Utility::translate('Optimizes the loading of fonts, including Google Fonts.')], ['name' => 'CDN', 'setting' => $setting = 'cookielessdomain_enable', 'icon' => 'cdn.png', 'enabled' => $this->params->get($setting, '0'), 'tooltip' => Utility::translate('Loads static assets from a CDN server. Requires the CDN domain(s) to be configured on the Configuration tab.')], ['name' => 'Smart Combine', 'setting' => $setting = 'pro_smart_combine', 'icon' => 'smart_combine.png', 'enabled' => $this->params->get($setting, '0'), 'proonly' => \true, 'tooltip' => Utility::translate('Intelligently combines files in a number of smaller files, instead of one large file for better http2 delivery.')], ['name' => 'Load Webp', 'setting' => $setting = 'pro_load_webp_images', 'icon' => 'webp.png', 'enabled' => $this->params->get($setting, '0'), 'proonly' => \true, 'tooltip' => Utility::translate('Loads generated WEBP images in place of the original ones. These images must be generated on the Optimize Image tab first.')], ['name' => 'Page Cache', 'setting' => 'integrated_page_cache_enable', 'icon' => 'cache.png', 'enabled' => Cache::isPageCacheEnabled($this->params), 'tooltip' => $pageCacheTooltip]];
|
||||
}
|
||||
|
||||
public function getCombineFilesEnableSetting(): array
|
||||
{
|
||||
return [['name' => 'Combine Files Enable', 'setting' => $setting = 'combine_files_enable', 'icon' => 'combine_files_enable.png', 'enabled' => $this->params->get($setting, '1')]];
|
||||
}
|
||||
|
||||
public function getAdvancedToggleSettings(): array
|
||||
{
|
||||
return [['name' => 'Reduce Unused CSS', 'setting' => $setting = 'pro_reduce_unused_css', 'icon' => 'reduce_unused_css.png', 'enabled' => $this->params->get($setting, '0'), 'proonly' => \true, 'tooltip' => Utility::translate('Loads only the critical CSS required for rendering the page above the fold until user interacts with the page. Requires Optimize CSS Delivery to be enabled and may need the CSS Dynamic Selectors setting to be configured to work properly.')], ['name' => 'Reduce Unused JavaScript', 'setting' => $setting = 'pro_reduce_unused_js_enable', 'icon' => 'reduce_unused_js.png', 'enabled' => $this->params->get($setting, '0'), 'proonly' => \true, 'tooltip' => Utility::translate('Will defer the loading of JavaScript until the user interacts with the page to improve performance affected by unused JavaScript. If your site uses JavaScript to perform the initial render you may need to \'exclude\' these critical JavaScript. These will be bundled together, preloaded and loaded asynchronously.')], ['name' => 'Reduce DOM', 'setting' => $setting = 'pro_reduce_dom', 'icon' => 'reduce_dom.png', 'enabled' => $this->params->get($setting, '0'), 'proonly' => \true, 'tooltip' => Utility::translate('\'Defers\' the loading of some HTML block elements to speed up page rendering.')]];
|
||||
}
|
||||
|
||||
public function compileToggleFeaturesIcons($settings): array
|
||||
{
|
||||
$buttons = [];
|
||||
for ($i = 0; $i < \count($settings); ++$i) {
|
||||
// id of figure icon
|
||||
$id = $this->generateIdFromName($settings[$i]['name']);
|
||||
$setting = $settings[$i]['setting'];
|
||||
$nonce = Utility::getNonce($setting);
|
||||
// script to run when icon is clicked
|
||||
$script = <<<JS
|
||||
onclick="jchPlatform.toggleSetting('{$setting}', '{$id}', '{$nonce}'); return false;"
|
||||
JS;
|
||||
$buttons[$i]['link'] = '';
|
||||
$buttons[$i]['icon'] = $settings[$i]['icon'];
|
||||
$buttons[$i]['name'] = Utility::translate($settings[$i]['name']);
|
||||
$buttons[$i]['id'] = $id;
|
||||
$buttons[$i]['script'] = $script;
|
||||
$buttons[$i]['class'] = $settings[$i]['enabled'] ? 'enabled' : 'disabled';
|
||||
$buttons[$i]['proonly'] = !empty($settings[$i]['proonly']);
|
||||
$buttons[$i]['tooltip'] = @$settings[$i]['tooltip'] ?: \false;
|
||||
}
|
||||
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
private function generateIdFromName($name): string
|
||||
{
|
||||
return \strtolower(\str_replace([' ', '/'], ['-', ''], \trim($name)));
|
||||
}
|
||||
|
||||
private function generateAutoSettingTooltip($setting): string
|
||||
{
|
||||
$aAutoSettingsMap = self::autoSettingsArrayMap();
|
||||
$aCurrentSettingValues = \array_column($aAutoSettingsMap, 's'.$setting);
|
||||
$aCurrentSettingArray = \array_combine(\array_keys($aAutoSettingsMap), $aCurrentSettingValues);
|
||||
$aSetting = \array_map(function ($v) {
|
||||
return '1' == $v ? 'on' : 'off';
|
||||
}, $aCurrentSettingArray);
|
||||
|
||||
return <<<HTML
|
||||
<h4 class="list-header">CSS</h4>
|
||||
<ul class="unstyled list-unstyled">
|
||||
<li>Combine CSS <i class="toggle fa {$aSetting['css']}"></i></li>
|
||||
<li>Minify CSS <i class="toggle fa {$aSetting['css_minify']}"></i></li>
|
||||
<li>Resolve @imports <i class="toggle fa {$aSetting['replaceImports']}"></i></li>
|
||||
<li>Include in-page styles <i class="toggle fa {$aSetting['inlineStyle']}"></i></li>
|
||||
</ul>
|
||||
<h4 class="list-header">JavaScript</h4>
|
||||
<ul class="unstyled list-unstyled">
|
||||
<li>Combine JavaScript <i class="toggle fa {$aSetting['javascript']}"></i></li>
|
||||
<li>Minify JavaScript <i class="toggle fa {$aSetting['js_minify']}"></i></li>
|
||||
<li>Include in-page scripts <i class="toggle fa {$aSetting['inlineScripts']}"></i></li>
|
||||
<li>Place JavaScript at bottom <i class="toggle fa {$aSetting['bottom_js']}"></i></li>
|
||||
<li>Defer/Async JavaScript <i class="toggle fa {$aSetting['loadAsynchronous']}"></i></li>
|
||||
</ul>
|
||||
<h4 class="list-header">Combine files</h4>
|
||||
<ul class="unstyled list-unstyled">
|
||||
<li>Gzip JavaScript/CSS <i class="toggle fa {$aSetting['gzip']}"></i> </li>
|
||||
<li>Minify HTML <i class="toggle fa {$aSetting['html_minify']}"></i> </li>
|
||||
<li>Include third-party files <i class="toggle fa {$aSetting['includeAllExtensions']}"></i></li>
|
||||
<li>Include external files <i class="toggle fa {$aSetting['phpAndExternal']}"></i></li>
|
||||
</ul>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Admin;
|
||||
|
||||
// No direct access
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
|
||||
class Json
|
||||
{
|
||||
/**
|
||||
* Determines whether the request was successful.
|
||||
*/
|
||||
public bool $success = \true;
|
||||
|
||||
/**
|
||||
* The response message.
|
||||
*/
|
||||
public string $message = '';
|
||||
|
||||
/**
|
||||
* The error code.
|
||||
*/
|
||||
public int $code = 0;
|
||||
|
||||
/**
|
||||
* The response data.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $data = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param mixed $response The Response data
|
||||
* @param string $message The response message
|
||||
*/
|
||||
public function __construct($response = null, $message = '')
|
||||
{
|
||||
$this->message = $message;
|
||||
// Check if we are dealing with an error
|
||||
if ($response instanceof \Exception) {
|
||||
// Prepare the error response
|
||||
$this->success = \false;
|
||||
$this->message = $response->getMessage();
|
||||
$this->code = $response->getCode();
|
||||
} else {
|
||||
$this->data = $response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic toString method for sending the response in JSON format.
|
||||
*
|
||||
* @return string The response in JSON format
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
@\header('Content-Type: application/json; charset=utf-8');
|
||||
if (\version_compare(\PHP_VERSION, '7.2', '>=')) {
|
||||
return \json_encode($this, \JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
}
|
||||
|
||||
return \json_encode($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Admin;
|
||||
|
||||
use _JchOptimizeVendor\Laminas\Cache\Pattern\CallbackCache;
|
||||
use CodeAlfa\Minify\Css;
|
||||
use CodeAlfa\Minify\Html;
|
||||
use CodeAlfa\Minify\Js;
|
||||
use JchOptimize\ContainerFactory;
|
||||
use JchOptimize\Core\Combiner;
|
||||
use JchOptimize\Core\Css\Sprite\Generator;
|
||||
use JchOptimize\Core\Exception;
|
||||
use JchOptimize\Core\FeatureHelpers\LazyLoadExtended;
|
||||
use JchOptimize\Core\FileUtils;
|
||||
use JchOptimize\Core\Helper;
|
||||
use JchOptimize\Core\Html\ElementObject;
|
||||
use JchOptimize\Core\Html\FilesManager;
|
||||
use JchOptimize\Core\Html\Parser;
|
||||
use JchOptimize\Core\Html\Processor as HtmlProcessor;
|
||||
use JchOptimize\Core\SerializableTrait;
|
||||
use JchOptimize\Core\SystemUri;
|
||||
use JchOptimize\Core\Uri\Utils;
|
||||
use JchOptimize\Platform\Excludes;
|
||||
use JchOptimize\Platform\Profiler;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
use function explode;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class MultiSelectItems implements \Serializable
|
||||
{
|
||||
use SerializableTrait;
|
||||
protected array $links = [];
|
||||
|
||||
private Registry $params;
|
||||
|
||||
private CallbackCache $callbackCache;
|
||||
|
||||
private FileUtils $fileUtils;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(Registry $params, CallbackCache $callbackCache, FileUtils $fileUtils)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->callbackCache = $callbackCache;
|
||||
$this->fileUtils = $fileUtils;
|
||||
}
|
||||
|
||||
public function prepareStyleValues(string $style): string
|
||||
{
|
||||
return $this->prepareScriptValues($style);
|
||||
}
|
||||
|
||||
public function prepareScriptValues(string $script): string
|
||||
{
|
||||
if (\strlen($script) > 52) {
|
||||
$script = \substr($script, 0, 52);
|
||||
$eps = '...';
|
||||
$script = $script.$eps;
|
||||
}
|
||||
if (\strlen($script) > 26) {
|
||||
$script = \str_replace($script[26], $script[26]."\n", $script);
|
||||
}
|
||||
|
||||
return $script;
|
||||
}
|
||||
|
||||
public function prepareImagesValues(string $image): string
|
||||
{
|
||||
return $image;
|
||||
}
|
||||
|
||||
public function prepareFolderValues($folder): string
|
||||
{
|
||||
return $this->fileUtils->prepareForDisplay(Utils::uriFor($folder));
|
||||
}
|
||||
|
||||
public function prepareFileValues($file): string
|
||||
{
|
||||
return $this->fileUtils->prepareForDisplay(Utils::uriFor($file));
|
||||
}
|
||||
|
||||
public function prepareClassValues($class): string
|
||||
{
|
||||
return $this->fileUtils->prepareForDisplay(null, $class, \false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a multidimensional array of items to populate the multi-select exclude lists in the
|
||||
* admin settings section.
|
||||
*
|
||||
* @param string $html HTML before it's optimized by JCH Optimize
|
||||
* @param string $css Combined css contents
|
||||
* @param bool $bCssJsOnly True if we're only interested in css and js values only as in smart combine
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getAdminLinks(string $html = '', string $css = '', bool $bCssJsOnly = \false): array
|
||||
{
|
||||
if (empty($this->links)) {
|
||||
$aFunction = [$this, 'generateAdminLinks'];
|
||||
$aArgs = [$html, $css, $bCssJsOnly];
|
||||
$this->links = $this->callbackCache->call($aFunction, $aArgs);
|
||||
}
|
||||
|
||||
return $this->links;
|
||||
}
|
||||
|
||||
public function generateAdminLinks(string $html, string $css, bool $bCssJsOnly): array
|
||||
{
|
||||
!JCH_DEBUG ?: Profiler::start('GenerateAdminLinks');
|
||||
// We need to get a new instance of the container here as we'll be changing the params, and we don't want to mess things up
|
||||
$container = ContainerFactory::getNewContainerInstance();
|
||||
$params = $container->get('params');
|
||||
$params->set('combine_files_enable', '1');
|
||||
$params->set('pro_smart_combine', '0');
|
||||
$params->set('javascript', '1');
|
||||
$params->set('css', '1');
|
||||
$params->set('gzip', '0');
|
||||
$params->set('css_minify', '0');
|
||||
$params->set('js_minify', '0');
|
||||
$params->set('html_minify', '0');
|
||||
$params->set('defer_js', '0');
|
||||
$params->set('debug', '0');
|
||||
$params->set('bottom_js', '1');
|
||||
$params->set('includeAllExtensions', '1');
|
||||
$params->set('excludeCss', []);
|
||||
$params->set('excludeJs', []);
|
||||
$params->set('excludeAllStyles', []);
|
||||
$params->set('excludeAllScripts', []);
|
||||
$params->set('excludeJs_peo', []);
|
||||
$params->set('excludeJsComponents_peo', []);
|
||||
$params->set('excludeScripts_peo', []);
|
||||
$params->set('excludeCssComponents', []);
|
||||
$params->set('excludeJsComponents', []);
|
||||
$params->set('csg_exclude_images', []);
|
||||
$params->set('csg_include_images', []);
|
||||
$params->set('phpAndExternal', '1');
|
||||
$params->set('inlineScripts', '1');
|
||||
$params->set('inlineStyle', '1');
|
||||
$params->set('replaceImports', '0');
|
||||
$params->set('loadAsynchronous', '0');
|
||||
$params->set('cookielessdomain_enable', '0');
|
||||
$params->set('lazyload_enable', '0');
|
||||
$params->set('optimizeCssDelivery_enable', '0');
|
||||
$params->set('pro_excludeLazyLoad', []);
|
||||
$params->set('pro_excludeLazyLoadFolders', []);
|
||||
$params->set('pro_excludeLazyLoadClass', []);
|
||||
$params->set('pro_reduce_unused_css', '0');
|
||||
$params->set('pro_reduce_unused_js_enable', '0');
|
||||
$params->set('pro_reduce_dom', '0');
|
||||
|
||||
try {
|
||||
// If we're doing multiselect it's better to fetch the HTML here than send it as an args
|
||||
// to prevent different cache keys generating when passed through callback cache
|
||||
if ('' == $html) {
|
||||
/** @var \JchOptimize\Platform\Html $oHtml */
|
||||
$oHtml = $container->get(\JchOptimize\Core\Admin\AbstractHtml::class);
|
||||
$html = $oHtml->getHomePageHtml();
|
||||
}
|
||||
|
||||
/** @var HtmlProcessor $oHtmlProcessor */
|
||||
$oHtmlProcessor = $container->get(HtmlProcessor::class);
|
||||
$oHtmlProcessor->setHtml($html);
|
||||
$oHtmlProcessor->processCombineJsCss();
|
||||
|
||||
/** @var FilesManager $oFilesManager */
|
||||
$oFilesManager = $container->get(FilesManager::class);
|
||||
$aLinks = ['css' => $oFilesManager->aCss, 'js' => $oFilesManager->aJs];
|
||||
// Only need css and js links if we're doing smart combine
|
||||
if ($bCssJsOnly) {
|
||||
return $aLinks;
|
||||
}
|
||||
if ('' == $css && !empty($aLinks['css'][0])) {
|
||||
$oCombiner = $container->get(Combiner::class);
|
||||
$aResult = $oCombiner->combineFiles($aLinks['css'][0], 'css');
|
||||
$css = $aResult['content'];
|
||||
}
|
||||
if (\JCH_PRO) {
|
||||
$aLinks['criticaljs'] = $aLinks['js'];
|
||||
$aLinks['modules'] = [];
|
||||
foreach ($oFilesManager->defers as $deferGroup) {
|
||||
if ('defer' == $deferGroup[0]['attributeType'] || 'async' == $deferGroup[0]['attributeType']) {
|
||||
foreach ($deferGroup as $defer) {
|
||||
$aLinks['criticaljs'][0][]['url'] = $defer['url'];
|
||||
}
|
||||
}
|
||||
if ('module' == $deferGroup[0]['attributeType']) {
|
||||
foreach ($deferGroup as $defer) {
|
||||
if (isset($defer['url'])) {
|
||||
$aLinks['modules'][0][]['url'] = $defer['url'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Generator $oSpriteGenerator */
|
||||
$oSpriteGenerator = $container->get(Generator::class);
|
||||
$aLinks['images'] = $oSpriteGenerator->processCssUrls($css, \true);
|
||||
$oHtmlParser = new Parser();
|
||||
$oHtmlParser->addExclude(Parser::htmlCommentToken());
|
||||
$oHtmlParser->addExclude(Parser::htmlElementsToken(['script', 'noscript', 'textarea']));
|
||||
$oElement = new ElementObject();
|
||||
$oElement->setNamesArray(['img', 'iframe', 'input']);
|
||||
$oElement->bSelfClosing = \true;
|
||||
$oElement->addNegAttrCriteriaRegex('(?:data-(?:src|original))');
|
||||
$oElement->setCaptureAttributesArray(['class', 'src']);
|
||||
$oHtmlParser->addElementObject($oElement);
|
||||
$aMatches = $oHtmlParser->findMatches($oHtmlProcessor->getBodyHtml());
|
||||
if (\JCH_PRO) {
|
||||
$aLinks['lazyloadclass'] = LazyLoadExtended::getLazyLoadClass($aMatches);
|
||||
}
|
||||
$aLinks['lazyload'] = \array_map(function ($a) {
|
||||
return Utils::uriFor($a);
|
||||
}, $aMatches[7]);
|
||||
} catch (Exception\ExceptionInterface $e) {
|
||||
$aLinks = [];
|
||||
}
|
||||
!JCH_DEBUG ?: Profiler::stop('GenerateAdminLinks', \true);
|
||||
|
||||
return $aLinks;
|
||||
}
|
||||
|
||||
public function prepareFieldOptions(string $type, string $excludeParams, string $group = '', bool $bIncludeExcludes = \true): array
|
||||
{
|
||||
if ('lazyload' == $type) {
|
||||
$fieldOptions = $this->getLazyLoad($group);
|
||||
$group = 'file';
|
||||
} elseif ('images' == $type) {
|
||||
$group = 'file';
|
||||
$aM = \explode('_', $excludeParams);
|
||||
$fieldOptions = $this->getImages($aM[1]);
|
||||
} else {
|
||||
$fieldOptions = $this->getOptions($type, $group.'s');
|
||||
}
|
||||
$options = [];
|
||||
$excludes = Helper::getArray($this->params->get($excludeParams, []));
|
||||
foreach ($excludes as $exclude) {
|
||||
if (\is_array($exclude)) {
|
||||
foreach ($exclude as $key => $value) {
|
||||
if ('url' == $key && \is_string($value)) {
|
||||
$options[$value] = $this->prepareGroupValues($group, $value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$options[$exclude] = $this->prepareGroupValues($group, $exclude);
|
||||
}
|
||||
}
|
||||
// Should we include saved exclude parameters?
|
||||
if ($bIncludeExcludes) {
|
||||
return \array_merge($fieldOptions, $options);
|
||||
}
|
||||
|
||||
return \array_diff($fieldOptions, $options);
|
||||
}
|
||||
|
||||
public function getLazyLoad(string $group): array
|
||||
{
|
||||
$aLinks = $this->links;
|
||||
$aFieldOptions = [];
|
||||
if ('file' == $group || 'folder' == $group) {
|
||||
if (!empty($aLinks['lazyload'])) {
|
||||
foreach ($aLinks['lazyload'] as $imageUri) {
|
||||
if ('folder' == $group) {
|
||||
$regex = '#(?<!/)/[^/\\n]++$|(?<=^)[^/.\\n]++$#';
|
||||
$i = 0;
|
||||
$imageUrl = $this->fileUtils->prepareForDisplay($imageUri, '', \false);
|
||||
$folder = \preg_replace($regex, '', $imageUrl);
|
||||
while (\preg_match($regex, $folder)) {
|
||||
$aFieldOptions[$folder] = $this->fileUtils->prepareForDisplay(Utils::uriFor($folder));
|
||||
$folder = \preg_replace($regex, '', $folder);
|
||||
++$i;
|
||||
if (12 == $i) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$imageUrl = $this->fileUtils->prepareForDisplay($imageUri, '', \false);
|
||||
$aFieldOptions[$imageUrl] = $this->fileUtils->prepareForDisplay($imageUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ('class' == $group) {
|
||||
if (!empty($aLinks['lazyloadclass'])) {
|
||||
foreach ($aLinks['lazyloadclass'] as $sClasses) {
|
||||
$aClass = \preg_split('# #', $sClasses, -1, \PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($aClass as $sClass) {
|
||||
$aFieldOptions[$sClass] = $sClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return \array_filter($aFieldOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @staticvar string $sUriBase
|
||||
* @staticvar string $sUriPath
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function prepareExtensionValues(string $url, bool $return = \true)
|
||||
{
|
||||
if ($return) {
|
||||
return $url;
|
||||
}
|
||||
static $host = '';
|
||||
$oUri = SystemUri::currentUri();
|
||||
$host = '' == $host ? $oUri->getHost() : $host;
|
||||
$result = \preg_match('#^(?:https?:)?//([^/]+)#', $url, $m1);
|
||||
$extension = $m1[1] ?? '';
|
||||
if (0 === $result || $extension == $host) {
|
||||
$result2 = \preg_match('#'.Excludes::extensions().'([^/]+)#', $url, $m);
|
||||
if (0 === $result2) {
|
||||
return \false;
|
||||
}
|
||||
$extension = $m[1];
|
||||
}
|
||||
|
||||
return $extension;
|
||||
}
|
||||
|
||||
protected function getImages(string $action = 'exclude'): array
|
||||
{
|
||||
$aLinks = $this->links;
|
||||
$aOptions = [];
|
||||
if (!empty($aLinks['images'][$action])) {
|
||||
foreach ($aLinks['images'][$action] as $sImage) {
|
||||
// $aImage = explode('/', $sImage);
|
||||
// $sImage = array_pop($aImage);
|
||||
$aOptions = \array_merge($aOptions, [$sImage => $this->fileUtils->prepareForDisplay($sImage)]);
|
||||
}
|
||||
}
|
||||
|
||||
return \array_unique($aOptions);
|
||||
}
|
||||
|
||||
protected function getOptions(string $type, string $group = 'files'): array
|
||||
{
|
||||
$aLinks = $this->links;
|
||||
$aOptions = [];
|
||||
if (!empty($aLinks[$type][0])) {
|
||||
foreach ($aLinks[$type][0] as $aLink) {
|
||||
if (isset($aLink['url']) && '' != (string) $aLink['url']) {
|
||||
if ('files' == $group) {
|
||||
$file = $this->fileUtils->prepareForDisplay($aLink['url'], '', \false);
|
||||
$aOptions[$file] = $this->fileUtils->prepareForDisplay($aLink['url']);
|
||||
} elseif ('extensions' == $group) {
|
||||
$extension = $this->prepareExtensionValues($aLink['url'], \false);
|
||||
if (\false === $extension) {
|
||||
continue;
|
||||
}
|
||||
$aOptions[$extension] = $extension;
|
||||
}
|
||||
} elseif (isset($aLink['content']) && '' != $aLink['content']) {
|
||||
if ('scripts' == $group) {
|
||||
$script = Html::cleanScript($aLink['content'], 'js');
|
||||
$script = \trim(Js::optimize($script));
|
||||
} elseif ('styles' == $group) {
|
||||
$script = Html::cleanScript($aLink['content'], 'css');
|
||||
$script = \trim(Css::optimize($script));
|
||||
}
|
||||
if (isset($script)) {
|
||||
if (\strlen($script) > 60) {
|
||||
$script = \substr($script, 0, 60);
|
||||
}
|
||||
$script = \htmlspecialchars($script);
|
||||
$aOptions[\addslashes($script)] = $this->prepareScriptValues($script);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $aOptions;
|
||||
}
|
||||
|
||||
private function prepareGroupValues(string $group, string $value)
|
||||
{
|
||||
return $this->{'prepare'.\ucfirst($group).'Values'}($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Admin;
|
||||
|
||||
use JchOptimize\ContainerFactory;
|
||||
use JchOptimize\Core\Admin\Ajax\OptimizeImage;
|
||||
use JchOptimize\Core\Admin\Helper as AdminHelper;
|
||||
use JchOptimize\Core\FeatureHelpers\Webp;
|
||||
use JchOptimize\Platform\Paths;
|
||||
use JchOptimize\Platform\Plugin;
|
||||
use Joomla\Filesystem\Exception\FilesystemException;
|
||||
use Joomla\Filesystem\File;
|
||||
use Joomla\Filesystem\Folder;
|
||||
use Joomla\Registry\Registry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Tasks
|
||||
{
|
||||
public static string $startHtaccessLine = '## BEGIN EXPIRES CACHING - JCH OPTIMIZE ##';
|
||||
public static string $endHtaccessLine = '## END EXPIRES CACHING - JCH OPTIMIZE ##';
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public static function leverageBrowserCaching(?bool &$success = null): ?string
|
||||
{
|
||||
$htaccess = Paths::rootPath().'/.htaccess';
|
||||
if (\file_exists($htaccess)) {
|
||||
$contents = \file_get_contents($htaccess);
|
||||
$cleanContents = \preg_replace(self::getHtaccessRegex(), \PHP_EOL, $contents);
|
||||
$startLine = self::$startHtaccessLine;
|
||||
$endLine = self::$endHtaccessLine;
|
||||
$expires = <<<APACHECONFIG
|
||||
|
||||
{$startLine}
|
||||
<IfModule mod_expires.c>
|
||||
\tExpiresActive on
|
||||
|
||||
\t# Your document html
|
||||
\tExpiresByType text/html "access plus 0 seconds"
|
||||
|
||||
\t# Data
|
||||
\tExpiresByType text/xml "access plus 0 seconds"
|
||||
\tExpiresByType application/xml "access plus 0 seconds"
|
||||
\tExpiresByType application/json "access plus 0 seconds"
|
||||
|
||||
\t# Feed
|
||||
\tExpiresByType application/rss+xml "access plus 1 hour"
|
||||
\tExpiresByType application/atom+xml "access plus 1 hour"
|
||||
|
||||
\t# Favicon (cannot be renamed)
|
||||
\tExpiresByType image/x-icon "access plus 1 week"
|
||||
|
||||
\t# Media: images, video, audio
|
||||
\tExpiresByType image/gif "access plus 1 year"
|
||||
\tExpiresByType image/png "access plus 1 year"
|
||||
\tExpiresByType image/jpg "access plus 1 year"
|
||||
\tExpiresByType image/jpeg "access plus 1 year"
|
||||
\tExpiresByType image/webp "access plus 1 year"
|
||||
\tExpiresByType audio/ogg "access plus 1 year"
|
||||
\tExpiresByType video/ogg "access plus 1 year"
|
||||
\tExpiresByType video/mp4 "access plus 1 year"
|
||||
\tExpiresByType video/webm "access plus 1 year"
|
||||
|
||||
\t# HTC files (css3pie)
|
||||
\tExpiresByType text/x-component "access plus 1 year"
|
||||
|
||||
\t# Webfonts
|
||||
\tExpiresByType image/svg+xml "access plus 1 year"
|
||||
\tExpiresByType font/* "access plus 1 year"
|
||||
\tExpiresByType application/x-font-ttf "access plus 1 year"
|
||||
\tExpiresByType application/x-font-truetype "access plus 1 year"
|
||||
\tExpiresByType application/x-font-opentype "access plus 1 year"
|
||||
\tExpiresByType application/font-ttf "access plus 1 year"
|
||||
\tExpiresByType application/font-woff "access plus 1 year"
|
||||
\tExpiresByType application/font-woff2 "access plus 1 year"
|
||||
\tExpiresByType application/vnd.ms-fontobject "access plus 1 year"
|
||||
\tExpiresByType application/font-sfnt "access plus 1 year"
|
||||
|
||||
\t# CSS and JavaScript
|
||||
\tExpiresByType text/css "access plus 1 year"
|
||||
\tExpiresByType text/javascript "access plus 1 year"
|
||||
\tExpiresByType application/javascript "access plus 1 year"
|
||||
|
||||
\t<IfModule mod_headers.c>
|
||||
\t\tHeader set Cache-Control "no-cache, no-store, must-revalidate"
|
||||
\t\t
|
||||
\t\t<FilesMatch "\\.(js|css|ttf|woff2?|svg|png|jpe?g|webp|webm|mp4|ogg)(\\.gz)?\$">
|
||||
\t\t\tHeader set Cache-Control "public"\t
|
||||
\t\t\tHeader set Vary: Accept-Encoding
|
||||
\t\t</FilesMatch>
|
||||
\t\t#Some server not properly recognizing WEBPs
|
||||
\t\t<FilesMatch "\\.webp\$">
|
||||
\t\t\tHeader set Content-Type "image/webp"
|
||||
\t\t\tExpiresDefault "access plus 1 year"
|
||||
\t\t</FilesMatch>\t
|
||||
\t</IfModule>
|
||||
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_brotli.c>
|
||||
\t<IfModule mod_filter.c>
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS text/html text/xml text/plain
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS application/rss+xml application/xml application/xhtml+xml
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS text/css
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS text/javascript application/javascript application/x-javascript
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS image/x-icon image/svg+xml
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS application/rss+xml
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS application/font application/font-truetype application/font-ttf
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS application/font-otf application/font-opentype
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS application/font-woff application/font-woff2
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS application/vnd.ms-fontobject
|
||||
\t\tAddOutputFilterByType BROTLI_COMPRESS font/ttf font/otf font/opentype font/woff font/woff2
|
||||
\t</IfModule>
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_deflate.c>
|
||||
\t<IfModule mod_filter.c>
|
||||
\t\tAddOutputFilterByType DEFLATE text/html text/xml text/plain
|
||||
\t\tAddOutputFilterByType DEFLATE application/rss+xml application/xml application/xhtml+xml
|
||||
\t\tAddOutputFilterByType DEFLATE text/css
|
||||
\t\tAddOutputFilterByType DEFLATE text/javascript application/javascript application/x-javascript
|
||||
\t\tAddOutputFilterByType DEFLATE image/x-icon image/svg+xml
|
||||
\t\tAddOutputFilterByType DEFLATE application/rss+xml
|
||||
\t\tAddOutputFilterByType DEFLATE application/font application/font-truetype application/font-ttf
|
||||
\t\tAddOutputFilterByType DEFLATE application/font-otf application/font-opentype
|
||||
\t\tAddOutputFilterByType DEFLATE application/font-woff application/font-woff2
|
||||
\t\tAddOutputFilterByType DEFLATE application/vnd.ms-fontobject
|
||||
\t\tAddOutputFilterByType DEFLATE font/ttf font/otf font/opentype font/woff font/woff2
|
||||
\t</IfModule>
|
||||
</IfModule>
|
||||
|
||||
# Don't compress files with extension .gz or .br
|
||||
<IfModule mod_rewrite.c>
|
||||
\tRewriteRule "\\.(gz|br)\$" "-" [E=no-gzip:1,E=no-brotli:1]
|
||||
</IfModule>
|
||||
|
||||
<IfModule !mod_rewrite.c>
|
||||
\t<IfModule mod_setenvif.c>
|
||||
\t\tSetEnvIfNoCase Request_URI \\.(gz|br)\$ no-gzip no-brotli
|
||||
\t</IfModule>
|
||||
</IfModule>
|
||||
{$endLine}
|
||||
|
||||
APACHECONFIG;
|
||||
$expires = \str_replace(["\r\n", "\n"], \PHP_EOL, $expires);
|
||||
$str = $expires.$cleanContents;
|
||||
$success = File::write($htaccess, $str);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'FILEDOESNTEXIST';
|
||||
}
|
||||
|
||||
public static function cleanHtaccess(): void
|
||||
{
|
||||
$htaccess = Paths::rootPath().'/.htaccess';
|
||||
if (\file_exists($htaccess)) {
|
||||
$contents = \file_get_contents($htaccess);
|
||||
$cleanContents = \preg_replace(self::getHtaccessRegex(), '', $contents, -1, $count);
|
||||
if ($count > 0) {
|
||||
File::write($htaccess, $cleanContents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|true
|
||||
*
|
||||
* @psalm-return 'BACKUPPATHDOESNTEXIST'|'SOMEIMAGESDIDNTRESTORE'|true
|
||||
*/
|
||||
public static function restoreBackupImages(?LoggerInterface $logger = null)
|
||||
{
|
||||
if (\is_null($logger)) {
|
||||
$logger = new NullLogger();
|
||||
}
|
||||
$backupPath = Paths::backupImagesParentDir().OptimizeImage::$backup_folder_name;
|
||||
if (!\is_dir($backupPath)) {
|
||||
return 'BACKUPPATHDOESNTEXIST';
|
||||
}
|
||||
$aFiles = Folder::files($backupPath, '.', \false, \true, []);
|
||||
$failure = \false;
|
||||
foreach ($aFiles as $backupContractedFile) {
|
||||
$success = \false;
|
||||
$aPotentialOriginalFilePaths = [AdminHelper::expandFileName($backupContractedFile), AdminHelper::expandFileNameLegacy($backupContractedFile)];
|
||||
foreach ($aPotentialOriginalFilePaths as $originalFilePath) {
|
||||
if (@\file_exists($originalFilePath)) {
|
||||
// Attempt to restore backup images
|
||||
if (AdminHelper::copyImage($backupContractedFile, $originalFilePath)) {
|
||||
try {
|
||||
if (\file_exists(Webp::getWebpPath($originalFilePath))) {
|
||||
File::delete(Webp::getWebpPath($originalFilePath));
|
||||
}
|
||||
if (\file_exists(Webp::getWebpPathLegacy($originalFilePath))) {
|
||||
File::delete(Webp::getWebpPathLegacy($originalFilePath));
|
||||
}
|
||||
if (\file_exists($backupContractedFile)) {
|
||||
File::delete($backupContractedFile);
|
||||
}
|
||||
AdminHelper::unmarkOptimized($originalFilePath);
|
||||
$success = \true;
|
||||
|
||||
break;
|
||||
} catch (FilesystemException $e) {
|
||||
$logger->debug('Error deleting '.Webp::getWebpPath($originalFilePath).' with message: '.$e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$logger->debug('Error copying image '.$backupContractedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$success) {
|
||||
$logger->debug('File not found: '.$backupContractedFile);
|
||||
$logger->debug('Potential file paths: '.\print_r($aPotentialOriginalFilePaths, \true));
|
||||
$failure = \true;
|
||||
}
|
||||
}
|
||||
\clearstatcache();
|
||||
if ($failure) {
|
||||
return 'SOMEIMAGESDIDNTRESTORE';
|
||||
}
|
||||
self::deleteBackupImages();
|
||||
|
||||
return \true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|string
|
||||
*
|
||||
* @psalm-return 'BACKUPPATHDOESNTEXIST'|bool
|
||||
*/
|
||||
public static function deleteBackupImages()
|
||||
{
|
||||
$backupPath = Paths::backupImagesParentDir().OptimizeImage::$backup_folder_name;
|
||||
if (!\is_dir($backupPath)) {
|
||||
return 'BACKUPPATHDOESNTEXIST';
|
||||
}
|
||||
|
||||
return Folder::delete($backupPath);
|
||||
}
|
||||
|
||||
public static function generateNewCacheKey(): void
|
||||
{
|
||||
$container = ContainerFactory::getContainer();
|
||||
$rand = \rand();
|
||||
|
||||
/** @var Registry $params */
|
||||
$params = $container->get('params');
|
||||
$params->set('cache_random_key', $rand);
|
||||
Plugin::saveSettings($params);
|
||||
}
|
||||
|
||||
private static function getHtaccessRegex(): string
|
||||
{
|
||||
return '#[\\r\\n]*'.\preg_quote(self::$startHtaccessLine).'.*?'.\preg_quote(\rtrim(self::$endHtaccessLine, "# \n\r\t\v\x00")).'[^\\r\\n]*[\\r\\n]*#s';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core;
|
||||
|
||||
use JchOptimize\Platform\Utility;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Browser
|
||||
{
|
||||
/**
|
||||
* @var Browser[]
|
||||
*/
|
||||
protected static array $instances = [];
|
||||
|
||||
/**
|
||||
* @var object{browser: string, browserVersion: string, os: string}
|
||||
*/
|
||||
protected object $oClient;
|
||||
|
||||
public function __construct(string $userAgent)
|
||||
{
|
||||
$this->oClient = Utility::userAgent($userAgent);
|
||||
}
|
||||
|
||||
public static function getInstance(string $userAgent = ''): Browser
|
||||
{
|
||||
if ('' == $userAgent && isset($_SERVER['HTTP_USER_AGENT'])) {
|
||||
$userAgent = \trim($_SERVER['HTTP_USER_AGENT']);
|
||||
}
|
||||
$signature = \md5($userAgent);
|
||||
if (!isset(self::$instances[$signature])) {
|
||||
self::$instances[$signature] = new \JchOptimize\Core\Browser($userAgent);
|
||||
}
|
||||
|
||||
return self::$instances[$signature];
|
||||
}
|
||||
|
||||
public function getBrowser(): string
|
||||
{
|
||||
return $this->oClient->browser;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->oClient->browserVersion;
|
||||
}
|
||||
}
|
||||
236
administrator/components/com_jchoptimize/lib/src/Core/Cdn.php
Normal file
236
administrator/components/com_jchoptimize/lib/src/Core/Cdn.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core;
|
||||
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\Uri;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\UriResolver;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use _JchOptimizeVendor\Psr\Http\Message\UriInterface;
|
||||
use JchOptimize\Core\Exception\RuntimeException;
|
||||
use JchOptimize\Core\FeatureHelpers\CdnDomains;
|
||||
use JchOptimize\Core\Uri\Utils;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Cdn implements ContainerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
public string $scheme = '';
|
||||
|
||||
/** @var null|array<string, array{domain:UriInterface, extensions:string}> */
|
||||
protected ?array $domains = null;
|
||||
|
||||
/** @var array<string, UriInterface> */
|
||||
protected array $filePaths = [];
|
||||
|
||||
/** @var null|string[] */
|
||||
protected ?array $cdnFileTypes = null;
|
||||
private Registry $params;
|
||||
private bool $enabled;
|
||||
private string $startHtaccessLine = '## BEGIN CDN CORS POLICY - JCH OPTIMIZE ##';
|
||||
private string $endHtaccessLine = '## END CDN CORS POLICY - JCH OPTIMIZE ##';
|
||||
|
||||
public function __construct(Registry $params)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->enabled = (bool) $this->params->get('cookielessdomain_enable', '0');
|
||||
|
||||
switch ($params->get('cdn_scheme', '0')) {
|
||||
case '1':
|
||||
$this->scheme = 'http';
|
||||
|
||||
break;
|
||||
|
||||
case '2':
|
||||
$this->scheme = 'https';
|
||||
|
||||
break;
|
||||
|
||||
case '0':
|
||||
default:
|
||||
$this->scheme = '';
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of file types that will be loaded by CDN.
|
||||
*
|
||||
* @return string[]
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function getCdnFileTypes(): array
|
||||
{
|
||||
if (null === $this->cdnFileTypes) {
|
||||
$this->initialize();
|
||||
}
|
||||
if (null !== $this->cdnFileTypes) {
|
||||
return $this->cdnFileTypes;
|
||||
}
|
||||
|
||||
throw new RuntimeException('CDN file types not initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array of default static files to load from CDN.
|
||||
*
|
||||
* @return string[] Array of file type extensions
|
||||
*/
|
||||
public static function getStaticFiles(): array
|
||||
{
|
||||
return ['css', 'js', 'jpe?g', 'gif', 'png', 'ico', 'bmp', 'pdf', 'webp', 'svg'];
|
||||
}
|
||||
|
||||
public function prepareDomain(string $domain): UriInterface
|
||||
{
|
||||
// If scheme not included then we need to add forward slashes to make UriInterfaces
|
||||
// implementations recognize the domain
|
||||
if (!\preg_match('#^(?:[^:/]++:|//)#', \trim($domain))) {
|
||||
$domain = '//'.$domain;
|
||||
}
|
||||
|
||||
return Utils::uriFor($domain)->withScheme($this->scheme);
|
||||
}
|
||||
|
||||
public function loadCdnResource(UriInterface $uri, ?UriInterface $origPath = null): UriInterface
|
||||
{
|
||||
$domains = $this->getCdnDomains();
|
||||
if (empty($origPath)) {
|
||||
$origPath = $uri;
|
||||
}
|
||||
// if disabled or no domain is configured abort
|
||||
if (!$this->enabled || empty($domains) || null === $this->domains) {
|
||||
return $origPath;
|
||||
}
|
||||
// If file already loaded on CDN return
|
||||
if ($this->isFileOnCdn($uri)) {
|
||||
return $origPath;
|
||||
}
|
||||
// We're now ready to load path on CDN but let's remove query first
|
||||
$path = $uri->getPath();
|
||||
// If we haven't matched a cdn domain to this file yet then find one.
|
||||
if (!isset($this->filePaths[$path])) {
|
||||
$this->filePaths[$path] = $this->selectDomain($this->domains, $uri);
|
||||
}
|
||||
if ('' === (string) $this->filePaths[$path]) {
|
||||
return $origPath;
|
||||
}
|
||||
|
||||
return $this->filePaths[$path];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{domain:UriInterface, extensions:string}>
|
||||
*/
|
||||
public function getCdnDomains(): array
|
||||
{
|
||||
if (null === $this->domains) {
|
||||
$this->initialize();
|
||||
}
|
||||
if (null !== $this->domains) {
|
||||
return $this->domains;
|
||||
}
|
||||
|
||||
throw new RuntimeException('CDN Domains not initialized');
|
||||
}
|
||||
|
||||
public function isFileOnCdn(UriInterface $uri): bool
|
||||
{
|
||||
foreach ($this->getCdnDomains() as $domainArray) {
|
||||
if ($uri->getHost() === $domainArray['domain']->getHost()) {
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
|
||||
return \false;
|
||||
}
|
||||
|
||||
public function updateHtaccess(): void
|
||||
{
|
||||
$htaccessDelimiters = ['## BEGIN CDN CORS POLICY - JCH OPTIMIZE ##', '## END CDN CORS POLICY - JCH OPTIMIZE ##'];
|
||||
$origin = \JchOptimize\Core\SystemUri::currentUri()->withPort(null)->withPath('')->withQuery('')->withFragment('');
|
||||
if ($this->enabled && !empty($this->getCdnDomains())) {
|
||||
$htaccessContents = <<<APACHECONFIG
|
||||
<IfModule mod_headers.c>
|
||||
Header append Access-Control-Allow-Origin "{$origin}"
|
||||
Header append Vary "Origin"
|
||||
</IfModule>
|
||||
APACHECONFIG;
|
||||
\JchOptimize\Core\Htaccess::updateHtaccess($htaccessContents, $htaccessDelimiters);
|
||||
} else {
|
||||
\JchOptimize\Core\Htaccess::cleanHtaccess($htaccessDelimiters);
|
||||
}
|
||||
}
|
||||
|
||||
private function initialize(): void
|
||||
{
|
||||
/** @var string[] $staticFiles1Array */
|
||||
$staticFiles1Array = $this->params->get('staticfiles', self::getStaticFiles());
|
||||
|
||||
/** @var array<string, array{domain:UriInterface, extensions:string}> $domainArray */
|
||||
$domainArray = [];
|
||||
$this->cdnFileTypes = [];
|
||||
if ($this->enabled) {
|
||||
/** @var string $domain1 */
|
||||
$domain1 = $this->params->get('cookielessdomain', '');
|
||||
if ('' != \trim($domain1)) {
|
||||
/** @var string[] $customExtns */
|
||||
$customExtns = $this->params->get('pro_customcdnextensions', []);
|
||||
$sStaticFiles1 = \implode('|', \array_merge($staticFiles1Array, $customExtns));
|
||||
$domainArray['domain1']['domain'] = $this->prepareDomain($domain1);
|
||||
$domainArray['domain1']['extensions'] = $sStaticFiles1;
|
||||
}
|
||||
if (JCH_PRO) {
|
||||
$this->container->get(CdnDomains::class)->addCdnDomains($domainArray);
|
||||
}
|
||||
}
|
||||
$this->domains = $domainArray;
|
||||
if (!empty($this->domains)) {
|
||||
foreach ($this->domains as $domains) {
|
||||
$this->cdnFileTypes = \array_merge($this->cdnFileTypes, \explode('|', $domains['extensions']));
|
||||
}
|
||||
$this->cdnFileTypes = \array_unique($this->cdnFileTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{domain:UriInterface, extensions:string}> $domainArray
|
||||
*/
|
||||
private function selectDomain(array &$domainArray, UriInterface $uri): UriInterface
|
||||
{
|
||||
// If no domain is matched to a configured file type then we'll just return the file
|
||||
$cdnUri = new Uri();
|
||||
for ($i = 0; \count($domainArray) > $i; ++$i) {
|
||||
$domain = \current($domainArray);
|
||||
$staticFiles = $domain['extensions'];
|
||||
\next($domainArray);
|
||||
if (\false === \current($domainArray)) {
|
||||
\reset($domainArray);
|
||||
}
|
||||
if (\preg_match('#\\.(?>'.$staticFiles.')#i', $uri->getPath())) {
|
||||
// Prepend the cdn domain to the file path if a match is found.
|
||||
$cdnDomain = $domain['domain'];
|
||||
// Some CDNs like Cloudinary includes path to the CDN domain to be prepended to the asset
|
||||
$uri = $uri->withPath(\rtrim($cdnDomain->getPath(), '/').'/'.\ltrim($uri->getPath(), '/'));
|
||||
$cdnUri = UriResolver::resolve($cdnDomain, $uri->withScheme('')->withHost(''));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $cdnUri;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core;
|
||||
|
||||
use _JchOptimizeVendor\GuzzleHttp\Client;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Exception\GuzzleException;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\UriResolver;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\Utils;
|
||||
use _JchOptimizeVendor\GuzzleHttp\RequestOptions;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use _JchOptimizeVendor\Laminas\Cache\Pattern\CallbackCache;
|
||||
use _JchOptimizeVendor\Laminas\Cache\Storage\IterableInterface;
|
||||
use _JchOptimizeVendor\Laminas\Cache\Storage\StorageInterface;
|
||||
use _JchOptimizeVendor\Laminas\Cache\Storage\TaggableInterface;
|
||||
use _JchOptimizeVendor\Psr\Http\Client\ClientInterface;
|
||||
use _JchOptimizeVendor\Psr\Http\Message\UriInterface;
|
||||
use CodeAlfa\Minify\Css;
|
||||
use CodeAlfa\Minify\Js;
|
||||
use CodeAlfa\RegexTokenizer\Debug\Debug;
|
||||
use Exception;
|
||||
use JchOptimize\Core\Css\Processor as CssProcessor;
|
||||
use JchOptimize\Core\Css\Sprite\Generator;
|
||||
use JchOptimize\Core\Uri\UriComparator;
|
||||
use JchOptimize\Core\Uri\UriConverter;
|
||||
use JchOptimize\Platform\Profiler;
|
||||
use Joomla\Registry\Registry;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
|
||||
/**
|
||||
* Class to combine CSS/JS files together.
|
||||
*/
|
||||
class Combiner implements ContainerAwareInterface, LoggerAwareInterface, \Serializable
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
use LoggerAwareTrait;
|
||||
use Debug;
|
||||
use \JchOptimize\Core\FileInfosUtilsTrait;
|
||||
use \JchOptimize\Core\SerializableTrait;
|
||||
use \JchOptimize\Core\StorageTaggingTrait;
|
||||
|
||||
public bool $isBackend;
|
||||
|
||||
private Registry $params;
|
||||
|
||||
private CallbackCache $callbackCache;
|
||||
|
||||
/**
|
||||
* @var null|(Client&ClientInterface)
|
||||
*/
|
||||
private $http;
|
||||
|
||||
/**
|
||||
* @var IterableInterface&StorageInterface&TaggableInterface
|
||||
*/
|
||||
private $taggableCache;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param IterableInterface&StorageInterface&TaggableInterface $taggableCache
|
||||
* @param null|(Client&ClientInterface) $http
|
||||
*/
|
||||
public function __construct(Registry $params, CallbackCache $callbackCache, $taggableCache, FileUtils $fileUtils, $http, bool $isBackend = \false)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->callbackCache = $callbackCache;
|
||||
$this->taggableCache = $taggableCache;
|
||||
$this->fileUtils = $fileUtils;
|
||||
$this->http = $http;
|
||||
$this->isBackend = $isBackend;
|
||||
}
|
||||
|
||||
public function getCssContents(array $urlArray): array
|
||||
{
|
||||
return $this->getContents($urlArray, 'css');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated and possibly minified content from js and css files.
|
||||
*
|
||||
* @param array $urlArray Indexed multidimensional array of urls of css or js files for aggregation
|
||||
* @param string $type css or js
|
||||
*
|
||||
* @return array Aggregated (and possibly minified) contents of files
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getContents(array $urlArray, string $type): array
|
||||
{
|
||||
!JCH_DEBUG ?: Profiler::start('GetContents - '.$type, \true);
|
||||
$aResult = $this->combineFiles($urlArray, $type);
|
||||
$sContents = $this->prepareContents($aResult['content']);
|
||||
if ('css' == $type) {
|
||||
if ($this->params->get('csg_enable', 0)) {
|
||||
try {
|
||||
/** @var Generator $oSpriteGenerator */
|
||||
$oSpriteGenerator = $this->container->get(Generator::class);
|
||||
$aSpriteCss = $oSpriteGenerator->getSprite($sContents);
|
||||
if (!empty($aSpriteCss) && !empty($aSpriteCss['needles']) && !empty($aSpriteCss['replacements'])) {
|
||||
$sContents = \str_replace($aSpriteCss['needles'], $aSpriteCss['replacements'], $sContents);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->logger->error($ex->getMessage());
|
||||
}
|
||||
}
|
||||
$sContents = $aResult['import'].$sContents;
|
||||
if (\function_exists('mb_convert_encoding')) {
|
||||
$sContents = '@charset "utf-8";'.$sContents;
|
||||
}
|
||||
}
|
||||
// Save contents in array to store in cache
|
||||
$aContents = ['filemtime' => \time(), 'etag' => \md5($sContents), 'contents' => $sContents, 'images' => \array_unique($aResult['images']), 'font-face' => $aResult['font-face'], 'preconnects' => $aResult['preconnects'], 'gfonts' => $aResult['gfonts'], 'bgselectors' => $aResult['bgselectors']];
|
||||
!JCH_DEBUG ?: Profiler::stop('GetContents - '.$type);
|
||||
|
||||
return $aContents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate contents of CSS and JS files.
|
||||
*
|
||||
* @param array $fileInfosArray Array of links of files to combine
|
||||
* @param string $type css|js
|
||||
* @param mixed $cacheItems
|
||||
*
|
||||
* @return array Aggregated contents
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function combineFiles(array $fileInfosArray, string $type, $cacheItems = \true): array
|
||||
{
|
||||
$responses = ['content' => '', 'import' => '', 'font-face' => [], 'preconnects' => [], 'images' => [], 'gfonts' => [], 'bgselectors' => []];
|
||||
// Iterate through each file/script to optimize and combine
|
||||
foreach ($fileInfosArray as $fileInfos) {
|
||||
// Truncate url to less than 40 characters
|
||||
$sUrl = $this->prepareFileUrl($fileInfos, $type);
|
||||
!JCH_DEBUG ?: Profiler::start('CombineFile - '.$sUrl);
|
||||
// Try to store tags first
|
||||
$function = [$this, 'cacheContent'];
|
||||
$args = [$fileInfos, $type, \true];
|
||||
$id = $this->callbackCache->generateKey($function, $args);
|
||||
$this->tagStorage($id);
|
||||
// If caching set and tagging was successful we attempt to cache
|
||||
if ($cacheItems && !empty($this->taggableCache->getTags($id))) {
|
||||
// Optimize and cache file/script returning the optimized content
|
||||
$results = $this->callbackCache->call($function, $args);
|
||||
// Append to combined contents
|
||||
$responses['content'] .= $this->addCommentedUrl($type, $fileInfos).$results['content']."\n".'DELIMITER';
|
||||
} else {
|
||||
// If we're not caching just get the optimized content
|
||||
$results = $this->cacheContent($fileInfos, $type, \false);
|
||||
$responses['content'] .= $this->addCommentedUrl($type, $fileInfos).$results['content'].'|"LINE_END"|';
|
||||
}
|
||||
if ('css' == $type) {
|
||||
$responses['import'] .= $results['import'];
|
||||
$responses['images'] = \array_merge($responses['images'], $results['images']);
|
||||
$responses['gfonts'] = \array_merge($responses['gfonts'], $results['gfonts']);
|
||||
$responses['font-face'] = \array_merge($responses['font-face'], $results['font-face']);
|
||||
$responses['preconnects'] = \array_merge($responses['preconnects'], $results['preconnects']);
|
||||
$responses['bgselectors'] = \array_merge($responses['bgselectors'], $results['bgselectors']);
|
||||
}
|
||||
!JCH_DEBUG ?: Profiler::stop('CombineFile - '.$sUrl, \true);
|
||||
}
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize and cache contents of individual file/script returning optimized content.
|
||||
*/
|
||||
public function cacheContent(array $fileInfos, string $type, bool $bPrepare): array
|
||||
{
|
||||
// Initialize content string
|
||||
$content = '';
|
||||
$responses = [];
|
||||
// If it's a file fetch the contents of the file
|
||||
if (isset($fileInfos['url'])) {
|
||||
$content .= $this->getFileContents($fileInfos['url']);
|
||||
// Remove zero-width non-breaking space
|
||||
$content = \trim($content, '');
|
||||
if (\defined('TEST_SITE_DOMAIN') && \defined('TEST_SITE_ALT_DOMAIN') && \defined('TEST_SITE_BASE')) {
|
||||
$content = \str_replace(['{{{domain}}}', '{{{altdomain}}}', '{{{base}}}'], [TEST_SITE_DOMAIN, TEST_SITE_ALT_DOMAIN, TEST_SITE_BASE], $content);
|
||||
}
|
||||
} else {
|
||||
// If it's a declaration just use it
|
||||
$content .= $fileInfos['content'];
|
||||
}
|
||||
if ('css' == $type) {
|
||||
/** @var CssProcessor $oCssProcessor */
|
||||
$oCssProcessor = $this->container->get(CssProcessor::class);
|
||||
$oCssProcessor->setCssInfos($fileInfos);
|
||||
$oCssProcessor->setCss($content);
|
||||
$oCssProcessor->formatCss();
|
||||
$oCssProcessor->processUrls(\false, \false, $this->isBackend);
|
||||
$oCssProcessor->processMediaQueries();
|
||||
$oCssProcessor->processAtRules();
|
||||
$content = $oCssProcessor->getCss();
|
||||
$responses['import'] = $oCssProcessor->getImports();
|
||||
$responses['images'] = $oCssProcessor->getImages();
|
||||
$responses['font-face'] = $oCssProcessor->getFontFace();
|
||||
$responses['gfonts'] = $oCssProcessor->getGFonts();
|
||||
$responses['preconnects'] = $oCssProcessor->getPreconnects();
|
||||
$responses['bgselectors'] = $oCssProcessor->getCssBgImagesSelectors();
|
||||
}
|
||||
if ('js' == $type && '' != \trim($content)) {
|
||||
if ($this->params->get('try_catch', '1')) {
|
||||
$content = $this->addErrorHandler($content, $fileInfos);
|
||||
} else {
|
||||
$content = $this->addSemiColon($content);
|
||||
}
|
||||
}
|
||||
if ($bPrepare) {
|
||||
$content = $this->minifyContent($content, $type, $fileInfos);
|
||||
$content = $this->prepareContents($content);
|
||||
}
|
||||
$responses['content'] = $content;
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove placeholders from aggregated file for caching.
|
||||
*
|
||||
* @param string $contents Aggregated file contents
|
||||
*/
|
||||
public function prepareContents(string $contents, bool $test = \false): string
|
||||
{
|
||||
return \str_replace(['|"COMMENT_START', '|"COMMENT_IMPORT_START', 'COMMENT_END"|', 'DELIMITER', '|"LINE_END"|'], ["\n".'/***! ', "\n\n".'/***! @import url', ' !***/'."\n\n", $test ? 'DELIMITER' : '', "\n"], \trim($contents));
|
||||
}
|
||||
|
||||
public function getJsContents(array $urlArray): array
|
||||
{
|
||||
return $this->getContents($urlArray, 'js');
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when you want to append the contents of files to some that are already combined, into one file.
|
||||
*
|
||||
* @param array $ids Array of ids of files that were already combined
|
||||
* @param array $fileMatches Array of file matches to be combined
|
||||
* @param string $type Type of files css|js
|
||||
*
|
||||
* @return array The contents of the combined files
|
||||
*/
|
||||
public function appendFiles(array $ids, array $fileMatches, string $type): array
|
||||
{
|
||||
$contents = '';
|
||||
foreach ($ids as $id) {
|
||||
$contents .= \JchOptimize\Core\Output::getCombinedFile(['f' => $id, 'type' => $type], \false);
|
||||
}
|
||||
|
||||
try {
|
||||
$results = $this->combineFiles($fileMatches, $type);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Error appending files: '.$e->getMessage());
|
||||
$results = ['content' => '', 'font-face' => [], 'gfonts' => [], 'images' => []];
|
||||
}
|
||||
$contents .= $this->prepareContents($results['content']);
|
||||
$contents .= "\n".'jchOptimizeDynamicScriptLoader.next()';
|
||||
|
||||
return ['filemtime' => \time(), 'etag' => \md5($contents), 'contents' => $contents, 'font-face' => $results['font-face'], 'preconnects' => $results['preconnects'], 'images' => $results['images']];
|
||||
}
|
||||
|
||||
protected function addCommentedUrl(string $type, array $fileInfos): string
|
||||
{
|
||||
$comment = '';
|
||||
if ($this->params->get('debug', '1')) {
|
||||
$fileInfos = $fileInfos['url'] ?? ('js' == $type ? 'script' : 'style').' declaration';
|
||||
$comment = '|"COMMENT_START '.$fileInfos.' COMMENT_END"|';
|
||||
}
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
private function getFileContents(UriInterface $uri): string
|
||||
{
|
||||
$uri = UriResolver::resolve(\JchOptimize\Core\SystemUri::currentUri(), $uri);
|
||||
if (!UriComparator::isCrossOrigin($uri)) {
|
||||
$filePath = UriConverter::uriToFilePath($uri);
|
||||
if (\file_exists($filePath) && \JchOptimize\Core\Helper::isStaticFile($filePath)) {
|
||||
try {
|
||||
$stream = Utils::streamFor(Utils::tryFopen($filePath, 'r'));
|
||||
if (!$stream->isReadable()) {
|
||||
throw new \Exception('Stream unreadable');
|
||||
}
|
||||
if ($stream->isSeekable()) {
|
||||
$stream->rewind();
|
||||
}
|
||||
|
||||
return $stream->getContents();
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Couldn\'t open file: '.$uri.'; error: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$options = [RequestOptions::HEADERS => ['Accept-Enconding' => 'identity;q=0']];
|
||||
$response = $this->http->get($uri, $options);
|
||||
if (200 === $response->getStatusCode()) {
|
||||
// Get body and set pointer to beginning of stream
|
||||
$body = $response->getBody();
|
||||
$body->rewind();
|
||||
|
||||
return $body->getContents();
|
||||
}
|
||||
|
||||
return '|"COMMENT_START Response returned status code: '.$response->getStatusCode().' COMMENT_END"|';
|
||||
} catch (GuzzleException $e) {
|
||||
return '|"COMMENT_START Exception fetching file with message: '.$e->getMessage().' COMMENT_END"|';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add try catch to contents of javascript file.
|
||||
*/
|
||||
private function addErrorHandler(string $content, array $fileInfos): string
|
||||
{
|
||||
if (empty($fileInfos['module']) || 'module' != $fileInfos['module']) {
|
||||
$content = 'try {'."\n".$content."\n".'} catch (e) {'."\n";
|
||||
$content .= 'console.error(\'Error in ';
|
||||
$content .= isset($fileInfos['url']) ? 'file:'.$fileInfos['url'] : 'script declaration';
|
||||
$content .= '; Error:\' + e.message);'."\n".'};';
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add semicolon to end of js files if non exists;.
|
||||
*/
|
||||
private function addSemiColon(string $content): string
|
||||
{
|
||||
$content = \rtrim($content);
|
||||
if (';' != \substr($content, -1) && !\preg_match('#\\|"COMMENT_START File[^"]+not found COMMENT_END"\\|#', $content)) {
|
||||
$content = $content.';';
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minify contents of fil.
|
||||
*
|
||||
* @return string $sMinifiedContent Minified content or original content if failed
|
||||
*/
|
||||
private function minifyContent(string $content, string $type, array $fileInfos): string
|
||||
{
|
||||
if ($this->params->get($type.'_minify', 0)) {
|
||||
$url = $this->prepareFileUrl($fileInfos, $type);
|
||||
$minifiedContent = \trim('css' == $type ? Css::optimize($content) : Js::optimize($content));
|
||||
// @TODO inject Exception class into minifier libraries
|
||||
if (0 !== \preg_last_error()) {
|
||||
$this->logger->error(\sprintf('Error occurred trying to minify: %s', $url));
|
||||
$minifiedContent = $content;
|
||||
}
|
||||
$this->_debug($url, '', 'minifyContent');
|
||||
|
||||
return $minifiedContent;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2023 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Container;
|
||||
|
||||
use _JchOptimizeVendor\Laminas\EventManager\SharedEventManager;
|
||||
use _JchOptimizeVendor\Laminas\EventManager\SharedEventManagerInterface;
|
||||
use JchOptimize\ContainerFactory;
|
||||
use JchOptimize\Core\Service\CachingConfigurationProvider;
|
||||
use JchOptimize\Core\Service\CachingProvider;
|
||||
use JchOptimize\Core\Service\CallbackProvider;
|
||||
use JchOptimize\Core\Service\CoreProvider;
|
||||
use JchOptimize\Core\Service\FeatureHelpersProvider;
|
||||
use JchOptimize\Core\Service\IlluminateViewFactoryProvider;
|
||||
use JchOptimize\Core\Service\SpatieProvider;
|
||||
|
||||
abstract class AbstractContainerFactory
|
||||
{
|
||||
/**
|
||||
* @var null|Container
|
||||
*/
|
||||
protected static ?\JchOptimize\Core\Container\Container $instance = null;
|
||||
|
||||
/**
|
||||
* Used to create a new global instance of Joomla/DI/Container or in cases where the container isn't
|
||||
* accessible by dependency injection.
|
||||
*/
|
||||
public static function getContainer(): Container
|
||||
{
|
||||
if (\is_null(self::$instance)) {
|
||||
self::$instance = self::getNewContainerInstance();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to return a new instance of the Container when we're making changes we don't want to affect the
|
||||
* global container.
|
||||
*/
|
||||
public static function getNewContainerInstance(): Container
|
||||
{
|
||||
$ContainerFactory = new ContainerFactory();
|
||||
$container = new \JchOptimize\Core\Container\Container();
|
||||
$ContainerFactory->registerCoreProviders($container);
|
||||
$ContainerFactory->registerPlatformProviders($container);
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* For use with test cases.
|
||||
*/
|
||||
public static function destroy(): void
|
||||
{
|
||||
self::$instance = null;
|
||||
}
|
||||
|
||||
protected function registerCoreProviders(Container $container): void
|
||||
{
|
||||
$container->alias(SharedEventManager::class, SharedEventManagerInterface::class)->share(SharedEventManagerInterface::class, new SharedEventManager(), \true)->registerServiceProvider(new CoreProvider())->registerServiceProvider(new CachingConfigurationProvider())->registerServiceProvider(new CallbackProvider())->registerServiceProvider(new CachingProvider())->registerServiceProvider(new IlluminateViewFactoryProvider());
|
||||
if (JCH_PRO) {
|
||||
$container->registerServiceProvider(new FeatureHelpersProvider())->registerServiceProvider(new SpatieProvider());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To be implemented by JchOptimize/Container to attach service providers specific to the particular platform.
|
||||
*/
|
||||
abstract protected function registerPlatformProviders(Container $container): void;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2023 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Container;
|
||||
|
||||
class Container extends \_JchOptimizeVendor\Joomla\DI\Container
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Callbacks;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use JchOptimize\Core\Container\Container;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
abstract class AbstractCallback implements ContainerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
|
||||
protected Registry $params;
|
||||
|
||||
public function __construct(Container $container, Registry $params)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $matches
|
||||
*/
|
||||
abstract public function processMatches(array $matches, string $context): string;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Callbacks;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class CombineMediaQueries extends \JchOptimize\Core\Css\Callbacks\AbstractCallback
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $cssInfos = [];
|
||||
|
||||
public function processMatches(array $matches, string $context): string
|
||||
{
|
||||
if (empty($this->cssInfos['media'])) {
|
||||
return $matches[0];
|
||||
}
|
||||
if ('media' == $context) {
|
||||
return '@media '.$this->combineMediaQueries($this->cssInfos['media'], \trim(\substr($matches[2], 6))).'{'.$matches[4].'}';
|
||||
}
|
||||
if ('import' == $context) {
|
||||
$sMediaQuery = $matches[7];
|
||||
$sAtImport = \substr($matches[0], 0, -\strlen($sMediaQuery.';'));
|
||||
|
||||
return $sAtImport.' '.$this->combineMediaQueries($this->cssInfos['media'], $sMediaQuery).';';
|
||||
}
|
||||
|
||||
return '@media '.$this->cssInfos['media'].'{'.$matches[0].'}';
|
||||
}
|
||||
|
||||
public function setCssInfos($cssInfos): void
|
||||
{
|
||||
$this->cssInfos = $cssInfos;
|
||||
}
|
||||
|
||||
protected function combineMediaQueries(string $parentMediaQueryList, string $childMediaQueryList): string
|
||||
{
|
||||
$parentMediaQueries = \preg_split('#\\s++or\\s++|,#i', $parentMediaQueryList);
|
||||
$childMediaQueries = \preg_split('#\\s++or\\s++|,#i', $childMediaQueryList);
|
||||
// $aMediaTypes = array('all', 'aural', 'braille', 'handheld', 'print', 'projection', 'screen', 'tty', 'tv', 'embossed');
|
||||
$mediaQueries = [];
|
||||
foreach ($parentMediaQueries as $parentMediaQuery) {
|
||||
$parentMediaQueryMatches = $this->parseMediaQuery(\trim($parentMediaQuery));
|
||||
foreach ($childMediaQueries as $childMediaQuery) {
|
||||
$mediaQuery = '';
|
||||
$childMediaQueryMatches = $this->parseMediaQuery(\trim($childMediaQuery));
|
||||
if ('only' == $parentMediaQueryMatches['keyword'] || 'only' == $childMediaQueryMatches['keyword']) {
|
||||
$mediaQuery .= 'only ';
|
||||
}
|
||||
if ('not' == $parentMediaQueryMatches['keyword'] && '' == $childMediaQueryMatches['keyword']) {
|
||||
if ('all' == $parentMediaQueryMatches['media_type']) {
|
||||
$mediaQuery .= '(not '.$parentMediaQueryMatches['media_type'].')';
|
||||
} elseif ($parentMediaQueryMatches['media_type'] == $childMediaQueryMatches['media_type']) {
|
||||
$mediaQuery .= '(not '.$parentMediaQueryMatches['media_type'].') and '.$childMediaQueryMatches['media_type'];
|
||||
} else {
|
||||
$mediaQuery .= $childMediaQueryMatches['media_type'];
|
||||
}
|
||||
} elseif ('' == $parentMediaQueryMatches['keyword'] && 'not' == $childMediaQueryMatches['keyword']) {
|
||||
if ('all' == $childMediaQueryMatches['media_type']) {
|
||||
$mediaQuery .= '(not '.$childMediaQueryMatches['media_type'].')';
|
||||
} elseif ($parentMediaQueryMatches['media_type'] == $childMediaQueryMatches['media_type']) {
|
||||
$mediaQuery .= $parentMediaQueryMatches['media_type'].' and (not '.$childMediaQueryMatches['media_type'].')';
|
||||
} else {
|
||||
$mediaQuery .= $childMediaQueryMatches['media_type'];
|
||||
}
|
||||
} elseif ('not' == $parentMediaQueryMatches['keyword'] && 'not' == $childMediaQueryMatches['keyword']) {
|
||||
$mediaQuery .= 'not '.$childMediaQueryMatches['keyword'];
|
||||
} else {
|
||||
if ($parentMediaQueryMatches['media_type'] == $childMediaQueryMatches['media_type'] || 'all' == $parentMediaQueryMatches['media_type']) {
|
||||
$mediaQuery .= $childMediaQueryMatches['media_type'];
|
||||
} elseif ('all' == $childMediaQueryMatches['media_type']) {
|
||||
$mediaQuery .= $parentMediaQueryMatches['media_type'];
|
||||
} else {
|
||||
// Two different media types are nested and neither is 'all' then
|
||||
// the enclosed rule will not be applied on any media type
|
||||
// We put 'not all' to maintain a syntactically correct combined media type
|
||||
$mediaQuery .= 'not all';
|
||||
// Don't bother including media features in the media query
|
||||
$mediaQueries[] = $mediaQuery;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (isset($parentMediaQueryMatches['expression'])) {
|
||||
$mediaQuery .= ' and '.$parentMediaQueryMatches['expression'];
|
||||
}
|
||||
if (isset($childMediaQueryMatches['expression'])) {
|
||||
$mediaQuery .= ' and '.$childMediaQueryMatches['expression'];
|
||||
}
|
||||
$mediaQueries[] = $mediaQuery;
|
||||
}
|
||||
}
|
||||
|
||||
return \implode(', ', \array_unique($mediaQueries));
|
||||
}
|
||||
|
||||
protected function parseMediaQuery(string $sMediaQuery): array
|
||||
{
|
||||
$aParts = [];
|
||||
$sMediaQuery = \preg_replace(['#\\(\\s++#', '#\\s++\\)#'], ['(', ')'], $sMediaQuery);
|
||||
\preg_match('#(?:\\(?(not|only)\\)?)?\\s*+(?:\\(?(all|screen|print|speech|aural|tv|tty|projection|handheld|braille|embossed)\\)?)?(?:\\s++and\\s++)?(.++)?#si', $sMediaQuery, $aMatches);
|
||||
$aParts['keyword'] = isset($aMatches[1]) ? \strtolower($aMatches[1]) : '';
|
||||
if (isset($aMatches[2]) && '' != $aMatches[2]) {
|
||||
$aParts['media_type'] = \strtolower($aMatches[2]);
|
||||
} else {
|
||||
$aParts['media_type'] = 'all';
|
||||
}
|
||||
if (isset($aMatches[3]) && '' != $aMatches[3]) {
|
||||
$aParts['expression'] = $aMatches[3];
|
||||
}
|
||||
|
||||
return $aParts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Callbacks;
|
||||
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\Uri;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\UriResolver;
|
||||
use _JchOptimizeVendor\Joomla\DI\Container;
|
||||
use JchOptimize\Core\Cdn;
|
||||
use JchOptimize\Core\Css\Parser;
|
||||
use JchOptimize\Core\FeatureHelpers\LazyLoadExtended;
|
||||
use JchOptimize\Core\FeatureHelpers\Webp;
|
||||
use JchOptimize\Core\Http2Preload;
|
||||
use JchOptimize\Core\SystemUri;
|
||||
use JchOptimize\Core\Uri\UriComparator;
|
||||
use JchOptimize\Core\Uri\Utils;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class CorrectUrls extends \JchOptimize\Core\Css\Callbacks\AbstractCallback
|
||||
{
|
||||
/** @var bool True if this callback is called when preloading assets for HTTP/2 */
|
||||
public bool $isHttp2 = \false;
|
||||
|
||||
/** @var bool If Optimize CSS Delivery is disabled, only fonts are preloaded */
|
||||
public bool $isFontsOnly = \false;
|
||||
|
||||
/** @var bool If run from admin we populate the array */
|
||||
public bool $isBackend = \false;
|
||||
|
||||
public Cdn $cdn;
|
||||
|
||||
public Http2Preload $http2Preload;
|
||||
public array $cssBgImagesSelectors = [];
|
||||
|
||||
private array $images = [];
|
||||
|
||||
/** @var array An array of external domains that we'll add preconnects for */
|
||||
private array $preconnects = [];
|
||||
|
||||
private array $cssInfos;
|
||||
|
||||
public function __construct(Container $container, Registry $params, Cdn $cdn, Http2Preload $http2Preload)
|
||||
{
|
||||
parent::__construct($container, $params);
|
||||
$this->cdn = $cdn;
|
||||
$this->http2Preload = $http2Preload;
|
||||
}
|
||||
|
||||
public function processMatches(array $matches, string $context): string
|
||||
{
|
||||
$sRegex = '(?>u?[^u]*+)*?\\K(?:'.Parser::cssUrlWithCaptureValueToken(\true).'|$)';
|
||||
if ('import' == $context) {
|
||||
$sRegex = Parser::cssAtImportWithCaptureValueToken(\true);
|
||||
}
|
||||
$css = \preg_replace_callback('#'.$sRegex.'#i', function ($aInnerMatches) use ($context) {
|
||||
return $this->processInnerMatches($aInnerMatches, $context);
|
||||
}, $matches[0]);
|
||||
// Lazy-load background images
|
||||
if (JCH_PRO && $this->params->get('lazyload_enable', '0') && $this->params->get('pro_lazyload_bgimages', '0') && !\in_array($context, ['font-face', 'import'])) {
|
||||
// @see LazyLoadExtended::handleCssBgImages()
|
||||
return $this->getContainer()->get(LazyLoadExtended::class)->handleCssBgImages($this, $css);
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
public function setCssInfos($cssInfos): void
|
||||
{
|
||||
$this->cssInfos = $cssInfos;
|
||||
}
|
||||
|
||||
public function getImages(): array
|
||||
{
|
||||
return $this->images;
|
||||
}
|
||||
|
||||
public function getPreconnects(): array
|
||||
{
|
||||
return $this->preconnects;
|
||||
}
|
||||
|
||||
public function getCssBgImagesSelectors(): array
|
||||
{
|
||||
return $this->cssBgImagesSelectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $matches
|
||||
* @param mixed $context
|
||||
*
|
||||
* @psalm-param array<string> $matches
|
||||
*/
|
||||
protected function processInnerMatches(array $matches, $context)
|
||||
{
|
||||
if (empty($matches[0])) {
|
||||
return $matches[0];
|
||||
}
|
||||
$originalUri = Utils::uriFor($matches[1]);
|
||||
if ('data' !== $originalUri->getScheme() && '' != $originalUri->getPath() && '/' != $originalUri->getPath()) {
|
||||
// The urls were already corrected on a previous run, we're only preloading assets in critical CSS and return
|
||||
if ($this->isHttp2) {
|
||||
$sFileType = 'font-face' == $context ? 'font' : 'image';
|
||||
// If Optimize CSS Delivery not enabled, we'll only preload fonts.
|
||||
if ($this->isFontsOnly && 'font' != $sFileType) {
|
||||
return \false;
|
||||
}
|
||||
$this->http2Preload->add($originalUri, $sFileType);
|
||||
|
||||
return \true;
|
||||
}
|
||||
// Get the url of the file that contained the CSS
|
||||
$cssFileUri = empty($this->cssInfos['url']) ? new Uri() : $this->cssInfos['url'];
|
||||
$cssFileUri = UriResolver::resolve(SystemUri::currentUri(), $cssFileUri);
|
||||
$imageUri = UriResolver::resolve($cssFileUri, $originalUri);
|
||||
if (!UriComparator::isCrossOrigin($imageUri)) {
|
||||
$imageUri = $this->cdn->loadCdnResource($imageUri);
|
||||
} elseif ($this->params->get('pro_optimizeFonts_enable', '0')) {
|
||||
// Cache external domains to add preconnects for them
|
||||
$domain = Uri::composeComponents($imageUri->getScheme(), $imageUri->getAuthority(), '', '', '');
|
||||
if (!\in_array($domain, $this->preconnects)) {
|
||||
$this->preconnects[] = $domain;
|
||||
}
|
||||
}
|
||||
if ($this->isBackend && 'font-face' != $context) {
|
||||
$this->images[] = $imageUri;
|
||||
}
|
||||
if (JCH_PRO && $this->params->get('pro_load_webp_images', '0')) {
|
||||
/** @see Webp::getWebpImages() */
|
||||
$imageUri = $this->getContainer()->get(Webp::class)->getWebpImages($imageUri) ?? $imageUri;
|
||||
}
|
||||
// If URL without quotes and contains any parentheses, whitespace characters,
|
||||
// single quotes (') and double quotes (") that are part of the URL, quote URL
|
||||
if (\false !== \strpos($matches[0], 'url('.$originalUri.')') && \preg_match('#[()\\s\'"]#', $imageUri)) {
|
||||
$imageUri = '"'.$imageUri.'"';
|
||||
}
|
||||
|
||||
return \str_replace($matches[1], $imageUri, $matches[0]);
|
||||
}
|
||||
|
||||
return $matches[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Callbacks;
|
||||
|
||||
use CodeAlfa\RegexTokenizer\Debug\Debug;
|
||||
use JchOptimize\Core\Css\Parser;
|
||||
use JchOptimize\Core\FeatureHelpers\DynamicSelectors;
|
||||
|
||||
use function str_replace;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class ExtractCriticalCss extends \JchOptimize\Core\Css\Callbacks\AbstractCallback
|
||||
{
|
||||
use Debug;
|
||||
public string $sHtmlAboveFold;
|
||||
public string $sFullHtml;
|
||||
public \DOMXPath $oXPath;
|
||||
public string $postCss = '';
|
||||
public string $preCss = '';
|
||||
public bool $isPostProcessing = \false;
|
||||
protected string $criticalCss = '';
|
||||
|
||||
public function processMatches(array $matches, string $context): string
|
||||
{
|
||||
$this->_debug($matches[0], $matches[0], 'beginExtractCriticalCss');
|
||||
if ('font-face' == $context || 'keyframes' == $context) {
|
||||
if (!$this->isPostProcessing) {
|
||||
// If we're not processing font-face or keyframes yet let's just save them for later until after we've done getting all the
|
||||
// critical css
|
||||
$this->postCss .= $matches[0];
|
||||
|
||||
return '';
|
||||
}
|
||||
if ('font-face' == $context) {
|
||||
\preg_match('#font-family\\s*+:\\s*+[\'"]?('.Parser::stringValueToken().'|[^;}]++)[\'"]?#i', $matches[0], $aM);
|
||||
// Only include fonts in the critical CSS that are being used above the fold
|
||||
// @TODO prevent duplication of fonts in critical css
|
||||
if (!empty($aM[1]) && \false !== \stripos($this->criticalCss, $aM[1])) {
|
||||
// $this->aFonts[] = $aM[1];
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
$sRule = \preg_replace('#@[^\\s{]*+\\s*+#', '', $matches[2]);
|
||||
if (!empty($sRule) && \false !== \stripos($this->criticalCss, $sRule)) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
// We'll compile these to prepend to the critical CSS, imported Google font files will never be expanded.
|
||||
if ('import' == $context) {
|
||||
$this->preCss .= $matches[0];
|
||||
}
|
||||
// We're only interested in global and conditional css
|
||||
if (!\in_array($context, ['global', 'media', 'supports', 'document'])) {
|
||||
return '';
|
||||
}
|
||||
if (JCH_PRO) {
|
||||
// @see DynamicSelectors::getDynamicSelectors()
|
||||
if ($this->getContainer()->get(DynamicSelectors::class)->getDynamicSelectors($matches)) {
|
||||
$this->appendToCriticalCss($matches[0]);
|
||||
$this->_debug('', '', 'afterAddDynamicCss');
|
||||
|
||||
return $matches[0];
|
||||
}
|
||||
$this->_debug('', '', 'afterSearchDynamicCss');
|
||||
}
|
||||
$sSelectorGroup = $matches[2];
|
||||
// Split selector groups into individual selector chains
|
||||
$aSelectorChains = \array_filter(\explode(',', $sSelectorGroup));
|
||||
$aFoundSelectorChains = [];
|
||||
// Iterate through each selector chain
|
||||
foreach ($aSelectorChains as $sSelectorChain) {
|
||||
// If selector chain is a pseudo selector we'll add this group
|
||||
if (\preg_match('#^:#', $sSelectorChain)) {
|
||||
$this->appendToCriticalCss($matches[0]);
|
||||
|
||||
return $matches[0];
|
||||
}
|
||||
// Remove pseudo-selectors
|
||||
$sSelectorChain = \preg_replace('#::?[a-zA-Z0-9-]++(\\((?>[^()]|(?1))*\\))?#', '', $sSelectorChain);
|
||||
// If Selector chain is already in critical css just go ahead and add this group
|
||||
if (\false !== \strpos($this->criticalCss, $sSelectorChain)) {
|
||||
$this->appendToCriticalCss($matches[0]);
|
||||
// Retain matched CSS in combined CSS
|
||||
return $matches[0];
|
||||
}
|
||||
// Check CSS selector chain against HTMl above the fold to find a match
|
||||
if ($this->checkCssAgainstHtml($sSelectorChain, $this->sHtmlAboveFold)) {
|
||||
// Match found, add selector chain to array
|
||||
$aFoundSelectorChains[] = $sSelectorChain;
|
||||
}
|
||||
}
|
||||
// If no valid selector chain was found in the group then we don't add this selector group to the critical CSS
|
||||
if (empty($aFoundSelectorChains)) {
|
||||
$this->_debug($sSelectorGroup, $matches[0], 'afterSelectorNotFound');
|
||||
// Don't add to critical css
|
||||
return '';
|
||||
}
|
||||
// Group the found selector chains
|
||||
$sFoundSelectorGroup = \implode(',', \array_unique($aFoundSelectorChains));
|
||||
// remove any backslash used for escaping
|
||||
// $sFoundSelectorGroup = str_replace('\\', '', $sFoundSelectorGroup);
|
||||
$this->_debug($sFoundSelectorGroup, $matches[0], 'afterSelectorFound');
|
||||
$success = null;
|
||||
// Convert the selector group to Xpath
|
||||
$sXPath = $this->convertCss2XPath($sFoundSelectorGroup, $success);
|
||||
$this->_debug($sXPath, $matches[0], 'afterConvertCss2XPath');
|
||||
if (\false !== $success) {
|
||||
$aXPaths = \array_unique(\explode(' | ', \str_replace('\\', '', $sXPath)));
|
||||
foreach ($aXPaths as $sXPathValue) {
|
||||
$oElement = $this->oXPath->query($sXPathValue);
|
||||
// if ($oElement === FALSE)
|
||||
// {
|
||||
// echo $aMatches[1] . "\n";
|
||||
// echo $sXPath . "\n";
|
||||
// echo $sXPathValue . "\n";
|
||||
// echo "\n\n";
|
||||
// }
|
||||
// Match found! Add to critical CSS
|
||||
if (\false !== $oElement && $oElement->length) {
|
||||
$this->appendToCriticalCss($matches[0]);
|
||||
$this->_debug($sXPathValue, $matches[0], 'afterCriticalCssFound');
|
||||
|
||||
return $matches[0];
|
||||
}
|
||||
$this->_debug($sXPathValue, $matches[0], 'afterCriticalCssNotFound');
|
||||
}
|
||||
}
|
||||
// No match found for critical CSS.
|
||||
return '';
|
||||
}
|
||||
|
||||
public function appendToCriticalCss(string $css): void
|
||||
{
|
||||
$this->criticalCss .= $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function convertCss2XPath(string $sSelector, ?bool &$success = null): ?string
|
||||
{
|
||||
$sSelector = \preg_replace('#\\s*([>+~,])\\s*#', '$1', $sSelector);
|
||||
$sSelector = \trim($sSelector);
|
||||
$sSelector = \preg_replace('#\\s+#', ' ', $sSelector);
|
||||
if (null === $sSelector) {
|
||||
$success = \false;
|
||||
|
||||
return null;
|
||||
}
|
||||
$sSelectorRegex = '#(?!$)([>+~, ]?)([*_a-z0-9-]*)(?:(([.\\#])((?:[_a-z0-9-]|\\\\[^\\r\\n\\f0-9a-z])+))(([.\\#])((?:[_a-z0-9-]|\\\\[^\\r\\n\\f0-9a-z])+))?|(\\[((?:[_a-z0-9-]|\\\\[^\\r\\n\\f0-9a-z])+)(([~|^$*]?=)["\']?([^\\]"\']+)["\']?)?\\]))*#i';
|
||||
$result = \preg_replace_callback($sSelectorRegex, [$this, '_tokenizer'], $sSelector).'[1]';
|
||||
if (null === $result) {
|
||||
$success = \false;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a preliminary simple check to see if a CSS declaration is used by the HTML.
|
||||
*
|
||||
* @return bool True is all parts of the CSS selector is found in the HTML, false if not
|
||||
*/
|
||||
protected function checkCssAgainstHtml(string $selectorChain, string $html): bool
|
||||
{
|
||||
// Split selector chain into simple selectors
|
||||
$aSimpleSelectors = \preg_split('#[^\\[ >+]*+(?:\\[[^\\]]*+\\])?\\K(?:[ >+]*+|$)#', \trim($selectorChain), -1, \PREG_SPLIT_NO_EMPTY);
|
||||
// We'll do a quick check first if all parts of each simple selector is found in the HTML
|
||||
// Iterate through each simple selector
|
||||
foreach ($aSimpleSelectors as $sSimpleSelector) {
|
||||
// Match the simple selector into its components
|
||||
$sSimpleSelectorRegex = '#([_a-z0-9-]*)(?:([.\\#]((?:[_a-z0-9-]|\\\\[^\\r\\n\\f0-9a-z])+))|(\\[((?:[_a-z0-9-]|\\\\[^\\r\\n\\f0-9a-z])+)(?:[~|^$*]?=(?|"([^"\\]]*+)"|\'([^\'\\]]*+)\'|([^\\]]*+)))?\\]))*#i';
|
||||
if (\preg_match($sSimpleSelectorRegex, $sSimpleSelector, $aS)) {
|
||||
// Elements
|
||||
if (!empty($aS[1])) {
|
||||
$sNeedle = '<'.$aS[1];
|
||||
// Just include elements that will be generated by the browser
|
||||
$aDynamicElements = ['<tbody'];
|
||||
if (\in_array($sNeedle, $aDynamicElements)) {
|
||||
continue;
|
||||
}
|
||||
if (\false === \strpos($html, $sNeedle)) {
|
||||
// Element part of selector not found,
|
||||
// abort and check next selector chain
|
||||
return \false;
|
||||
}
|
||||
}
|
||||
// Attribute selectors
|
||||
if (!empty($aS[4])) {
|
||||
// If the value of the attribute is set we'll look for that
|
||||
// otherwise just look for the attribute
|
||||
$sNeedle = !empty($aS[6]) ? $aS[6] : $aS[5];
|
||||
// . '="';
|
||||
if (!empty($sNeedle) && \false === \strpos($html, \str_replace('\\', '', $sNeedle))) {
|
||||
// Attribute part of selector not found,
|
||||
// abort and check next selector chain
|
||||
return \false;
|
||||
}
|
||||
}
|
||||
// Ids or Classes
|
||||
if (!empty($aS[2])) {
|
||||
$sNeedle = ' '.$aS[3].' ';
|
||||
if (\false === \strpos($html, \str_replace('\\', '', $sNeedle))) {
|
||||
// The id or class part of selector not found,
|
||||
// abort and check next selector chain
|
||||
return \false;
|
||||
}
|
||||
}
|
||||
// we found this Selector so let's remove it from the chain in case we need to check it
|
||||
// against the HTML below the fold
|
||||
\str_replace($sSimpleSelector, '', $selectorChain);
|
||||
}
|
||||
}
|
||||
// If we get to this point then we've found a simple selector that has all parts in the
|
||||
// HTML. Let's save this selector chain and refine its search with Xpath.
|
||||
return \true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $aM
|
||||
*/
|
||||
protected function _tokenizer(array $aM): string
|
||||
{
|
||||
$sXPath = '';
|
||||
|
||||
switch ($aM[1]) {
|
||||
case '>':
|
||||
$sXPath .= '/';
|
||||
|
||||
break;
|
||||
|
||||
case '+':
|
||||
$sXPath .= '/following-sibling::*';
|
||||
|
||||
break;
|
||||
|
||||
case '~':
|
||||
$sXPath .= '/following-sibling::';
|
||||
|
||||
break;
|
||||
|
||||
case ',':
|
||||
$sXPath .= '[1] | descendant-or-self::';
|
||||
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
$sXPath .= '/descendant::';
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$sXPath .= 'descendant-or-self::';
|
||||
|
||||
break;
|
||||
}
|
||||
if ('+' != $aM[1]) {
|
||||
$sXPath .= '' == $aM[2] ? '*' : $aM[2];
|
||||
}
|
||||
if (isset($aM[3]) || isset($aM[9])) {
|
||||
$sXPath .= '[';
|
||||
$aPredicates = [];
|
||||
if (isset($aM[4]) && '.' == $aM[4]) {
|
||||
$aPredicates[] = "contains(@class, ' ".$aM[5]." ')";
|
||||
}
|
||||
if (isset($aM[7]) && '.' == $aM[7]) {
|
||||
$aPredicates[] = "contains(@class, ' ".$aM[8]." ')";
|
||||
}
|
||||
if (isset($aM[4]) && '#' == $aM[4]) {
|
||||
$aPredicates[] = "@id = ' ".$aM[5]." '";
|
||||
}
|
||||
if (isset($aM[7]) && '#' == $aM[7]) {
|
||||
$aPredicates[] = "@id = ' ".$aM[8]." '";
|
||||
}
|
||||
if (isset($aM[9])) {
|
||||
if (!isset($aM[11])) {
|
||||
$aPredicates[] = '@'.$aM[10];
|
||||
} else {
|
||||
switch ($aM[12]) {
|
||||
case '=':
|
||||
$aPredicates[] = "@{$aM[10]} = ' {$aM[13]} '";
|
||||
|
||||
break;
|
||||
|
||||
case '|=':
|
||||
$aPredicates[] = "(@{$aM[10]} = ' {$aM[13]} ' or starts-with(@{$aM[10]}, ' {$aM[13]}'))";
|
||||
|
||||
break;
|
||||
|
||||
case '^=':
|
||||
$aPredicates[] = "starts-with(@{$aM[10]}, ' {$aM[13]}')";
|
||||
|
||||
break;
|
||||
|
||||
case '$=':
|
||||
$aPredicates[] = "substring(@{$aM[10]}, string-length(@{$aM[10]})-".\strlen($aM[13]).") = '{$aM[13]} '";
|
||||
|
||||
break;
|
||||
|
||||
case '~=':
|
||||
$aPredicates[] = "contains(@{$aM[10]}, ' {$aM[13]} ')";
|
||||
|
||||
break;
|
||||
|
||||
case '*=':
|
||||
$aPredicates[] = "contains(@{$aM[10]}, '{$aM[13]}')";
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('+' == $aM[1]) {
|
||||
if ('' != $aM[2]) {
|
||||
$aPredicates[] = "(name() = '".$aM[2]."')";
|
||||
}
|
||||
$aPredicates[] = '(position() = 1)';
|
||||
}
|
||||
$sXPath .= \implode(' and ', $aPredicates);
|
||||
$sXPath .= ']';
|
||||
}
|
||||
|
||||
return $sXPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Callbacks;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class FormatCss extends \JchOptimize\Core\Css\Callbacks\AbstractCallback
|
||||
{
|
||||
public string $validCssRules;
|
||||
|
||||
public function processMatches(array $matches, string $context): string
|
||||
{
|
||||
if (isset($matches[7]) && !\preg_match('#'.$this->validCssRules.'#i', $matches[7])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $matches[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Callbacks;
|
||||
|
||||
use JchOptimize\Core\Combiner;
|
||||
use JchOptimize\Core\Html\FilesManager;
|
||||
use JchOptimize\Core\Uri\Utils;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class HandleAtRules extends \JchOptimize\Core\Css\Callbacks\AbstractCallback
|
||||
{
|
||||
private array $atImports = [];
|
||||
private array $gFonts = [];
|
||||
private array $fontFace = [];
|
||||
|
||||
private array $cssInfos;
|
||||
|
||||
public function processMatches(array $matches, string $context): string
|
||||
{
|
||||
if ('charset' == $context) {
|
||||
return '';
|
||||
}
|
||||
if ('font-face' == $context) {
|
||||
if (!\preg_match('#font-display#i', $matches[0])) {
|
||||
$matches[0] = \preg_replace('#;?\\s*}$#', ';font-display:swap;}', $matches[0]);
|
||||
} elseif (\preg_match('#font-display#i', $matches[0]) && $this->params->get('pro_force_swap_policy', '1')) {
|
||||
$matches[0] = \preg_replace('#font-display[^;}]++#i', 'font-display:swap', $matches[0]);
|
||||
}
|
||||
if ($this->params->get('pro_optimizeFonts_enable', '0') && empty($this->cssInfos['combining-fontface'])) {
|
||||
$this->fontFace[] = ['content' => $matches[0], 'media' => $this->cssInfos['media']];
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return $matches[0];
|
||||
}
|
||||
// At this point we should be in import context
|
||||
$uri = Utils::uriFor($matches[3]);
|
||||
$media = $matches[4];
|
||||
// If we're importing a Google font file we may need to optimize it
|
||||
if ($this->params->get('pro_optimizeFonts_enable', '0') && 'fonts.googleapis.com' == $uri->getHost()) {
|
||||
// We have to add Gfonts here so this info will be cached
|
||||
$this->gFonts[] = ['url' => $uri, 'media' => $media];
|
||||
|
||||
return '';
|
||||
}
|
||||
// Don't import Google font files even if replaceImports is enabled
|
||||
if (!$this->params->get('replaceImports', '0') || 'fonts.googleapis.com' == $uri->getHost()) {
|
||||
$this->atImports[] = $matches[0];
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/** @var FilesManager $oFilesManager */
|
||||
$oFilesManager = $this->getContainer()->get(FilesManager::class);
|
||||
if ('' == (string) $uri || 'https' == $uri->getScheme() && !\extension_loaded('openssl')) {
|
||||
return $matches[0];
|
||||
}
|
||||
if ($oFilesManager->isDuplicated($uri)) {
|
||||
return '';
|
||||
}
|
||||
$aUrlArray = [];
|
||||
$aUrlArray[0]['url'] = $uri;
|
||||
$aUrlArray[0]['media'] = $media;
|
||||
|
||||
/** @var Combiner $oCombiner */
|
||||
$oCombiner = $this->getContainer()->get(Combiner::class);
|
||||
|
||||
try {
|
||||
$importContents = $oCombiner->combineFiles($aUrlArray, 'css');
|
||||
} catch (\Exception $e) {
|
||||
return $matches[0];
|
||||
}
|
||||
$this->atImports = \array_merge($this->atImports, [$importContents['import']]);
|
||||
$this->fontFace = \array_merge($this->fontFace, $importContents['font-face']);
|
||||
$this->gFonts = \array_merge($this->gFonts, $importContents['gfonts']);
|
||||
|
||||
return $importContents['content'];
|
||||
}
|
||||
|
||||
public function setCssInfos($cssInfos): void
|
||||
{
|
||||
$this->cssInfos = $cssInfos;
|
||||
}
|
||||
|
||||
public function getImports(): array
|
||||
{
|
||||
return $this->atImports;
|
||||
}
|
||||
|
||||
public function getGFonts(): array
|
||||
{
|
||||
return $this->gFonts;
|
||||
}
|
||||
|
||||
public function getFontFace(): array
|
||||
{
|
||||
return $this->fontFace;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class CssSearchObject
|
||||
{
|
||||
protected array $aCssRuleCriteria = [];
|
||||
protected array $aCssAtRuleCriteria = [];
|
||||
protected array $aCssNestedRuleNames = [];
|
||||
protected array $aCssCustomRule = [];
|
||||
protected bool $bIsCssCommentSet = \false;
|
||||
|
||||
public function setCssRuleCriteria(string $sCriteria): void
|
||||
{
|
||||
$this->aCssRuleCriteria[] = $sCriteria;
|
||||
}
|
||||
|
||||
public function getCssRuleCriteria(): array
|
||||
{
|
||||
return $this->aCssRuleCriteria;
|
||||
}
|
||||
|
||||
public function setCssAtRuleCriteria(string $sCriteria): void
|
||||
{
|
||||
$this->aCssAtRuleCriteria[] = $sCriteria;
|
||||
}
|
||||
|
||||
public function getCssAtRuleCriteria(): array
|
||||
{
|
||||
return $this->aCssAtRuleCriteria;
|
||||
}
|
||||
|
||||
public function setCssNestedRuleName(string $sNestedRule, bool $bRecurse = \false, bool $bEmpty = \false): void
|
||||
{
|
||||
$this->aCssNestedRuleNames[] = ['name' => $sNestedRule, 'recurse' => $bRecurse, 'empty-value' => $bEmpty];
|
||||
}
|
||||
|
||||
public function getCssNestedRuleNames(): array
|
||||
{
|
||||
return $this->aCssNestedRuleNames;
|
||||
}
|
||||
|
||||
public function setCssCustomRule(string $sCssCustomRule): void
|
||||
{
|
||||
$this->aCssCustomRule[] = $sCssCustomRule;
|
||||
}
|
||||
|
||||
public function getCssCustomRule(): array
|
||||
{
|
||||
return $this->aCssCustomRule;
|
||||
}
|
||||
|
||||
public function setCssComment(): void
|
||||
{
|
||||
$this->bIsCssCommentSet = \true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|string
|
||||
*/
|
||||
public function getCssComment()
|
||||
{
|
||||
if ($this->bIsCssCommentSet) {
|
||||
return \JchOptimize\Core\Css\Parser::blockCommentToken();
|
||||
}
|
||||
|
||||
return \false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css;
|
||||
|
||||
use CodeAlfa\RegexTokenizer\Css;
|
||||
use JchOptimize\Core\Exception;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Parser
|
||||
{
|
||||
use Css;
|
||||
protected array $aExcludes = [];
|
||||
|
||||
/** @var CssSearchObject */
|
||||
protected \JchOptimize\Core\Css\CssSearchObject $oCssSearchObject;
|
||||
protected bool $bBranchReset = \true;
|
||||
protected string $sParseTerm = '\\s*+';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->aExcludes = [
|
||||
self::blockCommentToken(),
|
||||
self::lineCommentToken(),
|
||||
self::cssRuleWithCaptureValueToken(),
|
||||
self::cssAtRulesToken(),
|
||||
self::cssNestedAtRulesWithCaptureValueToken(),
|
||||
// Custom exclude
|
||||
'\\|"(?>[^"{}]*+"?)*?[^"{}]*+"\\|',
|
||||
self::cssInvalidCssToken(),
|
||||
];
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssRuleWithCaptureValueToken(bool $bCaptureValue = \false, string $sCriteria = ''): string
|
||||
{
|
||||
$sCssRule = '<<(?<=^|[{}/\\s;|])[^@/\\s{}]'.self::parseNoStrings().'>>\\{'.$sCriteria.'<<'.self::parse().'>>\\}';
|
||||
|
||||
return self::prepare($sCssRule, $bCaptureValue);
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssAtRulesToken(): string
|
||||
{
|
||||
return '@\\w++\\b\\s++(?:'.self::cssIdentToken().')?(?:'.self::stringWithCaptureValueToken().'|'.self::cssUrlWithCaptureValueToken().')[^;]*+;';
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
/**
|
||||
* @param (mixed|string)[] $aAtRules
|
||||
*
|
||||
* @psalm-param list{0?: 'font-face'|'media'|mixed, 1?: 'keyframes'|mixed, 2?: 'page'|mixed, 3?: 'font-feature-values'|mixed, 4?: 'counter-style'|mixed, 5?: 'viewport'|mixed, 6?: 'property'|mixed,...} $aAtRules
|
||||
*/
|
||||
public static function cssNestedAtRulesWithCaptureValueToken(array $aAtRules = [], bool $bCV = \false, bool $bEmpty = \false): string
|
||||
{
|
||||
$sAtRules = !empty($aAtRules) ? '(?>'.\implode('|', $aAtRules).')' : '';
|
||||
$iN = $bCV ? 2 : 1;
|
||||
$sValue = $bEmpty ? '\\s*+' : '(?>'.self::parse('', \true).'|(?-'.$iN.'))*+';
|
||||
$sAtRules = '<<@(?:-[^-]++-)??'.$sAtRules.'[^{};]*+>>(\\{<<'.$sValue.'>>\\})';
|
||||
|
||||
return self::prepare($sAtRules, $bCV);
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssInvalidCssToken(): string
|
||||
{
|
||||
return '[^;}@\\r\\n]*+[;}@\\r\\n]';
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssAtImportWithCaptureValueToken(bool $bCV = \false): string
|
||||
{
|
||||
$sAtImport = '@import\\s++<<<'.self::stringWithCaptureValueToken($bCV).'|'.self::cssUrlWithCaptureValueToken($bCV).'>>><<[^;]*+>>;';
|
||||
|
||||
return self::prepare($sAtImport, $bCV);
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssAtFontFaceWithCaptureValueToken($sCaptureValue = \false): string
|
||||
{
|
||||
return self::cssNestedAtRulesWithCaptureValueToken(['font-face'], $sCaptureValue);
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssAtMediaWithCaptureValueToken($sCaptureValue = \false): string
|
||||
{
|
||||
return self::cssNestedAtRulesWithCaptureValueToken(['media'], $sCaptureValue);
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssAtCharsetWithCaptureValueToken($sCaptureValue = \false): string
|
||||
{
|
||||
return '@charset\\s++'.self::stringWithCaptureValueToken($sCaptureValue).'[^;]*+;';
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssAtNameSpaceToken(): string
|
||||
{
|
||||
return '@namespace\\s++(?:'.self::cssIdentToken().')?(?:'.self::stringWithCaptureValueToken().'|'.self::cssUrlWithCaptureValueToken().')[^;]*+;';
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssStatementsToken(): string
|
||||
{
|
||||
return '(?:'.self::cssRuleWithCaptureValueToken().'|'.self::cssAtRulesToken().'|'.self::cssNestedAtRulesWithCaptureValueToken().')';
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public static function cssMediaTypesToken(): string
|
||||
{
|
||||
return '(?>all|screen|print|speech|aural|tv|tty|projection|handheld|braille|embossed)';
|
||||
}
|
||||
|
||||
public function disableBranchReset(): void
|
||||
{
|
||||
$this->bBranchReset = \false;
|
||||
}
|
||||
|
||||
public function setExcludesArray($aExcludes): void
|
||||
{
|
||||
$this->aExcludes = $aExcludes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Callbacks\CombineMediaQueries|Callbacks\CorrectUrls|Callbacks\ExtractCriticalCss|Callbacks\FormatCss|Callbacks\HandleAtRules $oCallback
|
||||
*
|
||||
* @throws Exception\PregErrorException
|
||||
*/
|
||||
public function processMatchesWithCallback(string $sCss, $oCallback, string $sContext = 'global'): ?string
|
||||
{
|
||||
$sRegex = $this->getCssSearchRegex();
|
||||
$sProcessedCss = \preg_replace_callback('#'.$sRegex.'#six', function ($aMatches) use ($oCallback, $sContext): string {
|
||||
if (empty(\trim($aMatches[0]))) {
|
||||
return $aMatches[0];
|
||||
}
|
||||
if ('@' == \substr($aMatches[0], 0, 1)) {
|
||||
$sContext = $this->getContext($aMatches[0]);
|
||||
foreach ($this->oCssSearchObject->getCssNestedRuleNames() as $aAtRule) {
|
||||
if ($aAtRule['name'] == $sContext) {
|
||||
if ($aAtRule['recurse']) {
|
||||
return $aMatches[2].'{'.$this->processMatchesWithCallback($aMatches[4], $oCallback, $sContext).'}';
|
||||
}
|
||||
|
||||
return $oCallback->processMatches($aMatches, $sContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $oCallback->processMatches($aMatches, $sContext);
|
||||
}, $sCss);
|
||||
|
||||
try {
|
||||
self::throwExceptionOnPregError();
|
||||
} catch (\Exception $exception) {
|
||||
throw new Exception\PregErrorException($exception->getMessage());
|
||||
}
|
||||
|
||||
return $sProcessedCss;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param '' $sReplace
|
||||
*
|
||||
* @param mixed $sCss
|
||||
*
|
||||
* @throws Exception\PregErrorException
|
||||
*/
|
||||
public function replaceMatches($sCss, string $sReplace): ?string
|
||||
{
|
||||
$sProcessedCss = \preg_replace('#'.$this->getCssSearchRegex().'#i', $sReplace, $sCss);
|
||||
|
||||
try {
|
||||
self::throwExceptionOnPregError();
|
||||
} catch (\Exception $exception) {
|
||||
throw new Exception\PregErrorException($exception->getMessage());
|
||||
}
|
||||
|
||||
return $sProcessedCss;
|
||||
}
|
||||
|
||||
public function setCssSearchObject(CssSearchObject $oCssSearchObject): void
|
||||
{
|
||||
$this->oCssSearchObject = $oCssSearchObject;
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
public function setExcludes(array $aExcludes): void
|
||||
{
|
||||
$this->aExcludes = $aExcludes;
|
||||
}
|
||||
|
||||
public function setParseTerm(string $sParseTerm): void
|
||||
{
|
||||
$this->sParseTerm = $sParseTerm;
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
protected static function parseNoStrings(): string
|
||||
{
|
||||
return '(?>(?:[^{}/]++|/)(?>'.self::blockCommentToken().')?)*?';
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
/**
|
||||
* @psalm-param '' $sInclude
|
||||
*/
|
||||
protected static function parse(string $sInclude = '', bool $bNoEmpty = \false): string
|
||||
{
|
||||
$sRepeat = $bNoEmpty ? '+' : '*';
|
||||
|
||||
return '(?>(?:[^{}"\'/'.$sInclude.']++|/)(?>'.self::blockCommentToken().'|'.self::stringWithCaptureValueToken().')?)'.$sRepeat.'?';
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
protected static function _parseCss($sInclude = '', $bNoEmpty = \false): string
|
||||
{
|
||||
return self::parse($sInclude, $bNoEmpty);
|
||||
}
|
||||
|
||||
protected function getCssSearchRegex(): string
|
||||
{
|
||||
return $this->parseCss($this->getExcludes()).'\\K(?:'.$this->getCriteria().'|$)';
|
||||
}
|
||||
|
||||
protected function parseCSS($aExcludes = []): string
|
||||
{
|
||||
if (!empty($aExcludes)) {
|
||||
$aExcludes = '(?>'.\implode('|', $aExcludes).')?';
|
||||
} else {
|
||||
$aExcludes = '';
|
||||
}
|
||||
|
||||
return '(?>'.$this->sParseTerm.$aExcludes.')*?'.$this->sParseTerm;
|
||||
}
|
||||
|
||||
protected function getExcludes(): array
|
||||
{
|
||||
return $this->aExcludes;
|
||||
}
|
||||
|
||||
protected function getCriteria(): string
|
||||
{
|
||||
$oObj = $this->oCssSearchObject;
|
||||
$aCriteria = [];
|
||||
// We need to add Nested Rules criteria first to avoid trouble with recursion and branch capture reset
|
||||
$aNestedRules = $oObj->getCssNestedRuleNames();
|
||||
if (!empty($aNestedRules)) {
|
||||
if (1 == \count($aNestedRules) && \true == $aNestedRules[0]['empty-value']) {
|
||||
$aCriteria[] = self::cssNestedAtRulesWithCaptureValueToken([$aNestedRules[0]['name']], \false, \true);
|
||||
} elseif (1 == \count($aNestedRules) && '*' == $aNestedRules[0]['name']) {
|
||||
$aCriteria[] = self::cssNestedAtRulesWithCaptureValueToken([]);
|
||||
} else {
|
||||
$aCriteria[] = self::cssNestedAtRulesWithCaptureValueToken(\array_column($aNestedRules, 'name'), \true);
|
||||
}
|
||||
}
|
||||
$aAtRules = $oObj->getCssAtRuleCriteria();
|
||||
if (!empty($aAtRules)) {
|
||||
$aCriteria[] = '('.\implode('|', $aAtRules).')';
|
||||
}
|
||||
$aCssRules = $oObj->getCssRuleCriteria();
|
||||
if (!empty($aCssRules)) {
|
||||
if (1 == \count($aCssRules) && '.' == $aCssRules[0]) {
|
||||
$aCriteria[] = self::cssRuleWithCaptureValueToken(\true);
|
||||
} elseif (1 == \count($aCssRules) && '*' == $aCssRules[0]) {
|
||||
// Array of nested rules we don't want to recurse in
|
||||
$aNestedRules = ['font-face', 'keyframes', 'page', 'font-feature-values', 'counter-style', 'viewport', 'property'];
|
||||
$aCriteria[] = '(?:(?:'.self::cssRuleWithCaptureValueToken().'\\s*+|'.self::blockCommentToken().'\\s*+|'.self::cssNestedAtRulesWithCaptureValueToken($aNestedRules).'\\s*+)++)';
|
||||
} else {
|
||||
$sStr = self::getParseStr($aCssRules);
|
||||
$sRulesCriteria = '(?=(?>['.$sStr.']?[^{}'.$sStr.']*+)*?('.\implode('|', $aCssRules).'))';
|
||||
$aCriteria[] = self::cssRuleWithCaptureValueToken(\true, $sRulesCriteria);
|
||||
}
|
||||
}
|
||||
$aCssCustomRules = $oObj->getCssCustomRule();
|
||||
if (!empty($aCssCustomRules)) {
|
||||
$aCriteria[] = '('.\implode('|', $aCssCustomRules).')';
|
||||
}
|
||||
|
||||
return ($this->bBranchReset ? '(?|' : '(?:').\implode('|', $aCriteria).')';
|
||||
}
|
||||
|
||||
// language=RegExp
|
||||
protected static function getParseStr(array $aExcludes): string
|
||||
{
|
||||
$aStr = [];
|
||||
foreach ($aExcludes as $sExclude) {
|
||||
$sSubStr = \substr($sExclude, 0, 1);
|
||||
if (!\in_array($sSubStr, $aStr)) {
|
||||
$aStr[] = $sSubStr;
|
||||
}
|
||||
}
|
||||
|
||||
return \implode('', $aStr);
|
||||
}
|
||||
|
||||
protected function getContext(string $sMatch): string
|
||||
{
|
||||
\preg_match('#^@(?:-[^-]+-)?([^\\s{(]++)#i', $sMatch, $aMatches);
|
||||
|
||||
return !empty($aMatches[1]) ? \strtolower($aMatches[1]) : 'global';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use CodeAlfa\RegexTokenizer\Debug\Debug;
|
||||
use JchOptimize\Core\Css\Callbacks\CombineMediaQueries;
|
||||
use JchOptimize\Core\Css\Callbacks\CorrectUrls;
|
||||
use JchOptimize\Core\Css\Callbacks\ExtractCriticalCss;
|
||||
use JchOptimize\Core\Css\Callbacks\FormatCss;
|
||||
use JchOptimize\Core\Css\Callbacks\HandleAtRules;
|
||||
use JchOptimize\Core\Exception;
|
||||
use JchOptimize\Core\FileInfosUtilsTrait;
|
||||
use JchOptimize\Core\FileUtils;
|
||||
use JchOptimize\Core\Html\Processor as HtmlProcessor;
|
||||
use JchOptimize\Core\SerializableTrait;
|
||||
use JchOptimize\Platform\Profiler;
|
||||
use Joomla\Registry\Registry;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Processor implements LoggerAwareInterface, ContainerAwareInterface, \Serializable
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
use LoggerAwareTrait;
|
||||
use Debug;
|
||||
use FileInfosUtilsTrait;
|
||||
use SerializableTrait;
|
||||
protected string $css;
|
||||
|
||||
private Registry $params;
|
||||
|
||||
private string $debugUrl = '';
|
||||
|
||||
private CombineMediaQueries $combineMediaQueries;
|
||||
|
||||
private CorrectUrls $correctUrls;
|
||||
|
||||
private ExtractCriticalCss $extractCriticalCss;
|
||||
|
||||
private FormatCss $formatCss;
|
||||
|
||||
private HandleAtRules $handleAtRules;
|
||||
|
||||
public function __construct(Registry $params, CombineMediaQueries $combineMediaQueries, CorrectUrls $correctUrls, ExtractCriticalCss $extractCriticalCss, FormatCss $formatCss, HandleAtRules $handleAtRules)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->combineMediaQueries = $combineMediaQueries;
|
||||
$this->correctUrls = $correctUrls;
|
||||
$this->extractCriticalCss = $extractCriticalCss;
|
||||
$this->formatCss = $formatCss;
|
||||
$this->handleAtRules = $handleAtRules;
|
||||
}
|
||||
|
||||
public function setCssInfos(array $cssInfos): void
|
||||
{
|
||||
$this->combineMediaQueries->setCssInfos($cssInfos);
|
||||
$this->correctUrls->setCssInfos($cssInfos);
|
||||
$this->handleAtRules->setCssInfos($cssInfos);
|
||||
$this->fileUtils = $this->container->get(FileUtils::class);
|
||||
$this->debugUrl = $this->prepareFileUrl($cssInfos, 'css');
|
||||
// initialize debug
|
||||
$this->_debug($this->debugUrl, '', 'CssProcessorConstructor');
|
||||
}
|
||||
|
||||
public function getCss(): string
|
||||
{
|
||||
return $this->css;
|
||||
}
|
||||
|
||||
public function setCss(string $css): void
|
||||
{
|
||||
if (\function_exists('mb_convert_encoding')) {
|
||||
$sEncoding = \mb_detect_encoding($css);
|
||||
if (\false === $sEncoding) {
|
||||
$sEncoding = \mb_internal_encoding();
|
||||
}
|
||||
$css = \mb_convert_encoding($css, 'utf-8', $sEncoding);
|
||||
}
|
||||
$this->css = $css;
|
||||
}
|
||||
|
||||
public function formatCss(): void
|
||||
{
|
||||
$oParser = new \JchOptimize\Core\Css\Parser();
|
||||
$oParser->setExcludes([\JchOptimize\Core\Css\Parser::blockCommentToken(), \JchOptimize\Core\Css\Parser::lineCommentToken(), \JchOptimize\Core\Css\Parser::cssNestedAtRulesWithCaptureValueToken()]);
|
||||
$sPrepareExcludeRegex = '\\|"(?>[^"{}]*+"?)*?[^"{}]*+"\\|';
|
||||
$oSearchObject = new \JchOptimize\Core\Css\CssSearchObject();
|
||||
$oSearchObject->setCssNestedRuleName('media', \true);
|
||||
$oSearchObject->setCssNestedRuleName('supports', \true);
|
||||
$oSearchObject->setCssNestedRuleName('document', \true);
|
||||
$oSearchObject->setCssAtRuleCriteria(\JchOptimize\Core\Css\Parser::cssAtRulesToken());
|
||||
$oSearchObject->setCssRuleCriteria('*');
|
||||
$oSearchObject->setCssCustomRule($sPrepareExcludeRegex);
|
||||
$oSearchObject->setCssCustomRule(\JchOptimize\Core\Css\Parser::cssInvalidCssToken());
|
||||
$oParser->setCssSearchObject($oSearchObject);
|
||||
$oParser->disableBranchReset();
|
||||
$this->formatCss->validCssRules = $sPrepareExcludeRegex;
|
||||
|
||||
try {
|
||||
$this->css = $oParser->processMatchesWithCallback($this->css.'}', $this->formatCss);
|
||||
} catch (Exception\PregErrorException $oException) {
|
||||
$this->logger->error('FormatCss failed - '.$this->debugUrl.': '.$oException->getMessage());
|
||||
}
|
||||
$this->_debug($this->debugUrl, '', 'formatCss');
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload resources in CSS.
|
||||
*
|
||||
* @param string $css Css to process
|
||||
* @param bool $isFontsOnly If Optimize CSS not enabled then lets just preload only fonts
|
||||
*/
|
||||
public function preloadHttp2(string $css, bool $isFontsOnly = \false)
|
||||
{
|
||||
$this->css = $css;
|
||||
$this->processUrls(\true, $isFontsOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path to the combined CSS files differs from the original path so relative paths to images in the files are
|
||||
* converted to absolute paths. This method is used again to preload assets found in the Critical CSS after Optimize
|
||||
* CSS Delivery is performed.
|
||||
*
|
||||
* @param bool $isHttp2 Indicates if we're doing the run to preload assets
|
||||
* @param bool $isFontsOnly If Optimize CSS Delivery disabled, only preload fonts
|
||||
* @param bool $isBackend True if this is done from admin to populate the drop-down lists
|
||||
*/
|
||||
public function processUrls(bool $isHttp2 = \false, bool $isFontsOnly = \false, bool $isBackend = \false)
|
||||
{
|
||||
$oParser = new \JchOptimize\Core\Css\Parser();
|
||||
$oSearchObject = new \JchOptimize\Core\Css\CssSearchObject();
|
||||
$oSearchObject->setCssNestedRuleName('font-face');
|
||||
$oSearchObject->setCssNestedRuleName('keyframes');
|
||||
$oSearchObject->setCssNestedRuleName('media', \true);
|
||||
$oSearchObject->setCssNestedRuleName('supports', \true);
|
||||
$oSearchObject->setCssNestedRuleName('document', \true);
|
||||
$oSearchObject->setCssRuleCriteria(\JchOptimize\Core\Css\Parser::cssUrlWithCaptureValueToken());
|
||||
$oSearchObject->setCssAtRuleCriteria(\JchOptimize\Core\Css\Parser::cssAtImportWithCaptureValueToken());
|
||||
$oParser->setCssSearchObject($oSearchObject);
|
||||
$this->correctUrls->isHttp2 = $isHttp2;
|
||||
$this->correctUrls->isFontsOnly = $isFontsOnly;
|
||||
$this->correctUrls->isBackend = $isBackend;
|
||||
|
||||
try {
|
||||
$this->css = $oParser->processMatchesWithCallback($this->css, $this->correctUrls);
|
||||
} catch (Exception\PregErrorException $oException) {
|
||||
$sPreMessage = $isHttp2 ? 'Http/2 preload failed' : 'ProcessUrls failed';
|
||||
$this->logger->error($sPreMessage.' - '.$this->debugUrl.': '.$oException->getMessage());
|
||||
}
|
||||
$this->_debug($this->debugUrl, '', 'processUrls');
|
||||
}
|
||||
|
||||
public function processAtRules(): void
|
||||
{
|
||||
$oParser = new \JchOptimize\Core\Css\Parser();
|
||||
$oSearchObject = new \JchOptimize\Core\Css\CssSearchObject();
|
||||
$oSearchObject->setCssAtRuleCriteria(\JchOptimize\Core\Css\Parser::cssAtImportWithCaptureValueToken(\true));
|
||||
$oSearchObject->setCssAtRuleCriteria(\JchOptimize\Core\Css\Parser::cssAtCharsetWithCaptureValueToken());
|
||||
$oSearchObject->setCssNestedRuleName('font-face');
|
||||
$oSearchObject->setCssNestedRuleName('media', \true);
|
||||
$oParser->setCssSearchObject($oSearchObject);
|
||||
|
||||
try {
|
||||
$this->css = $this->cleanEmptyMedias($oParser->processMatchesWithCallback($this->css, $this->handleAtRules));
|
||||
} catch (Exception\PregErrorException $oException) {
|
||||
$this->logger->error('ProcessAtRules failed - '.$this->debugUrl.': '.$oException->getMessage());
|
||||
}
|
||||
$this->_debug($this->debugUrl, '', 'ProcessAtRules');
|
||||
}
|
||||
|
||||
public function cleanEmptyMedias($css)
|
||||
{
|
||||
$oParser = new \JchOptimize\Core\Css\Parser();
|
||||
$oParser->setExcludes([\JchOptimize\Core\Css\Parser::blockCommentToken(), '[@/]']);
|
||||
$oParser->setParseTerm('[^@/]*+');
|
||||
$oCssEmptyMediaObject = new \JchOptimize\Core\Css\CssSearchObject();
|
||||
$oCssEmptyMediaObject->setCssNestedRuleName('media', \false, \true);
|
||||
$oParser->setCssSearchObject($oCssEmptyMediaObject);
|
||||
|
||||
return $oParser->replaceMatches($css, '');
|
||||
}
|
||||
|
||||
public function processMediaQueries(): void
|
||||
{
|
||||
$oParser = new \JchOptimize\Core\Css\Parser();
|
||||
$oSearchObject = new \JchOptimize\Core\Css\CssSearchObject();
|
||||
$oSearchObject->setCssNestedRuleName('media');
|
||||
$oSearchObject->setCssAtRuleCriteria(\JchOptimize\Core\Css\Parser::cssAtImportWithCaptureValueToken(\true));
|
||||
$oSearchObject->setCssRuleCriteria('*');
|
||||
$oParser->setCssSearchObject($oSearchObject);
|
||||
$oParser->disableBranchReset();
|
||||
|
||||
try {
|
||||
$this->css = $oParser->processMatchesWithCallback($this->css, $this->combineMediaQueries);
|
||||
} catch (Exception\PregErrorException $oException) {
|
||||
$this->logger->error('HandleMediaQueries failed - '.$this->debugUrl.': '.$oException->getMessage());
|
||||
}
|
||||
$this->_debug($this->debugUrl, '', 'handleMediaQueries');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $css
|
||||
* @param mixed $html
|
||||
*
|
||||
* @throws Exception\PregErrorException
|
||||
*/
|
||||
public function optimizeCssDelivery($css, $html): string
|
||||
{
|
||||
!JCH_DEBUG ?: Profiler::start('OptimizeCssDelivery');
|
||||
$this->_debug('', '', 'StartCssDelivery');
|
||||
// We can't use the $html coming in as argument as that was used to generate cache key. Let's get the
|
||||
// HTML from the HTML processor
|
||||
/** @var HtmlProcessor $htmlProcessor */
|
||||
$htmlProcessor = $this->container->get(HtmlProcessor::class);
|
||||
$html = $htmlProcessor->cleanHtml();
|
||||
// Place space around HTML attributes for easy processing with XPath
|
||||
$html = \preg_replace('#\\s*=\\s*(?|"([^"]*+)"|\'([^\']*+)\'|([^\\s/>]*+))#i', '=" $1 "', $html);
|
||||
// Truncate HTML to number of elements set in params
|
||||
$sHtmlAboveFold = '';
|
||||
\preg_replace_callback('#<[a-z0-9]++[^>]*+>(?><?[^<]*+(<ul\\b[^>]*+>(?>[^<]*+<(?!ul)[^<]*+|(?1))*?</ul>)?)*?(?=<[a-z0-9])#i', function ($aM) use (&$sHtmlAboveFold) {
|
||||
$sHtmlAboveFold .= $aM[0];
|
||||
|
||||
return $aM[0];
|
||||
}, $html, (int) $this->params->get('optimizeCssDelivery', '800'));
|
||||
$this->_debug('', '', 'afterHtmlTruncated');
|
||||
$oDom = new \DOMDocument();
|
||||
// Load HTML in DOM
|
||||
\libxml_use_internal_errors(\true);
|
||||
$oDom->loadHtml($sHtmlAboveFold);
|
||||
\libxml_clear_errors();
|
||||
$oXPath = new \DOMXPath($oDom);
|
||||
$this->_debug('', '', 'afterLoadHtmlDom');
|
||||
$sFullHtml = $html;
|
||||
$oParser = new \JchOptimize\Core\Css\Parser();
|
||||
$oCssSearchObject = new \JchOptimize\Core\Css\CssSearchObject();
|
||||
$oCssSearchObject->setCssNestedRuleName('media', \true);
|
||||
$oCssSearchObject->setCssNestedRuleName('supports', \true);
|
||||
$oCssSearchObject->setCssNestedRuleName('document', \true);
|
||||
$oCssSearchObject->setCssNestedRuleName('font-face');
|
||||
$oCssSearchObject->setCssNestedRuleName('keyframes');
|
||||
$oCssSearchObject->setCssNestedRuleName('page');
|
||||
$oCssSearchObject->setCssNestedRuleName('font-feature-values');
|
||||
$oCssSearchObject->setCssNestedRuleName('counter-style');
|
||||
$oCssSearchObject->setCssAtRuleCriteria(\JchOptimize\Core\Css\Parser::cssAtImportWithCaptureValueToken());
|
||||
$oCssSearchObject->setCssAtRuleCriteria(\JchOptimize\Core\Css\Parser::cssAtCharsetWithCaptureValueToken());
|
||||
$oCssSearchObject->setCssAtRuleCriteria(\JchOptimize\Core\Css\Parser::cssAtNameSpaceToken());
|
||||
$oCssSearchObject->setCssRuleCriteria('.');
|
||||
$this->extractCriticalCss->sHtmlAboveFold = $sHtmlAboveFold;
|
||||
$this->extractCriticalCss->sFullHtml = $sFullHtml;
|
||||
$this->extractCriticalCss->oXPath = $oXPath;
|
||||
$oParser->setCssSearchObject($oCssSearchObject);
|
||||
$sCriticalCss = $oParser->processMatchesWithCallback($css, $this->extractCriticalCss);
|
||||
$sCriticalCss = $this->cleanEmptyMedias($sCriticalCss);
|
||||
// Process Font-Face and Key frames
|
||||
$this->extractCriticalCss->isPostProcessing = \true;
|
||||
$preCss = $this->extractCriticalCss->preCss;
|
||||
$sPostCss = $oParser->processMatchesWithCallback($this->extractCriticalCss->postCss, $this->extractCriticalCss);
|
||||
!JCH_DEBUG ?: Profiler::stop('OptimizeCssDelivery', \true);
|
||||
|
||||
return $preCss.$sCriticalCss.$sPostCss;
|
||||
// $this->_debug(self::cssRulesRegex(), '', 'afterCleanCriticalCss');
|
||||
}
|
||||
|
||||
public function getImports(): string
|
||||
{
|
||||
return \implode($this->handleAtRules->getImports());
|
||||
}
|
||||
|
||||
public function getImages(): array
|
||||
{
|
||||
return $this->correctUrls->getImages();
|
||||
}
|
||||
|
||||
public function getFontFace(): array
|
||||
{
|
||||
return $this->handleAtRules->getFontFace();
|
||||
}
|
||||
|
||||
public function getGFonts(): array
|
||||
{
|
||||
return $this->handleAtRules->getGFonts();
|
||||
}
|
||||
|
||||
public function getPreconnects(): array
|
||||
{
|
||||
return $this->correctUrls->getPreconnects();
|
||||
}
|
||||
|
||||
public function getCssBgImagesSelectors(): array
|
||||
{
|
||||
return $this->correctUrls->getCssBgImagesSelectors();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Sprite;
|
||||
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\UriResolver;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use Exception;
|
||||
use JchOptimize\Core\Exception\MissingDependencyException;
|
||||
use JchOptimize\Core\SystemUri;
|
||||
use JchOptimize\Core\Uri\Utils;
|
||||
use JchOptimize\Platform\Paths;
|
||||
use JchOptimize\Platform\Profiler;
|
||||
use Joomla\Filesystem\Folder;
|
||||
use Joomla\Registry\Registry;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
use function count;
|
||||
use function md5;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Controller implements LoggerAwareInterface, ContainerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
use LoggerAwareTrait;
|
||||
public array $options = [];
|
||||
public bool $bTransparent;
|
||||
protected array $imageTypes = [];
|
||||
protected array $aFormErrors = [];
|
||||
protected string $sZipFolder = '';
|
||||
protected $sCss;
|
||||
protected string $sTempSpriteName = '';
|
||||
protected bool $bValidImages;
|
||||
protected array $aBackground = [];
|
||||
protected array $aPosition = [];
|
||||
|
||||
/**
|
||||
* @var HandlerInterface
|
||||
*/
|
||||
protected \JchOptimize\Core\Css\Sprite\HandlerInterface $imageHandler;
|
||||
protected Registry $params;
|
||||
|
||||
/**
|
||||
* @var array To store CSS rules
|
||||
*/
|
||||
private array $aCss = [];
|
||||
|
||||
/**
|
||||
* Controller constructor.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(Registry $params, LoggerInterface $logger)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->setLogger($logger);
|
||||
$this->options = [
|
||||
'path' => '',
|
||||
'sub' => '',
|
||||
'file-regex' => '',
|
||||
'wrap-columns' => $this->params->get('csg_wrap_images', 'off'),
|
||||
'build-direction' => $this->params->get('csg_direction', 'vertical'),
|
||||
'use-transparency' => 'on',
|
||||
'use-optipng' => '',
|
||||
'vertical-offset' => 50,
|
||||
'horizontal-offset' => 50,
|
||||
'background' => '',
|
||||
'image-output' => 'PNG',
|
||||
// $this->params->get('csg_file_output'),
|
||||
'image-num-colours' => 'true-colour',
|
||||
'image-quality' => 100,
|
||||
'width-resize' => 100,
|
||||
'height-resize' => 100,
|
||||
'ignore-duplicates' => 'merge',
|
||||
'class-prefix' => '',
|
||||
'selector-prefix' => '',
|
||||
'selector-suffix' => '',
|
||||
'add-width-height-to-css' => 'off',
|
||||
'sprite-path' => Paths::spritePath(),
|
||||
];
|
||||
// Should the sprite be transparent
|
||||
$this->options['is-transparent'] = \in_array($this->options['image-output'], ['GIF', 'PNG']);
|
||||
$imageLibrary = $this->getImageLibrary();
|
||||
$class = 'JchOptimize\\Core\\Css\\Sprite\\Handler\\'.\ucfirst($imageLibrary);
|
||||
// @var HandlerInterface&LoggerAwareInterface $class imageHandler
|
||||
$this->imageHandler = new $class($this->params, $this->options);
|
||||
$this->imageHandler->setLogger($logger);
|
||||
$this->imageTypes = $this->imageHandler->getSupportedFormats();
|
||||
}
|
||||
|
||||
public function GetImageTypes(): array
|
||||
{
|
||||
return $this->imageTypes;
|
||||
}
|
||||
|
||||
public function GetSpriteFormats()
|
||||
{
|
||||
return $this->imageHandler->spriteFormats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $aFilePaths
|
||||
*
|
||||
* @psalm-param array<int<0, max>, string> $aFilePaths
|
||||
*
|
||||
* @return null|array
|
||||
*
|
||||
* @psalm-return list<mixed>|null
|
||||
*/
|
||||
public function CreateSprite(array $aFilePaths, bool $returnValues = \false)
|
||||
{
|
||||
// set up variable defaults used when calculating offsets etc
|
||||
$aFilesInfo = [];
|
||||
$aFilesMD5 = [];
|
||||
$bResize = \false;
|
||||
$aValidImages = [];
|
||||
// $this->aFormValues['build-direction'] = 'horizontal'
|
||||
$iRowCount = 1;
|
||||
$aMaxRowHeight = [];
|
||||
$iMaxVOffset = 0;
|
||||
// $this->aFormValues['build-direction'] = 'vertical'
|
||||
$iColumnCount = 1;
|
||||
$aMaxColumnWidth = [];
|
||||
$iMaxHOffset = 0;
|
||||
$iTotalWidth = 0;
|
||||
$iTotalHeight = 0;
|
||||
$iMaxWidth = 0;
|
||||
$iMaxHeight = 0;
|
||||
$i = 0;
|
||||
$k = 0;
|
||||
$bValidImages = \false;
|
||||
$sOutputFormat = \strtolower($this->options['image-output']);
|
||||
$optimize = \false;
|
||||
|
||||
// this section calculates all offsets etc
|
||||
|
||||
foreach ($aFilePaths as $sFile) {
|
||||
JCH_DEBUG ? Profiler::start('CalculateSprite') : null;
|
||||
$fileUri = Utils::uriFor($sFile);
|
||||
$fileUri = $fileUri->withScheme('')->withHost('');
|
||||
$fileUri = UriResolver::resolve(SystemUri::currentUri(), $fileUri);
|
||||
$filePath = \str_replace(SystemUri::baseFull(), '', (string) $fileUri);
|
||||
$sFilePath = Paths::rootPath().\DIRECTORY_SEPARATOR.$filePath;
|
||||
$bFileExists = \true;
|
||||
if (@\file_exists($sFilePath)) {
|
||||
// do we want to scale down the source images
|
||||
// scaling up isn't supported as that would result in poorer quality images
|
||||
$bResize = 100 != $this->options['width-resize'] && 100 != $this->options['height-resize'];
|
||||
// grab path information
|
||||
// $sFilePath = $sFolderMD5.$sFile;
|
||||
$aPathParts = \pathinfo($sFilePath);
|
||||
$sFileBaseName = $aPathParts['basename'];
|
||||
$aImageInfo = @\getimagesize($sFilePath);
|
||||
if ($aImageInfo) {
|
||||
$iWidth = $aImageInfo[0];
|
||||
$iHeight = $aImageInfo[1];
|
||||
$iImageType = $aImageInfo[2];
|
||||
// are we matching filenames against a regular expression
|
||||
// if so it's likely not all images from the ZIP file will end up in the generated sprite image
|
||||
if (!empty($this->options['file-regex'])) {
|
||||
// forward slashes should be escaped - it's likely not doing this might be a security risk also
|
||||
// one might be able to break out and change the modifiers (to for example run PHP code)
|
||||
$this->options['file-regex'] = \str_replace('/', '\\/', $this->options['file-regex']);
|
||||
// if the regular expression matches grab the first match and store for use as the class name
|
||||
if (\preg_match('/^'.$this->options['file-regex'].'$/i', $sFileBaseName, $aMatches)) {
|
||||
$sFileClass = $aMatches[1];
|
||||
} else {
|
||||
$sFileClass = '';
|
||||
}
|
||||
} else {
|
||||
// not using regular expressions - set the class name to the base part of the filename (excluding extension)
|
||||
$sFileClass = $aPathParts['basename'];
|
||||
}
|
||||
// format the class name - it should only contain certain characters
|
||||
// this strips out any which aren't
|
||||
$sFileClass = $this->FormatClassName($sFileClass);
|
||||
} else {
|
||||
$bFileExists = \false;
|
||||
}
|
||||
} else {
|
||||
$bFileExists = \false;
|
||||
}
|
||||
// the file also isn't valid if its extension doesn't match one of the image formats supported by the tool
|
||||
// discard images whose height or width is greater than 50px
|
||||
if ($bFileExists && !empty($sFileClass) && \in_array(\strtoupper($aPathParts['extension']), $this->imageTypes) && \in_array($iImageType, [\IMAGETYPE_GIF, \IMAGETYPE_JPEG, \IMAGETYPE_PNG]) && '.' != \substr($sFileBaseName, 0, 1) && $iWidth < 50 && $iHeight < 50 && $iWidth > 0 && $iHeight > 0) {
|
||||
// grab the file extension
|
||||
$sExtension = $aPathParts['extension'];
|
||||
// get MD5 of file (this can be used to compare if a file's content is exactly the same as another's)
|
||||
$sFileMD5 = \md5(\file_get_contents($sFilePath));
|
||||
// check if this file's MD5 already exists in array of MD5s recorded so far
|
||||
// if so it's a duplicate of another file in the ZIP
|
||||
if (($sKey = \array_search($sFileMD5, $aFilesMD5)) !== \false) {
|
||||
// do we want to drop duplicate files and merge CSS rules
|
||||
// if so CSS will end up like .filename1, .filename2 { }
|
||||
if ('merge' == $this->options['ignore-duplicates']) {
|
||||
if (isset($aFilesInfo[$sKey]['class'])) {
|
||||
$aFilesInfo[$sKey]['class'] = $aFilesInfo[$sKey]['class'].$this->options['selector-suffix'].', '.$this->options['selector-prefix'].'.'.$this->options['class-prefix'].$sFileClass;
|
||||
}
|
||||
$this->aBackground[$k] = $sKey;
|
||||
++$k;
|
||||
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$this->aBackground[$k] = $i;
|
||||
++$k;
|
||||
}
|
||||
// add MD5 to array to check future files against
|
||||
$aFilesMD5[$i] = $sFileMD5;
|
||||
// store generated class selector details
|
||||
// $aFilesInfo[$i]['class'] = ".{$this->aFormValues['class-prefix']}$sFileClass";
|
||||
// store file path information and extension
|
||||
$aFilesInfo[$i]['path'] = $sFilePath;
|
||||
$aFilesInfo[$i]['ext'] = $sExtension;
|
||||
if ('horizontal' == $this->options['build-direction']) {
|
||||
// get the current width of the sprite image - after images processed so far
|
||||
$iCurrentWidth = $iTotalWidth + $this->options['horizontal-offset'] + $iWidth;
|
||||
// store the maximum width reached so far
|
||||
// if we're on a new column current height might be less than the maximum
|
||||
if ($iMaxWidth < $iCurrentWidth) {
|
||||
$iMaxWidth = $iCurrentWidth;
|
||||
}
|
||||
} else {
|
||||
// get the current height of the sprite image - after images processed so far
|
||||
$iCurrentHeight = $iTotalHeight + $this->options['vertical-offset'] + $iHeight;
|
||||
// store the maximum height reached so far
|
||||
// if we're on a new column current height might be less than the maximum
|
||||
if ($iMaxHeight < $iCurrentHeight) {
|
||||
$iMaxHeight = $iCurrentHeight;
|
||||
}
|
||||
}
|
||||
// store the original width and height of the image
|
||||
// we'll need this later if the image is to be resized
|
||||
$aFilesInfo[$i]['original-width'] = $iWidth;
|
||||
$aFilesInfo[$i]['original-height'] = $iHeight;
|
||||
// store the width and height of the image
|
||||
// if we're resizing they'll be less than the original
|
||||
$aFilesInfo[$i]['width'] = $bResize ? \round($iWidth / 100 * $this->options['width-resize']) : $iWidth;
|
||||
$aFilesInfo[$i]['height'] = $bResize ? \round($iHeight / 100 * $this->options['height-resize']) : $iHeight;
|
||||
if ('horizontal' == $this->options['build-direction']) {
|
||||
// opera (9.0 and below) has a bug which prevents it recognising offsets of less than -2042px
|
||||
// all subsequent values are treated as -2042px
|
||||
// if we've hit 2000 pixels and we care about this (as set in the interface) then wrap to a new row
|
||||
// increment row count and reset current height
|
||||
if ($iTotalWidth + $this->options['horizontal-offset'] >= 2000 && !empty($this->options['wrap-columns'])) {
|
||||
++$iRowCount;
|
||||
$iTotalWidth = 0;
|
||||
}
|
||||
// if the current image is higher than any other in the current row then set the maximum height to that
|
||||
// it will be used to set the height of the current row
|
||||
if ($aFilesInfo[$i]['height'] > $iMaxHeight) {
|
||||
$iMaxHeight = $aFilesInfo[$i]['height'];
|
||||
}
|
||||
// keep track of the height of rows added so far
|
||||
$aMaxRowHeight[$iRowCount] = $iMaxHeight;
|
||||
// calculate the current maximum vertical offset so far
|
||||
$iMaxVOffset = $this->options['vertical-offset'] * ($iRowCount - 1);
|
||||
// get the x position of current image in overall sprite
|
||||
$aFilesInfo[$i]['x'] = $iTotalWidth;
|
||||
$iTotalWidth += $aFilesInfo[$i]['width'] + $this->options['horizontal-offset'];
|
||||
// get the y position of current image in overall sprite
|
||||
if (1 == $iRowCount) {
|
||||
$aFilesInfo[$i]['y'] = 0;
|
||||
} else {
|
||||
$aFilesInfo[$i]['y'] = $this->options['vertical-offset'] * ($iRowCount - 1) + (\array_sum($aMaxRowHeight) - $aMaxRowHeight[$iRowCount]);
|
||||
}
|
||||
$aFilesInfo[$i]['currentCombinedWidth'] = $iTotalWidth;
|
||||
$aFilesInfo[$i]['rowNumber'] = $iRowCount;
|
||||
} else {
|
||||
if ($iTotalHeight + $this->options['vertical-offset'] >= 2000 && !empty($this->options['wrap-columns'])) {
|
||||
++$iColumnCount;
|
||||
$iTotalHeight = 0;
|
||||
}
|
||||
// if the current image is wider than any other in the current column then set the maximum width to that
|
||||
// it will be used to set the width of the current column
|
||||
if ($aFilesInfo[$i]['width'] > $iMaxWidth) {
|
||||
$iMaxWidth = $aFilesInfo[$i]['width'];
|
||||
}
|
||||
// keep track of the width of columns added so far
|
||||
$aMaxColumnWidth[$iColumnCount] = $iMaxWidth;
|
||||
// calculate the current maximum horizontal offset so far
|
||||
$iMaxHOffset = $this->options['horizontal-offset'] * ($iColumnCount - 1);
|
||||
// get the y position of current image in overall sprite
|
||||
$aFilesInfo[$i]['y'] = $iTotalHeight;
|
||||
$iTotalHeight += $aFilesInfo[$i]['height'] + $this->options['vertical-offset'];
|
||||
// get the x position of current image in overall sprite
|
||||
if (1 == $iColumnCount) {
|
||||
$aFilesInfo[$i]['x'] = 0;
|
||||
} else {
|
||||
$aFilesInfo[$i]['x'] = $this->options['horizontal-offset'] * ($iColumnCount - 1) + (\array_sum($aMaxColumnWidth) - $aMaxColumnWidth[$iColumnCount]);
|
||||
}
|
||||
$aFilesInfo[$i]['currentCombinedHeight'] = $iTotalHeight;
|
||||
$aFilesInfo[$i]['columnNumber'] = $iColumnCount;
|
||||
}
|
||||
++$i;
|
||||
$aValidImages[] = $sFile;
|
||||
} else {
|
||||
$this->aBackground[$k] = null;
|
||||
++$k;
|
||||
}
|
||||
if ($i > 30) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
JCH_DEBUG ? Profiler::stop('CalculateSprite', \true) : null;
|
||||
if ($returnValues) {
|
||||
return $aValidImages;
|
||||
}
|
||||
JCH_DEBUG ? Profiler::start('CreateSprite') : null;
|
||||
|
||||
// this section generates the sprite image
|
||||
// and CSS rules
|
||||
|
||||
// if $i is greater than 1 then we managed to generate enough info to create a sprite
|
||||
if (\count($aFilesInfo) > 1) {
|
||||
// if Imagick throws an exception we want the script to terminate cleanly so that
|
||||
// temporary files are cleaned up
|
||||
try {
|
||||
// get the sprite width and height
|
||||
if ('horizontal' == $this->options['build-direction']) {
|
||||
$iSpriteWidth = $iMaxWidth - $this->options['horizontal-offset'];
|
||||
$iSpriteHeight = \array_sum($aMaxRowHeight) + $iMaxVOffset;
|
||||
} else {
|
||||
$iSpriteHeight = $iMaxHeight - $this->options['vertical-offset'];
|
||||
$iSpriteWidth = \array_sum($aMaxColumnWidth) + $iMaxHOffset;
|
||||
}
|
||||
// get background colour - remove # if added
|
||||
$sBgColour = \str_replace('#', '', $this->options['background']);
|
||||
// convert 3 digit hex values to 6 digit equivalent
|
||||
if (3 == \strlen($sBgColour)) {
|
||||
$sBgColour = \substr($sBgColour, 0, 1).\substr($sBgColour, 0, 1).\substr($sBgColour, 1, 1).\substr($sBgColour, 1, 1).\substr($sBgColour, 2, 1).\substr($sBgColour, 2, 1);
|
||||
}
|
||||
// should the image be transparent
|
||||
$this->bTransparent = !empty($this->options['use-transparency']) && \in_array($this->options['image-output'], ['GIF', 'PNG']);
|
||||
$oSprite = $this->imageHandler->createSprite($iSpriteWidth, $iSpriteHeight, $sBgColour, $sOutputFormat);
|
||||
// loop through file info for valid images
|
||||
for ($i = 0; $i < \count($aFilesInfo); ++$i) {
|
||||
// create a new image object for current file
|
||||
if (!($oCurrentImage = $this->imageHandler->createImage($aFilesInfo[$i]))) {
|
||||
// if we've got here then a valid but corrupt image was found
|
||||
// at this stage we've already allocated space for the image so create
|
||||
// a blank one to fill the space instead
|
||||
// this should happen very rarely
|
||||
$oCurrentImage = $this->imageHandler->createBlankImage($aFilesInfo[$i]);
|
||||
}
|
||||
// if resizing get image width and height and resample to new dimensions (percentage of original)
|
||||
// and copy to sprite image
|
||||
if ($bResize) {
|
||||
$this->imageHandler->resizeImage($oSprite, $oCurrentImage, $aFilesInfo[$i]);
|
||||
}
|
||||
// copy image to sprite
|
||||
$this->imageHandler->copyImageToSprite($oSprite, $oCurrentImage, $aFilesInfo[$i], $bResize);
|
||||
// get CSS x & y values
|
||||
$iX = 0 != $aFilesInfo[$i]['x'] ? '-'.$aFilesInfo[$i]['x'].'px' : '0';
|
||||
$iY = 0 != $aFilesInfo[$i]['y'] ? '-'.$aFilesInfo[$i]['y'].'px' : '0';
|
||||
$this->aPosition[$i] = $iX.' '.$iY;
|
||||
// create CSS rules and append to overall CSS rules
|
||||
// $this->sCss .= "{$this->aFormValues['selector-prefix']}{$aFilesInfo[$i]['class']} "
|
||||
// . "{$this->aFormValues['selector-suffix']}{ background-position: $iX $iY; ";
|
||||
//
|
||||
// // If add widths and heights the sprite image width and height are added to the CSS
|
||||
// if ($this->aFormValues['add-width-height-to-css'] == 'on')
|
||||
// {
|
||||
// $this->sCss .= "width: {$aFilesInfo[$i]['width']}px; height: {$aFilesInfo[$i]['height']}px;";
|
||||
// }
|
||||
//
|
||||
// $this->sCss .= " } \n";
|
||||
// destroy object created for current image to save memory
|
||||
$this->imageHandler->destroy($oCurrentImage);
|
||||
}
|
||||
$path = $this->options['sprite-path'];
|
||||
// See if image already exists
|
||||
//
|
||||
// create a unqiue filename for sprite image
|
||||
$sSpriteMD5 = \md5(\implode($aFilesMD5).\implode($this->options));
|
||||
$this->sTempSpriteName = $path.\DIRECTORY_SEPARATOR.'csg-'.$sSpriteMD5.".{$sOutputFormat}";
|
||||
if (!\file_exists($path)) {
|
||||
Folder::create($path);
|
||||
}
|
||||
// write image to file
|
||||
if (!\file_exists($this->sTempSpriteName)) {
|
||||
$this->imageHandler->writeImage($oSprite, $sOutputFormat, $this->sTempSpriteName);
|
||||
$optimize = \true;
|
||||
}
|
||||
// destroy object created for sprite image to save memory
|
||||
$this->imageHandler->destroy($oSprite);
|
||||
// set flag to indicate valid images created
|
||||
$this->bValidImages = \true;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
}
|
||||
JCH_DEBUG ? Profiler::stop('CreateSprite', \true) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public function ValidImages(): bool
|
||||
{
|
||||
return $this->bValidImages;
|
||||
}
|
||||
|
||||
public function GetSpriteFilename(): string
|
||||
{
|
||||
$aFileParts = \pathinfo($this->sTempSpriteName);
|
||||
|
||||
return $aFileParts['basename'];
|
||||
}
|
||||
|
||||
public function GetSpriteHash(): void
|
||||
{
|
||||
// return md5($this->GetSpriteFilename().ConfigHelper::Get('/checksum'));
|
||||
}
|
||||
|
||||
public function GetCss(): array
|
||||
{
|
||||
return $this->aCss;
|
||||
}
|
||||
|
||||
public function GetAllErrors(): array
|
||||
{
|
||||
return $this->aFormErrors;
|
||||
}
|
||||
|
||||
public function GetZipFolder(): string
|
||||
{
|
||||
return $this->sZipFolder;
|
||||
}
|
||||
|
||||
public function GetCssBackground(): array
|
||||
{
|
||||
$aCssBackground = [];
|
||||
foreach ($this->aBackground as $background) {
|
||||
// if(!empty($background))
|
||||
// {
|
||||
$aCssBackground[] = @$this->aPosition[$background];
|
||||
// }
|
||||
}
|
||||
|
||||
return $aCssBackground;
|
||||
}
|
||||
|
||||
protected function FormatClassName(string $sClassName): ?string
|
||||
{
|
||||
$aExtensions = [];
|
||||
foreach ($this->imageTypes as $sType) {
|
||||
$aExtensions[] = ".{$sType}";
|
||||
}
|
||||
|
||||
return \preg_replace('/[^a-z0-9_-]+/i', '', \str_ireplace($aExtensions, '', $sClassName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the Image library imagick|gd that is available, false if failed.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getImageLibrary(): string
|
||||
{
|
||||
if (!\extension_loaded('exif')) {
|
||||
throw new MissingDependencyException('EXIF extension not loaded');
|
||||
}
|
||||
if (\extension_loaded('imagick')) {
|
||||
$sImageLibrary = 'imagick';
|
||||
} else {
|
||||
if (!\extension_loaded('gd')) {
|
||||
throw new MissingDependencyException('No image manipulation library installed');
|
||||
}
|
||||
$sImageLibrary = 'gd';
|
||||
}
|
||||
|
||||
return $sImageLibrary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Sprite;
|
||||
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareInterface;
|
||||
use _JchOptimizeVendor\Joomla\DI\ContainerAwareTrait;
|
||||
use JchOptimize\Core\Cdn;
|
||||
use JchOptimize\Core\Exception;
|
||||
use JchOptimize\Core\Helper;
|
||||
use JchOptimize\Core\Uri\Utils;
|
||||
use JchOptimize\Platform\Paths;
|
||||
use JchOptimize\Platform\Profiler;
|
||||
use Joomla\Registry\Registry;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Generator implements LoggerAwareInterface, ContainerAwareInterface
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var null|Controller
|
||||
*/
|
||||
private ?\JchOptimize\Core\Css\Sprite\Controller $spriteController;
|
||||
|
||||
private Registry $params;
|
||||
|
||||
public function __construct(Registry $params, ?Controller $spriteController)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->spriteController = $spriteController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs background images with no-repeat attribute from css and merge them in one file called a sprite.
|
||||
* Css is updated with sprite url and correct background positions for affected images.
|
||||
*
|
||||
* @param string $sCss Aggregated css file before sprite generation
|
||||
*/
|
||||
public function getSprite(string $sCss): array
|
||||
{
|
||||
$aMatches = $this->processCssUrls($sCss);
|
||||
if (empty($aMatches)) {
|
||||
return [];
|
||||
}
|
||||
if (\is_null($this->spriteController)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->generateSprite($aMatches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses regex to find css declarations containing background images to include in sprite.
|
||||
*
|
||||
* @param string $css Aggregated css file
|
||||
* @param bool $isBackend True if running in admin
|
||||
*
|
||||
* @return array Array of css declarations and image urls to replace with sprite
|
||||
*
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
public function processCssUrls(string $css, bool $isBackend = \false): array
|
||||
{
|
||||
JCH_DEBUG ? Profiler::start('ProcessCssUrls') : null;
|
||||
$aRegexStart = [];
|
||||
$aRegexStart[0] = '
|
||||
#(?:{
|
||||
(?=\\s*+(?>[^}\\s:]++[\\s:]++)*?url\\(
|
||||
(?=[^)]+\\.(?:png|gif|jpe?g))
|
||||
([^)]+)\\))';
|
||||
$aRegexStart[1] = '
|
||||
(?=\\s*+(?>[^}\\s:]++[\\s:]++)*?no-repeat)';
|
||||
$aRegexStart[2] = '
|
||||
(?(?=\\s*(?>[^};]++[;\\s]++)*?background(?:-position)?\\s*+:\\s*+(?:[^;}\\s]++[^}\\S]++)*?
|
||||
(?<p>(?:[tblrc](?:op|ottom|eft|ight|enter)|-?[0-9]+(?:%|[c-x]{2})?))(?:\\s+(?&p))?)
|
||||
(?=\\s*(?>[^};]++[;\\s]++)*?background(?:-position)?\\s*+:\\s*+(?>[^;}\\s]++[\\s]++)*?
|
||||
(?:left|top|0(?:%|[c-x]{2})?)\\s+(?:left|top|0(?:%|[c-x]{2})?)
|
||||
)
|
||||
)';
|
||||
$sRegexMiddle = '
|
||||
[^{}]++} )';
|
||||
$sRegexEnd = '#isx';
|
||||
$aIncludeImages = Helper::getArray($this->params->get('csg_include_images', ''));
|
||||
$aExcludeImages = Helper::getArray($this->params->get('csg_exclude_images', ''));
|
||||
$sIncImagesRegex = '';
|
||||
if (!empty($aIncludeImages[0])) {
|
||||
foreach ($aIncludeImages as &$sImage) {
|
||||
$sImage = \preg_quote($sImage, '#');
|
||||
}
|
||||
$sIncImagesRegex .= '
|
||||
|(?:{
|
||||
(?=\\s*+(?>[^}\\s:]++[\\s:]++)*?url';
|
||||
$sIncImagesRegex .= ' (?=[^)]* [/(](?:'.\implode('|', $aIncludeImages).')\\))';
|
||||
$sIncImagesRegex .= '\\(([^)]+)\\)
|
||||
)
|
||||
[^{}]++ })';
|
||||
}
|
||||
$sExImagesRegex = '';
|
||||
if (!empty($aExcludeImages[0])) {
|
||||
$sExImagesRegex .= '(?=\\s*+(?>[^}\\s:]++[\\s:]++)*?url\\(
|
||||
[^)]++ (?<!';
|
||||
foreach ($aExcludeImages as &$sImage) {
|
||||
$sImage = \preg_quote($sImage, '#');
|
||||
}
|
||||
$sExImagesRegex .= \implode('|', $aExcludeImages).')\\)
|
||||
)';
|
||||
}
|
||||
$sRegexStart = \implode('', $aRegexStart);
|
||||
$sRegex = $sRegexStart.$sExImagesRegex.$sRegexMiddle.$sIncImagesRegex.$sRegexEnd;
|
||||
if (\false === \preg_match_all($sRegex, $css, $aMatches)) {
|
||||
throw new Exception\RuntimeException('Error occurred matching for images to sprite');
|
||||
}
|
||||
if (isset($aMatches[3])) {
|
||||
$total = \count($aMatches[1]);
|
||||
for ($i = 0; $i < $total; ++$i) {
|
||||
$aMatches[1][$i] = \trim($aMatches[1][$i]) ? $aMatches[1][$i] : $aMatches[3][$i];
|
||||
}
|
||||
}
|
||||
if ($isBackend) {
|
||||
if (\is_null($this->spriteController)) {
|
||||
return ['include' => [], 'exclude' => []];
|
||||
}
|
||||
$aImages = [];
|
||||
$aImagesMatches = [];
|
||||
$aImgRegex = [];
|
||||
$aImgRegex[0] = $aRegexStart[0];
|
||||
$aImgRegex[2] = $aRegexStart[1];
|
||||
$aImgRegex[4] = $sRegexMiddle;
|
||||
$aImgRegex[5] = $sRegexEnd;
|
||||
$sImgRegex = \implode('', $aImgRegex);
|
||||
if (\false === \preg_match_all($sImgRegex, $css, $aImagesMatches)) {
|
||||
throw new Exception\RuntimeException('Error occurred matching for images to include');
|
||||
}
|
||||
$aImagesMatches[0] = \array_diff($aImagesMatches[0], $aMatches[0]);
|
||||
$aImagesMatches[1] = \array_diff($aImagesMatches[1], $aMatches[1]);
|
||||
$aImages['include'] = $this->spriteController->CreateSprite($aImagesMatches[1], \true);
|
||||
$aImages['exclude'] = $this->spriteController->CreateSprite($aMatches[1], \true);
|
||||
|
||||
return $aImages;
|
||||
}
|
||||
JCH_DEBUG ? Profiler::stop('ProcessCssUrls', \true) : null;
|
||||
|
||||
return $aMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates sprite image and return background positions for image replaced with sprite.
|
||||
*
|
||||
* @param array $matches Array of css declarations and image url to be included in sprite
|
||||
*
|
||||
* @throws Exception\RuntimeException
|
||||
*/
|
||||
public function generateSprite(array $matches): array
|
||||
{
|
||||
JCH_DEBUG ? Profiler::start('GenerateSprite') : null;
|
||||
$aDeclaration = $matches[0];
|
||||
$aImages = $matches[1];
|
||||
$this->spriteController->CreateSprite($aImages);
|
||||
$aSpriteCss = $this->spriteController->GetCssBackground();
|
||||
$aPatterns = [];
|
||||
$aPatterns[0] = '#background-position:[^;}]+;?#i';
|
||||
// Background position declaration regex
|
||||
$aPatterns[1] = '#(background:[^;}]*)\\b((?:top|bottom|left|right|center|-?[0-9]+(?:%|[c-x]{2})?)\\s(?:top|bottom|left|right|center|-?[0-9]+(?:%|[c-x]{2})?))([^;}]*[;}])#i';
|
||||
$aPatterns[2] = '#(background-image:)[^;}]+;?#i';
|
||||
// Background image declaration regex
|
||||
$aPatterns[3] = '#(background:[^;}]*)\\burl\\((?=[^\\)]+\\.(?:png|gif|jpe?g))[^\\)]+\\)([^;}]*[;}])#i';
|
||||
// Background image regex
|
||||
$sSpriteName = $this->spriteController->GetSpriteFilename();
|
||||
$aSearch = [];
|
||||
$sRelSpritePath = Paths::spritePath(\true).\DIRECTORY_SEPARATOR.$sSpriteName;
|
||||
$cdn = $this->container->get(Cdn::class);
|
||||
$sRelSpritePath = $cdn->loadCdnResource(Utils::uriFor($sRelSpritePath));
|
||||
for ($i = 0; $i < \count($aSpriteCss); ++$i) {
|
||||
if (isset($aSpriteCss[$i])) {
|
||||
$aSearch['needles'][] = $aDeclaration[$i];
|
||||
$aReplacements = [];
|
||||
$aReplacements[0] = '';
|
||||
$aReplacements[1] = '$1$3';
|
||||
$aReplacements[2] = '$1 url('.$sRelSpritePath.'); background-position: '.$aSpriteCss[$i].';';
|
||||
$aReplacements[3] = '$1url('.$sRelSpritePath.') '.$aSpriteCss[$i].'$2';
|
||||
$sReplacement = \preg_replace($aPatterns, $aReplacements, $aDeclaration[$i]);
|
||||
if (\is_null($sReplacement)) {
|
||||
throw new Exception\RuntimeException('Error finding replacements for sprite background positions');
|
||||
}
|
||||
$aSearch['replacements'][] = $sReplacement;
|
||||
}
|
||||
}
|
||||
JCH_DEBUG ? Profiler::stop('GenerateSprite', \true) : null;
|
||||
|
||||
return $aSearch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Sprite\Handler;
|
||||
|
||||
use JchOptimize\Core\Css\Sprite\HandlerInterface;
|
||||
use Joomla\Registry\Registry;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
abstract class AbstractHandler implements HandlerInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
public array $spriteFormats = [];
|
||||
|
||||
protected Registry $params;
|
||||
|
||||
protected array $options;
|
||||
|
||||
public function __construct(Registry $params, array $options)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->options = $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Sprite\Handler;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Gd extends \JchOptimize\Core\Css\Sprite\Handler\AbstractHandler
|
||||
{
|
||||
public function getSupportedFormats(): array
|
||||
{
|
||||
// get info about installed GD library to get image types (some versions of GD don't include GIF support)
|
||||
$oGD = \gd_info();
|
||||
$imageTypes = [];
|
||||
// store supported formats for populating drop downs etc later
|
||||
if (isset($oGD['PNG Support'])) {
|
||||
$imageTypes[] = 'PNG';
|
||||
$this->spriteFormats[] = 'PNG';
|
||||
}
|
||||
if (isset($oGD['GIF Create Support'])) {
|
||||
$imageTypes[] = 'GIF';
|
||||
}
|
||||
if (isset($oGD['JPG Support']) || isset($oGD['JPEG Support'])) {
|
||||
$imageTypes[] = 'JPG';
|
||||
}
|
||||
|
||||
return $imageTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $spriteWidth
|
||||
* @param mixed $spriteHeight
|
||||
* @param mixed $bgColour
|
||||
* @param mixed $outputFormat
|
||||
*
|
||||
* @return false|resource
|
||||
*/
|
||||
public function createSprite($spriteWidth, $spriteHeight, $bgColour, $outputFormat)
|
||||
{
|
||||
if ($this->options['is-transparent'] && !empty($this->options['background'])) {
|
||||
$oSprite = \imagecreate($spriteWidth, $spriteHeight);
|
||||
} else {
|
||||
$oSprite = \imagecreatetruecolor($spriteWidth, $spriteHeight);
|
||||
}
|
||||
// check for transparency option
|
||||
if ($this->options['is-transparent']) {
|
||||
if ('png' == $outputFormat) {
|
||||
\imagealphablending($oSprite, \false);
|
||||
$colorTransparent = \imagecolorallocatealpha($oSprite, 0, 0, 0, 127);
|
||||
\imagefill($oSprite, 0, 0, $colorTransparent);
|
||||
\imagesavealpha($oSprite, \true);
|
||||
} elseif ('gif' == $outputFormat) {
|
||||
$iBgColour = \imagecolorallocate($oSprite, 0, 0, 0);
|
||||
\imagecolortransparent($oSprite, $iBgColour);
|
||||
}
|
||||
} else {
|
||||
if (empty($bgColour)) {
|
||||
$bgColour = 'ffffff';
|
||||
}
|
||||
$iBgColour = \hexdec($bgColour);
|
||||
$iBgColour = \imagecolorallocate($oSprite, 0xFF & $iBgColour >> 0x10, 0xFF & $iBgColour >> 0x8, 0xFF & $iBgColour);
|
||||
\imagefill($oSprite, 0, 0, $iBgColour);
|
||||
}
|
||||
|
||||
return $oSprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $fileInfos
|
||||
*
|
||||
* @return false|resource
|
||||
*/
|
||||
public function createBlankImage($fileInfos)
|
||||
{
|
||||
$oCurrentImage = \imagecreatetruecolor($fileInfos['original-width'], $fileInfos['original-height']);
|
||||
\imagecolorallocate($oCurrentImage, 255, 255, 255);
|
||||
|
||||
return $oCurrentImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $spriteObject
|
||||
* @param mixed $currentImage
|
||||
* @param mixed $fileInfos
|
||||
*/
|
||||
public function resizeImage($spriteObject, $currentImage, $fileInfos)
|
||||
{
|
||||
\imagecopyresampled($spriteObject, $currentImage, $fileInfos['x'], $fileInfos['y'], 0, 0, $fileInfos['width'], $fileInfos['height'], $fileInfos['original-width'], $fileInfos['original-height']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $spriteObject
|
||||
* @param mixed $currentImage
|
||||
* @param mixed $fileInfos
|
||||
* @param mixed $resize
|
||||
*/
|
||||
public function copyImageToSprite($spriteObject, $currentImage, $fileInfos, $resize)
|
||||
{
|
||||
// if already resized the image will have been copied as part of the resize
|
||||
if (!$resize) {
|
||||
\imagecopy($spriteObject, $currentImage, $fileInfos['x'], $fileInfos['y'], 0, 0, $fileInfos['width'], $fileInfos['height']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $imageObject
|
||||
*/
|
||||
public function destroy($imageObject)
|
||||
{
|
||||
\imagedestroy($imageObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $fileInfos
|
||||
*
|
||||
* @return false|resource
|
||||
*/
|
||||
public function createImage($fileInfos)
|
||||
{
|
||||
$sFile = $fileInfos['path'];
|
||||
|
||||
switch ($fileInfos['ext']) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
$oImage = @\imagecreatefromjpeg($sFile);
|
||||
|
||||
break;
|
||||
|
||||
case 'gif':
|
||||
$oImage = @\imagecreatefromgif($sFile);
|
||||
|
||||
break;
|
||||
|
||||
case 'png':
|
||||
$oImage = @\imagecreatefrompng($sFile);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$oImage = @\imagecreatefromstring($sFile);
|
||||
}
|
||||
|
||||
return $oImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $imageObject
|
||||
* @param mixed $extension
|
||||
* @param mixed $fileName
|
||||
*/
|
||||
public function writeImage($imageObject, $extension, $fileName)
|
||||
{
|
||||
// check if we want to resample image to lower number of colours (to reduce file size)
|
||||
if (\in_array($extension, ['gif', 'png']) && 'true-colour' != $this->options['image-num-colours']) {
|
||||
\imagetruecolortopalette($imageObject, \true, $this->options['image-num-colours']);
|
||||
}
|
||||
|
||||
switch ($extension) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
// GD takes quality setting in main creation function
|
||||
\imagejpeg($imageObject, $fileName, $this->options['image-quality']);
|
||||
|
||||
break;
|
||||
|
||||
case 'gif':
|
||||
// force colour palette to 256 colours if saving sprite image as GIF
|
||||
// this will happen anyway (as GIFs can't be more than 256 colours)
|
||||
// but the quality will be better if pre-forcing
|
||||
if ($this->options['is-transparent'] && (-1 == $this->options['image-num-colours'] || $this->options['image-num-colours'] > 256 || 'true-colour' == $this->options['image-num-colours'])) {
|
||||
\imagetruecolortopalette($imageObject, \true, 256);
|
||||
}
|
||||
\imagegif($imageObject, $fileName);
|
||||
|
||||
break;
|
||||
|
||||
case 'png':
|
||||
\imagepng($imageObject, $fileName, 0);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Sprite\Handler;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class Imagick extends \JchOptimize\Core\Css\Sprite\Handler\AbstractHandler
|
||||
{
|
||||
public function getSupportedFormats(): array
|
||||
{
|
||||
$imageTypes = [];
|
||||
|
||||
try {
|
||||
$oImagick = new \Imagick();
|
||||
$aImageFormats = $oImagick->queryFormats();
|
||||
} catch (\ImagickException $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
}
|
||||
// store supported formats for populating drop downs etc later
|
||||
if (\in_array('PNG', $aImageFormats)) {
|
||||
$imageTypes[] = 'PNG';
|
||||
$this->spriteFormats[] = 'PNG';
|
||||
}
|
||||
if (\in_array('GIF', $aImageFormats)) {
|
||||
$imageTypes[] = 'GIF';
|
||||
$this->spriteFormats[] = 'GIF';
|
||||
}
|
||||
if (\in_array('JPG', $aImageFormats) || \in_array('JPEG', $aImageFormats)) {
|
||||
$imageTypes[] = 'JPG';
|
||||
}
|
||||
|
||||
return $imageTypes;
|
||||
}
|
||||
|
||||
public function createSprite($spriteWidth, $spriteHeight, $bgColour, $outputFormat): \Imagick
|
||||
{
|
||||
$spriteObject = new \Imagick();
|
||||
// create a new image - set background according to transparency
|
||||
if (!empty($this->options['background'])) {
|
||||
$spriteObject->newImage($spriteWidth, $spriteHeight, new \ImagickPixel("#{$bgColour}"), $outputFormat);
|
||||
} else {
|
||||
if ($this->options['is-transparent']) {
|
||||
$spriteObject->newImage($spriteWidth, $spriteHeight, new \ImagickPixel('#000000'), $outputFormat);
|
||||
} else {
|
||||
$spriteObject->newImage($spriteWidth, $spriteHeight, new \ImagickPixel('#ffffff'), $outputFormat);
|
||||
}
|
||||
}
|
||||
// check for transparency option
|
||||
if ($this->options['is-transparent']) {
|
||||
// set background colour to transparent
|
||||
// if no background colour use black
|
||||
if (!empty($this->options['background'])) {
|
||||
$spriteObject->transparentPaintImage(new \ImagickPixel("#{$bgColour}"), 0.0, 0, \false);
|
||||
} else {
|
||||
$spriteObject->transparentPaintImage(new \ImagickPixel('#000000'), 0.0, 0, \false);
|
||||
}
|
||||
}
|
||||
|
||||
return $spriteObject;
|
||||
}
|
||||
|
||||
public function createBlankImage($fileInfos): \Imagick
|
||||
{
|
||||
$currentImage = new \Imagick();
|
||||
$currentImage->newImage($fileInfos['original-width'], $fileInfos['original-height'], new \ImagickPixel('#ffffff'));
|
||||
|
||||
return $currentImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Imagick $currentImage
|
||||
*
|
||||
* @throws \ImagickException
|
||||
*
|
||||
* @since version
|
||||
*/
|
||||
public function resizeImage($spriteObject, $currentImage, $fileInfos)
|
||||
{
|
||||
$currentImage->thumbnailImage($fileInfos['width'], $fileInfos['height']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Imagick $spriteObject
|
||||
* @param \Imagick $currentImage
|
||||
*
|
||||
* @throws \ImagickException
|
||||
*/
|
||||
public function copyImageToSprite($spriteObject, $currentImage, $fileInfos, $resize)
|
||||
{
|
||||
$spriteObject->compositeImage($currentImage, $currentImage->getImageCompose(), $fileInfos['x'], $fileInfos['y']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Imagick $imageObject
|
||||
*
|
||||
* @since version
|
||||
*/
|
||||
public function destroy($imageObject)
|
||||
{
|
||||
$imageObject->destroy();
|
||||
}
|
||||
|
||||
public function createImage($fileInfos): \Imagick
|
||||
{
|
||||
// Imagick auto-detects file extension when creating object from image
|
||||
$oImage = new \Imagick();
|
||||
$oImage->readImage($fileInfos['path']);
|
||||
|
||||
return $oImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Imagick $imageObject
|
||||
* @param string $extension
|
||||
* @param string $fileName
|
||||
*
|
||||
* @throws \ImagickException
|
||||
*/
|
||||
public function writeImage($imageObject, $extension, $fileName)
|
||||
{
|
||||
// check if we want to resample image to lower number of colours (to reduce file size)
|
||||
if (\in_array($extension, ['gif', 'png']) && 'true-colour' != $this->options['image-num-colours']) {
|
||||
$imageObject->quantizeImage($this->options['image-num-colours'], \Imagick::COLORSPACE_RGB, 0, \false, \false);
|
||||
}
|
||||
// if we're creating a JEPG set image quality - 0% - 100%
|
||||
if (\in_array($extension, ['jpg', 'jpeg'])) {
|
||||
$imageObject->setCompression(\Imagick::COMPRESSION_JPEG);
|
||||
$imageObject->SetCompressionQuality($this->options['image-quality']);
|
||||
}
|
||||
// write out image to file
|
||||
$imageObject->writeImage($fileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Css\Sprite;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
interface HandlerInterface
|
||||
{
|
||||
public function getSupportedFormats();
|
||||
|
||||
public function createSprite($spriteWidth, $spriteHeight, $bgColour, $outputFormat);
|
||||
|
||||
public function createBlankImage($fileInfos);
|
||||
|
||||
public function resizeImage($spriteObject, $currentImage, $fileInfos);
|
||||
|
||||
public function copyImageToSprite($spriteObject, $currentImage, $fileInfos, $resize);
|
||||
|
||||
public function destroy($imageObject);
|
||||
|
||||
public function createImage($fileInfos);
|
||||
|
||||
public function writeImage($imageObject, $extension, $fileName);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core;
|
||||
|
||||
use JchOptimize\ContainerFactory;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
abstract class Debugger
|
||||
{
|
||||
private static bool $dieOnError = \false;
|
||||
|
||||
public static function printr($var, $label = null, $condition = \true): void
|
||||
{
|
||||
if ($condition) {
|
||||
self::debug('printr', $var, $label);
|
||||
}
|
||||
}
|
||||
|
||||
public static function vdump($var, $label = null, $condition = \true): void
|
||||
{
|
||||
if ($condition) {
|
||||
self::debug('vdump', $var, $label);
|
||||
}
|
||||
}
|
||||
|
||||
public static function attachErrorHandler(bool $dieOnError = \false): void
|
||||
{
|
||||
self::$dieOnError = $dieOnError;
|
||||
\set_error_handler([\JchOptimize\Core\Debugger::class, 'debuggerErrorHandler'], \E_ALL);
|
||||
\register_shutdown_function([\JchOptimize\Core\Debugger::class, 'debuggerCatchFatalErrors']);
|
||||
}
|
||||
|
||||
public static function debuggerErrorHandler(int $errno, string $errstr, string $errfile, int $errline): void
|
||||
{
|
||||
/** @var LoggerInterface $logger */
|
||||
$logger = ContainerFactory::getContainer()->get(LoggerInterface::class);
|
||||
$msg = 'Error no: '.$errno.', Message: '.$errstr.' in file: '.$errfile.' at line: '.$errline."\n";
|
||||
$logger->error($msg);
|
||||
if (self::$dieOnError) {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public static function debuggerCatchFatalErrors(): void
|
||||
{
|
||||
/** @var LoggerInterface $logger */
|
||||
$logger = ContainerFactory::getContainer()->get(LoggerInterface::class);
|
||||
$error = \error_get_last();
|
||||
$msg = 'Error type: '.$error['type'].', Message: '.$error['message'].' in file: '.$error['file'].' at line: '.$error['line']."\n";
|
||||
$logger->error($msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress ForbiddenCode
|
||||
*/
|
||||
private static function debug(string $method, $var, $label = null): void
|
||||
{
|
||||
/** @var LoggerInterface $logger */
|
||||
$logger = ContainerFactory::getContainer()->get(LoggerInterface::class);
|
||||
if (\is_null($label)) {
|
||||
$name = '';
|
||||
} else {
|
||||
$name = $label.': ';
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
case 'vdump':
|
||||
\ob_start();
|
||||
\var_dump($var);
|
||||
$logger->debug($name.\ob_get_clean());
|
||||
|
||||
break;
|
||||
|
||||
case 'printr':
|
||||
default:
|
||||
$logger->debug($name.\print_r($var, \true));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Exception;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
interface ExceptionInterface extends \Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Exception;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class ExcludeException extends \Exception implements \JchOptimize\Core\Exception\ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Exception;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements \JchOptimize\Core\Exception\ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Exception;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class MissingDependencyException extends \JchOptimize\Core\Exception\RuntimeException implements \JchOptimize\Core\Exception\ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Exception;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class PregErrorException extends \ErrorException implements \JchOptimize\Core\Exception\ExceptionInterface
|
||||
{
|
||||
public function __construct($message = '', $code = 0, $severity = \E_WARNING)
|
||||
{
|
||||
parent::__construct($message, $code, $severity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Exception;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class RuntimeException extends \RuntimeException implements \JchOptimize\Core\Exception\ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core\Exception;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
|
||||
trait StringableTrait
|
||||
{
|
||||
public function __toString()
|
||||
{
|
||||
return \get_class($this)." '{$this->getMessage()}' in {$this->getFile()}({$this->getLine()})\n{$this->getTraceAsString()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
trait FileInfosUtilsTrait
|
||||
{
|
||||
/**
|
||||
* @var FileUtils
|
||||
*/
|
||||
private \JchOptimize\Core\FileUtils $fileUtils;
|
||||
|
||||
/**
|
||||
* Truncate url at the '/' less than 40 characters prepending '...' to the string.
|
||||
*/
|
||||
public function prepareFileUrl(array $fileInfos, string $type): string
|
||||
{
|
||||
return isset($fileInfos['url']) ? $this->fileUtils->prepareForDisplay($fileInfos['url'], '', \true, 40) : ('css' == $type ? 'Style' : 'Script').' Declaration';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core;
|
||||
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\Uri;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\UriComparator;
|
||||
use _JchOptimizeVendor\GuzzleHttp\Psr7\UriResolver;
|
||||
use _JchOptimizeVendor\Psr\Http\Message\UriInterface;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
class FileUtils
|
||||
{
|
||||
/**
|
||||
* Prepare a representation of a file URL or value for display, possibly truncated.
|
||||
*
|
||||
* @param null|UriInterface $uri The string being prepared
|
||||
* @param bool $truncate If true will be truncated at specified length, prepending with an epsilon
|
||||
* @param int $length the length in number of characters
|
||||
*/
|
||||
public function prepareForDisplay(?UriInterface $uri = null, string $content = '', bool $truncate = \true, int $length = 27): string
|
||||
{
|
||||
$eps = '';
|
||||
if ($uri) {
|
||||
/* $uri = UriResolver::resolve(SystemUri::currentUri(), $uri);
|
||||
if ( ! UriComparator::isCrossOrigin(SystemUri::currentUri(), $uri)) {
|
||||
$url = $uri->getPath();
|
||||
} else {
|
||||
$url = Uri::composeComponents($uri->getScheme(), $uri->getAuthority(), $uri->getPath(), '', '');
|
||||
}*/
|
||||
$url = (string) $uri->withQuery('')->withFragment('');
|
||||
if (!$truncate) {
|
||||
return $url;
|
||||
}
|
||||
if (\strlen($url) > $length) {
|
||||
$url = \substr($url, -$length);
|
||||
$url = \preg_replace('#^[^/]*+/#', '/', $url);
|
||||
$eps = '...';
|
||||
}
|
||||
|
||||
return $eps.$url;
|
||||
}
|
||||
if (!$truncate) {
|
||||
return $content;
|
||||
}
|
||||
if (\strlen($content) > 52) {
|
||||
$content = \substr($content, 0, 52);
|
||||
$eps = '...';
|
||||
$content = $content.$eps;
|
||||
}
|
||||
if (\strlen($content) > 26) {
|
||||
$content = \str_replace($content[26], $content[26]."\n", $content);
|
||||
}
|
||||
|
||||
return $eps.$content;
|
||||
}
|
||||
}
|
||||
286
administrator/components/com_jchoptimize/lib/src/Core/Helper.php
Normal file
286
administrator/components/com_jchoptimize/lib/src/Core/Helper.php
Normal file
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* JCH Optimize - Performs several front-end optimizations for fast downloads.
|
||||
*
|
||||
* @author Samuel Marshall <samuel@jch-optimize.net>
|
||||
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize
|
||||
* @license GNU/GPLv3, or later. See LICENSE file
|
||||
*
|
||||
* If LICENSE file missing, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace JchOptimize\Core;
|
||||
|
||||
use _JchOptimizeVendor\Psr\Http\Message\UriInterface;
|
||||
use JchOptimize\Platform\Paths;
|
||||
use Joomla\Filesystem\Folder;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
\defined('_JCH_EXEC') or exit('Restricted access');
|
||||
|
||||
/**
|
||||
* Some helper functions.
|
||||
*/
|
||||
class Helper
|
||||
{
|
||||
/**
|
||||
* Checks if file (can be external) exists.
|
||||
*/
|
||||
public static function fileExists(string $sPath): bool
|
||||
{
|
||||
if (0 === \strpos($sPath, 'http')) {
|
||||
$sFileHeaders = @\get_headers($sPath);
|
||||
|
||||
return \false !== $sFileHeaders && \false === \strpos($sFileHeaders[0], '404');
|
||||
}
|
||||
|
||||
return \file_exists($sPath);
|
||||
}
|
||||
|
||||
public static function isMsieLT10(): bool
|
||||
{
|
||||
// $browser = Browser::getInstance( 'Mozilla/5.0 (Macintosh; Intel Mac OS X10_15_7) AppleWebkit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15' );
|
||||
/** @var Browser $browser */
|
||||
$browser = \JchOptimize\Core\Browser::getInstance();
|
||||
|
||||
return 'Internet Explorer' == $browser->getBrowser() && \version_compare($browser->getVersion(), '10', '<');
|
||||
}
|
||||
|
||||
public static function cleanReplacement(string $string): string
|
||||
{
|
||||
return \strtr($string, ['\\' => '\\\\', '$' => '\\$']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public static function getBaseFolder(): string
|
||||
{
|
||||
return \JchOptimize\Core\SystemUri::basePath();
|
||||
}
|
||||
|
||||
public static function strReplace(string $search, string $replace, string $subject): string
|
||||
{
|
||||
return \str_replace(self::cleanPath($search), $replace, self::cleanPath($subject));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|string[]
|
||||
*/
|
||||
public static function cleanPath(string $str)
|
||||
{
|
||||
return \str_replace(['\\\\', '\\'], '/', $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if document is of XHTML doctype.
|
||||
*/
|
||||
public static function isXhtml(string $html): bool
|
||||
{
|
||||
return (bool) \preg_match('#^\\s*+(?:<!DOCTYPE(?=[^>]+XHTML)|<\\?xml.*?\\?>)#i', \trim($html));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if document is of html5 doctype.
|
||||
*
|
||||
* @return bool True if doctype is html5
|
||||
*/
|
||||
public static function isHtml5(string $html): bool
|
||||
{
|
||||
return (bool) \preg_match('#^<!DOCTYPE html>#i', \trim($html));
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a string into an array using any regular delimiter or whitespace.
|
||||
*
|
||||
* @param array|string $string Delimited string of components
|
||||
*
|
||||
* @return string[] An array of the components
|
||||
*/
|
||||
public static function getArray($string): array
|
||||
{
|
||||
if (\is_array($string)) {
|
||||
$array = $string;
|
||||
} elseif (\is_string($string)) {
|
||||
$array = \explode(',', \trim($string));
|
||||
} else {
|
||||
$array = [];
|
||||
}
|
||||
if (!empty($array)) {
|
||||
$array = \array_map(function ($value) {
|
||||
if (\is_string($value)) {
|
||||
return \trim($value);
|
||||
}
|
||||
if (\is_object($value)) {
|
||||
return (array) $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}, $array);
|
||||
}
|
||||
|
||||
return \array_filter($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* //Being used in Sprite Controller
|
||||
*/
|
||||
public static function postAsync(string $url, Registry $params, array $posts, $logger): void
|
||||
{
|
||||
$post_params = [];
|
||||
foreach ($posts as $key => &$val) {
|
||||
if (\is_array($val)) {
|
||||
$val = \implode(',', $val);
|
||||
}
|
||||
$post_params[] = $key.'='.\urlencode($val);
|
||||
}
|
||||
$post_string = \implode('&', $post_params);
|
||||
$parts = \JchOptimize\Core\Helper::parseUrl($url);
|
||||
if (isset($parts['scheme']) && 'https' == $parts['scheme']) {
|
||||
$protocol = 'ssl://';
|
||||
$default_port = 443;
|
||||
} else {
|
||||
$protocol = '';
|
||||
$default_port = 80;
|
||||
}
|
||||
$fp = @\fsockopen($protocol.$parts['host'], $parts['port'] ?? $default_port, $errno, $errstr, 1);
|
||||
if (!$fp) {
|
||||
$logger->error($errno.': '.$errstr, $params);
|
||||
$logger->debug($errno.': '.$errstr, 'JCH_post-error');
|
||||
} else {
|
||||
$out = 'POST '.$parts['path'].'?'.$parts['query']." HTTP/1.1\r\n";
|
||||
$out .= 'Host: '.$parts['host']."\r\n";
|
||||
$out .= "Content-Type: application/x-www-form-urlencoded\r\n";
|
||||
$out .= 'Content-Length: '.\strlen($post_string)."\r\n";
|
||||
$out .= "Connection: Close\r\n\r\n";
|
||||
if (isset($post_string)) {
|
||||
$out .= $post_string;
|
||||
}
|
||||
\fwrite($fp, $out);
|
||||
\fclose($fp);
|
||||
$logger->debug($out, 'JCH_post');
|
||||
}
|
||||
}
|
||||
|
||||
public static function parseUrl(string $sUrl): array
|
||||
{
|
||||
\preg_match('#^(?:([a-z][a-z0-9+.-]*+):)?(?://(?:([^:@/]*+)(?::([^@/]*+))?@)?([^:/]++)(?::([^/]*+))?)?([^?\\#\\n]*+)?(?:\\?([^\\#\\n]*+))?(?:\\#(.*+))?$#i', $sUrl, $m);
|
||||
$parts = [];
|
||||
$parts['scheme'] = !empty($m[1]) ? $m[1] : null;
|
||||
$parts['user'] = !empty($m[2]) ? $m[2] : null;
|
||||
$parts['pass'] = !empty($m[3]) ? $m[3] : null;
|
||||
$parts['host'] = !empty($m[4]) ? $m[4] : null;
|
||||
$parts['port'] = !empty($m[5]) ? $m[5] : null;
|
||||
$parts['path'] = !empty($m[6]) ? $m[6] : '';
|
||||
$parts['query'] = !empty($m[7]) ? $m[7] : null;
|
||||
$parts['fragment'] = !empty($m[8]) ? $m[8] : null;
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|int
|
||||
*/
|
||||
public static function validateHtml(string $html)
|
||||
{
|
||||
return \preg_match('#^(?>(?><?[^<]*+)*?<html(?><?[^<]*+)*?<head(?><?[^<]*+)*?</head\\s*+>)(?><?[^<]*+)*?<body.*</body\\s*+>(?><?[^<]*+)*?</html\\s*+>#is', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $excludedStringsArray Array of excluded values to compare against
|
||||
* @param string $testString The string we're testing to see if it was excluded
|
||||
* @param string $type (css|js) No longer used
|
||||
*/
|
||||
public static function findExcludes(array $excludedStringsArray, string $testString, string $type = ''): bool
|
||||
{
|
||||
if (empty($excludedStringsArray)) {
|
||||
return \false;
|
||||
}
|
||||
foreach ($excludedStringsArray as $excludedString) {
|
||||
// Remove all spaces from test string and excluded string
|
||||
$excludedString = \preg_replace('#\\s#', '', $excludedString);
|
||||
$testString = \preg_replace('#\\s#', '', $testString);
|
||||
if ($excludedString && \false !== \strpos(\htmlspecialchars_decode($testString), $excludedString)) {
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
|
||||
return \false;
|
||||
}
|
||||
|
||||
public static function extractUrlsFromSrcset($srcSet): array
|
||||
{
|
||||
$strings = \explode(',', $srcSet);
|
||||
|
||||
return \array_map(function ($v) {
|
||||
$aUrlString = \explode(' ', \trim($v));
|
||||
|
||||
return \array_shift($aUrlString);
|
||||
}, $strings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to convert a rule set to a unique class.
|
||||
*/
|
||||
public static function cssSelectorsToClass(string $selectorGroup): string
|
||||
{
|
||||
return '_jch-'.\preg_replace('#[^0-9a-z_-]#i', '', $selectorGroup);
|
||||
}
|
||||
|
||||
public static function deleteFolder(string $folder): bool
|
||||
{
|
||||
$it = new \RecursiveDirectoryIterator($folder, \FilesystemIterator::SKIP_DOTS);
|
||||
$files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST);
|
||||
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($files as $file) {
|
||||
if ($file->isDir()) {
|
||||
\rmdir($file->getPathname());
|
||||
} else {
|
||||
\unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
\rmdir($folder);
|
||||
|
||||
return !\file_exists($folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a Uri is valid.
|
||||
*/
|
||||
public static function uriInvalid(UriInterface $uri): bool
|
||||
{
|
||||
if ('' == (string) $uri) {
|
||||
return \true;
|
||||
}
|
||||
if ('' == $uri->getScheme() && '' == $uri->getAuthority() && '' == $uri->getQuery() && '' == $uri->getFragment()) {
|
||||
if ('/' == $uri->getPath() || $uri->getPath() == \JchOptimize\Core\SystemUri::basePath()) {
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
|
||||
return \false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|int
|
||||
*
|
||||
* @psalm-return 0|1|false
|
||||
*/
|
||||
public static function isStaticFile(string $filePath)
|
||||
{
|
||||
return \preg_match('#\\.(?:css|js|png|jpe?g|gif|bmp|webp|svg)$#i', $filePath);
|
||||
}
|
||||
|
||||
public static function createCacheFolder(): void
|
||||
{
|
||||
if (!\file_exists(Paths::cacheDir())) {
|
||||
try {
|
||||
Folder::create(Paths::cacheDir());
|
||||
} catch (\Exception $exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user