Initial commit: is_themecore out of the box v4.1.3

This commit is contained in:
Isabelle Anno
2025-11-19 13:17:30 +01:00
committed by Isabelle
commit 5d80babd5a
333 changed files with 33026 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Oksydan\Module\IsThemeCore\Controller\Admin;
use PrestaShop\PrestaShop\Core\Form\FormHandlerInterface;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use PrestaShopBundle\Security\Annotation\AdminSecurity;
use PrestaShopBundle\Security\Annotation\DemoRestricted;
use PrestaShopBundle\Security\Annotation\ModuleActivated;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Class SettingsController
*
* @ModuleActivated(moduleName="is_themecore", redirectRoute="admin_module_manage")
*/
class SettingsController extends FrameworkBundleAdminController
{
/**
* @AdminSecurity(
* "is_granted(['read'], request.get('_legacy_controller'))",
* message="You do not have permission to access this."
* )
*
* @param Request $request
*
* @return Response
*/
public function indexAction(Request $request): Response
{
$generalFormDataHandler = $this->getGeneralFormHandler();
$webpFormDataHandler = $this->getWebpFormHandler();
/** @var FormInterface<string, mixed> $generalForm */
$generalForm = $generalFormDataHandler->getForm();
$webpForm = $webpFormDataHandler->getForm();
return $this->render('@Modules/is_themecore/views/templates/back/components/layouts/settings.html.twig', [
'general_form' => $generalForm->createView(),
'webp_form' => $webpForm->createView(),
]);
}
/**
* @AdminSecurity(
* "is_granted('update', request.get('_legacy_controller')) && is_granted('create', request.get('_legacy_controller')) && is_granted('delete', request.get('_legacy_controller'))",
* message="You do not have permission to update this.",
* redirectRoute="is_themecore_module_settings"
* )
*
* @DemoRestricted(redirectRoute="is_themecore_module_settings")
*
* @param Request $request
*
* @return RedirectResponse
*
* @throws \LogicException
*/
public function processGeneralFormAction(Request $request)
{
return $this->processForm(
$request,
$this->getGeneralFormHandler(),
'General'
);
}
/**
* @AdminSecurity(
* "is_granted('update', request.get('_legacy_controller')) && is_granted('create', request.get('_legacy_controller')) && is_granted('delete', request.get('_legacy_controller'))",
* message="You do not have permission to update this.",
* redirectRoute="is_themecore_module_settings"
* )
*
* @DemoRestricted(redirectRoute="is_themecore_module_settings")
*
* @param Request $request
*
* @return RedirectResponse
*
* @throws \LogicException
*/
public function processWebpFormAction(Request $request)
{
return $this->processForm(
$request,
$this->getWebpFormHandler(),
'Webp'
);
}
/**
* @DemoRestricted(redirectRoute="is_themecore_module_settings")
*
* @param Request $request
*
* @return RedirectResponse
*
* @throws \LogicException
*/
public function processWebpEraseImages(Request $request)
{
$time_start = microtime(true);
$eraser = $this->get('oksydan.module.is_themecore.core.webp.webp_files_eraser');
switch ($request->get('type')) {
case 'all':
$eraser->setQuery(_PS_ROOT_DIR_);
break;
case 'product':
$eraser->setQuery(_PS_PROD_IMG_DIR_);
break;
case 'module':
$eraser->setQuery(_PS_MODULE_DIR_);
break;
case 'cms':
$eraser->setQuery(_PS_IMG_DIR_ . 'cms/');
break;
case 'themes':
$eraser->setQuery(_PS_ROOT_DIR_ . '/themes/');
break;
default:
$eraser->setQuery(_PS_ROOT_DIR_);
break;
}
$eraser->eraseFiles();
$time_end = microtime(true);
$execution_time = round($time_end - $time_start, 2);
$this->addFlash('success', $this->trans('%1$s - webp images has been erased successfully in %2$ss', 'Modules.isthemecore.Admin', [$eraser->getFilesCount(), $execution_time]));
return $this->redirectToRoute('is_themecore_module_settings');
}
/**
* Process form.
*
* @param Request $request
* @param FormHandlerInterface $formHandler
* @param string $hookName
*
* @return RedirectResponse
*/
private function processForm(Request $request, FormHandlerInterface $formHandler)
{
$form = $formHandler->getForm();
$form->handleRequest($request);
if ($form->isSubmitted()) {
if ($form->isValid()) {
$data = $form->getData();
$saveErrors = $formHandler->save($data);
if (!empty($data['webp_enabled'])) {
$generator = $this->get('oksydan.module.is_themecore.core.htaccess.htaccess_generator');
$generator->generate((bool) $data['webp_enabled']);
$generator->writeFile();
}
if (0 === count($saveErrors)) {
$this->addFlash('success', $this->trans('Successful update.', 'Admin.Notifications.Success'));
} else {
$this->flashErrors($saveErrors);
}
}
$formErrors = [];
foreach ($form->getErrors(true) as $error) {
$formErrors[] = $error->getMessage();
}
$this->flashErrors($formErrors);
}
return $this->redirectToRoute('is_themecore_module_settings');
}
/**
* @return FormHandlerInterface
*/
private function getGeneralFormHandler()
{
/** @var FormHandlerInterface */
$formDataHandler = $this->get('oksydan.module.is_themecore.form.settings.general_form_data_handler');
return $formDataHandler;
}
/**
* @return FormHandlerInterface
*/
private function getWebpFormHandler()
{
/** @var FormHandlerInterface */
$formDataHandler = $this->get('oksydan.module.is_themecore.form.settings.webp_form_data_handler');
return $formDataHandler;
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,80 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Breadcrumbs;
class ThemeBreadcrumbs
{
protected $breadcrumbs = [];
public $pageName;
public $translator;
public function __construct()
{
$this->context = \Context::getContext();
$this->pageName = $this->context->controller->getPageName();
$this->getAvailableBreadcrumbs();
}
protected function getAvailableBreadcrumbs()
{
$this->breadcrumbs = $this->getCommonBreadcrumbs();
}
public function getBreadcrumb()
{
$breadcrumb = [];
$breadcrumb['links'] = $this->getBreadcrumbByPageName();
$breadcrumb['count'] = count($breadcrumb['links']);
return $breadcrumb;
}
public function getBreadcrumbByPageName()
{
$breadcrumb = [];
if (isset($this->breadcrumbs[$this->pageName])) {
$breadcrumb = $this->breadcrumbs[$this->pageName];
}
return $breadcrumb;
}
protected function getCommonBreadcrumbs()
{
$pages = [
[
'controller' => 'cart',
'name' => $this->context->getTranslator()->trans('Shopping Cart', [], 'Shop.Theme.Checkout'),
],
[
'controller' => 'pagenotfound',
'name' => $this->context->getTranslator()->trans('404', [], 'Shop.Theme.Global'),
],
[
'controller' => 'stores',
'name' => $this->context->getTranslator()->trans('Our stores', [], 'Shop.Theme.Global'),
],
[
'controller' => 'sitemap',
'name' => $this->context->getTranslator()->trans('Sitemap', [], 'Shop.Theme.Global'),
],
];
$breadcrumbs = [];
foreach ($pages as $page) {
$breadcrumbs[$page['controller']] = [
[
'url' => $this->context->link->getPageLink('index'),
'title' => $this->context->getTranslator()->trans('Home', [], 'Shop.Theme.Global'),
],
[
'url' => $this->context->link->getPageLink($page['controller']),
'title' => $page['name'],
],
];
}
return $breadcrumbs;
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,233 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Htaccess;
class HtaccessGenerator
{
private $module;
private $domains = [];
private $medias = [];
protected $moduleWebpGeneratorFile;
protected $mediaDomains = null;
protected $tempContent = '';
protected $contentBefore = '';
protected $contentAfter = '';
protected $wrapperBlockComments = [
'COMMENT_START' => '~~start-is_themecore~~',
'COMMENT_END' => '~~end-is_themecore~~',
];
public function __construct(\Is_themecore $module)
{
$this->domains = \Tools::getDomains();
$this->module = $module;
$this->moduleWebpGeneratorFile = "%{ENV:REWRITEBASE}modules/{$this->module->name}/webp.php";
}
public function generate($addRewrite = true): void
{
$htaccessFile = $this->getHtaccessFilePath();
if (file_exists($htaccessFile)) {
$content = \Tools::file_get_contents($htaccessFile);
if (preg_match('#^(.*)\# ' . $this->wrapperBlockComments['COMMENT_START'] . '.*\# ' . $this->wrapperBlockComments['COMMENT_END'] . '[^\n]*(.*)$#s', $content, $match)) {
$this->contentBefore = $match[1];
$this->contentAfter = $match[2];
} else {
$this->contentAfter = $content;
}
}
if ($addRewrite) {
$this->generateHtaccessHeader();
$this->write('<IfModule mod_rewrite.c>');
$this->write('RewriteEngine On');
$this->writeNl();
$this->generateImagesRewrites();
$this->write('</IfModule>');
$this->write("# {$this->wrapperBlockComments['COMMENT_END']} Do not remove this comment");
}
$this->write($this->contentAfter);
}
public function writeFile(): bool
{
$htaccessFile = $this->getHtaccessFilePath();
if (!$writeToFile = @fopen($htaccessFile, 'wb')) {
return false;
}
if (!fwrite($writeToFile, $this->tempContent)) {
return false;
}
fclose($writeToFile);
return true;
}
protected function getMediaDomains(): string
{
if ($this->mediaDomains === null) {
if (\Configuration::getMultiShopValues('PS_MEDIA_SERVER_1')
&& \Configuration::getMultiShopValues('PS_MEDIA_SERVER_2')
&& \Configuration::getMultiShopValues('PS_MEDIA_SERVER_3')
) {
$this->medias = [
\Configuration::getMultiShopValues('PS_MEDIA_SERVER_1'),
\Configuration::getMultiShopValues('PS_MEDIA_SERVER_2'),
\Configuration::getMultiShopValues('PS_MEDIA_SERVER_3'),
];
}
$this->mediaDomains = '';
foreach ($this->medias as $media) {
foreach ($media as $mediaUrl) {
if ($mediaUrl) {
$this->mediaDomains .= 'RewriteCond %{HTTP_HOST} ^' . $mediaUrl . '$ [OR]' . PHP_EOL;
}
}
}
}
return $this->mediaDomains;
}
protected function getDomainRewriteCond($domain): string
{
return "RewriteCond %{HTTP_HOST} ^$domain$";
}
protected function generateImagesRewrites(): void
{
foreach ($this->domains as $domain => $uri) {
$this->generateProductImagesRewrite($domain);
$this->generateCategoryImagesRewrite($domain);
$this->generateOtherImagesRewrite($domain);
}
}
protected function writeMediaDomainsCondition()
{
$mediaDomains = $this->getMediaDomains();
if ($mediaDomains) {
$this->write($mediaDomains, false);
}
}
protected function generateProductImagesRewrite($domain): void
{
$domainRewriteCond = $this->getDomainRewriteCond($domain);
for ($i = 1; $i <= 7; ++$i) {
$imgPath = $imgName = '';
for ($j = 1; $j <= $i; ++$j) {
$imgPath .= '$' . $j . '/';
$imgName .= '$' . $j;
}
$imgName .= '$' . $j;
// WEBP FILE EXISTS
$this->writeMediaDomainsCondition();
$this->write($domainRewriteCond);
$this->write('RewriteCond %{DOCUMENT_ROOT}/img/p/' . $imgPath . $imgName . '$' . ($j + 1) . '.webp -f');
$this->write('RewriteRule ^' . str_repeat('([0-9])', $i) . '(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.webp$ %{ENV:REWRITEBASE}img/p/' . $imgPath . $imgName . '$' . ($j + 1) . '.webp [L]');
$this->writeNl();
// WEBP FILE NOT EXISTS
$this->writeMediaDomainsCondition();
$this->write($domainRewriteCond);
$this->write('RewriteCond %{DOCUMENT_ROOT}/img/p/' . $imgPath . $imgName . '$' . ($j + 1) . '.webp !-f');
$this->write('RewriteRule ^' . str_repeat('([0-9])', $i) . '(\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\.webp$ ' . $this->moduleWebpGeneratorFile . '?source=%{DOCUMENT_ROOT}/img/p/' . $imgPath . $imgName . '$' . ($j + 1) . '.webp [NC,L]');
$this->writeNl();
}
}
protected function generateCategoryImagesRewrite($domain): void
{
$domainRewriteCond = $this->getDomainRewriteCond($domain);
// WEBP FILE EXISTS
$this->writeMediaDomainsCondition();
$this->write($domainRewriteCond);
$this->write('RewriteCond %{DOCUMENT_ROOT}/img/c/$1$2.webp -f');
$this->write('RewriteRule ^c/([0-9]+)(\-[\.*_a-zA-Z0-9-]*)(-[0-9]+)?/.+\.webp$ %{ENV:REWRITEBASE}img/c/$1$2.webp [L]');
$this->writeNl();
$this->writeMediaDomainsCondition();
$this->write($domainRewriteCond);
$this->write('RewriteCond %{DOCUMENT_ROOT}/img/c/$1$2$3.webp -f');
$this->write('RewriteRule ^c/([0-9]+)(\-[\.*_a-zA-Z0-9-]*)(-[0-9]+)?/.+\.webp$ %{ENV:REWRITEBASE}img/c/$1$2$3.webp [L]');
$this->writeNl();
// WEBP FILE NOT EXISTS
$this->writeMediaDomainsCondition();
$this->write($domainRewriteCond);
$this->write('RewriteCond %{DOCUMENT_ROOT}/img/c/$1$2.webp !-f');
$this->write('RewriteRule ^c/([0-9]+)(\-[\.*_a-zA-Z0-9-]*)(-[0-9]+)?/.+\.webp$ ' . $this->moduleWebpGeneratorFile . '?source=%{DOCUMENT_ROOT}/img/c/$1$2.webp [NC,L]');
$this->writeNl();
$this->writeMediaDomainsCondition();
$this->write($domainRewriteCond);
$this->write('RewriteCond %{DOCUMENT_ROOT}/img/c/$1$2$3.webp !-f');
$this->write('RewriteRule ^c/([0-9]+)(\-[\.*_a-zA-Z0-9-]*)(-[0-9]+)?/.+\.webp$ ' . $this->moduleWebpGeneratorFile . '?source=%{DOCUMENT_ROOT}/img/c/$1$2$3.webp [NC,L]');
$this->writeNl();
}
protected function generateOtherImagesRewrite($domain): void
{
$domainRewriteCond = $this->getDomainRewriteCond($domain);
// WEBP FILE NOT EXISTS
$this->writeMediaDomainsCondition();
$this->write($domainRewriteCond);
$this->write('RewriteCond %{REQUEST_FILENAME} !-f');
$this->write('RewriteRule ^(.*)\.webp$ ' . $this->moduleWebpGeneratorFile . '?source=%{DOCUMENT_ROOT}/$1.webp [NC,L]');
$this->writeNl();
}
protected function generateHtaccessHeader(): void
{
$this->write("# {$this->wrapperBlockComments['COMMENT_START']} Do not remove this comment");
$this->write('# Allow webp files to be sent by Apache 2.2');
$this->write('<IfModule !mod_authz_core.c>');
$this->write('<Files ~ "\\.(webp)$">', true, 1);
$this->write('Allow from all', true, 2);
$this->write('</Files>', true, 1);
$this->write('</IfModule>');
$this->writeNl();
$this->write('# Allow webp files to be sent by Apache 2.4');
$this->write('<IfModule mod_authz_core.c>');
$this->write('<Files ~ "\\.(webp)$">', true, 1);
$this->write('Require all granted', true, 2);
$this->write('allow from all', true, 2);
$this->write('</Files>', true, 1);
$this->write('</IfModule>');
$this->writeNl();
}
protected function write($line, $addEOL = true, $tabs = 0): void
{
$this->tempContent .= str_repeat("\t", $tabs) . $line . ($addEOL ? PHP_EOL : '');
}
protected function writeNl(): void
{
$this->write('');
}
protected function getHtaccessFilePath(): string
{
return _PS_ROOT_DIR_ . '/.htaccess';
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,60 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\ListingDisplay;
use Oksydan\Module\IsThemeCore\Form\Settings\GeneralConfiguration;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ThemeListDisplay
{
private $cookieName = 'listingDisplayType';
private $displayList = [
'grid',
'list',
];
protected function getRequest(): Request
{
Request::setFactory(static function ($query, $request, $attributes, $cookies, $files, $server, $content) {
return new Request($query, $request, $attributes, $cookies, [], $server, $content);
});
return Request::createFromGlobals();
}
public function setDisplay($display): Response
{
if (!in_array($display, $this->displayList)) {
$display = \Configuration::get(GeneralConfiguration::THEMECORE_DISPLAY_LIST);
}
$response = new Response();
$response->headers->setCookie(new Cookie(
$this->cookieName,
$display,
(new \DateTime('now'))->modify('+ 30 days')->getTimestamp(),
'/'
));
return $response->sendHeaders();
}
public function getDisplay()
{
$displayFromCookie = $this->getRequest()->cookies->get($this->cookieName);
if ($displayFromCookie) {
return $displayFromCookie;
}
return \Configuration::get(GeneralConfiguration::THEMECORE_DISPLAY_LIST);
}
public function getDisplayOptions()
{
return $this->displayList;
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,54 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Partytown;
use Symfony\Component\Filesystem\Filesystem;
class FilesInstallation
{
private \Is_themecore $module;
public function __construct(
\Is_themecore $module
) {
$this->module = $module;
}
public function installFiles(): void
{
$this->installPartytown();
}
protected function getFileSystem(): Filesystem
{
return new Filesystem();
}
private function installPartytown(): void
{
$source = _PS_MODULE_DIR_ . $this->module->name . '/public/~partytown';
$destination = _PS_ROOT_DIR_ . '/~partytown';
$fileSystem = $this->getFileSystem();
if (!file_exists($source)) {
return;
}
if (file_exists($destination)) {
$fileSystem->remove($destination);
}
$fileSystem->mkdir($destination);
$directoryIterator = new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS);
$iterator = new \RecursiveIteratorIterator($directoryIterator, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $item) {
if ($item->isDir()) {
$fileSystem->mkdir($destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
} else {
$fileSystem->copy($item, $destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
}
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Partytown;
class PartytownScript
{
private \Is_themecore $module;
public function __construct(
\Is_themecore $module
) {
$this->module = $module;
}
public function getScriptPath(): string
{
return _PS_MODULE_DIR_ . $this->module->name . '/public/partytown.js';
}
public function getScriptUri(): string
{
return $this->module->getPathUri() . '/public/partytown.js';
}
public function getScriptContent(): string
{
$script = '';
$filePath = $this->getScriptPath();
if (file_exists($filePath)) {
$script = file_get_contents($filePath);
}
return $script;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Partytown;
class PartytownScriptUriResolver
{
private \Context $context;
const PUBLIC_PARTYTOWN_PATH = '~partytown/';
public function __construct(
\Context $context
) {
$this->context = $context;
}
public function getScriptUri(): string
{
return $this->context->shop->physical_uri . self::PUBLIC_PARTYTOWN_PATH;
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Smarty;
use Oksydan\Module\IsThemeCore\Core\Webp\WebpPictureGenerator;
use Oksydan\Module\IsThemeCore\Form\Settings\WebpConfiguration;
class SmartyHelperFunctions
{
public static function generateImagesSources($params)
{
$image = $params['image'];
$size = $params['size'];
$lazyLoad = isset($params['lazyload']) ? $params['lazyload'] : true;
$attributes = [];
$highDpiImagesEnabled = (bool) \Configuration::get('PS_HIGHT_DPI');
$srcAttributePrefix = $lazyLoad ? 'data-' : '';
$img = $image['bySize'][$size]['url'];
if ($highDpiImagesEnabled) {
$size2x = $size . '2x';
$img2x = str_replace($size, $size2x, $img);
$attributeName = $srcAttributePrefix . 'srcset';
$attributes[$attributeName] = "$img, $img2x 2x";
} else {
$attributeName = $srcAttributePrefix . 'src';
$attributes[$attributeName] = $img;
}
if ($lazyLoad) {
$width = $image['bySize'][$size]['width'];
$height = $image['bySize'][$size]['height'];
$placeholderSrc = self::generateImageSvgPlaceholder(['width' => $width, 'height' => $height]);
$attributes['src'] = $placeholderSrc;
}
$attributesToPrint = [];
foreach ($attributes as $attr => $value) {
$attributesToPrint[] = $attr . '="' . $value . '"';
}
return implode(PHP_EOL, $attributesToPrint);
}
public static function generateImageSvgPlaceholder($params)
{
$width = $params['width'];
$height = $params['height'];
return "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='$width' height='$height' viewBox='0 0 1 1'%3E%3C/svg%3E";
}
public static function appendParamToUrl($params)
{
list(
'url' => $url,
'key' => $key,
'value' => $value
) = $params;
$replace = false;
if (isset($params['replace'])) {
$replace = $params['replace'];
}
if (!is_array($value)) {
$value = [$value];
} else {
$replace = false;
}
foreach ($value as $qValue) {
$query = parse_url($url, PHP_URL_QUERY);
if ($query) {
if ($replace) {
parse_str($query, $queryParams);
$queryParams[$key] = $qValue;
$url = str_replace("?$query", '?' . http_build_query($queryParams), $url);
} else {
$queryParams = [];
$queryParams[$key] = $qValue;
$url .= '&' . http_build_query($queryParams);
}
} else {
$url .= '?' . urlencode($key) . '=' . urlencode($qValue);
}
}
return $url;
}
public static function imagesBlock($params, $content, $smarty)
{
$webpEnabled = isset($params['webpEnabled']) ? $params['webpEnabled'] : \Configuration::get(WebpConfiguration::THEMECORE_WEBP_ENABLED);
if ($webpEnabled && !empty($content)) {
$pictureGenerator = new WebpPictureGenerator($content);
$pictureGenerator
->loadContent()
->generatePictureTags();
return $pictureGenerator->getContent();
}
return $content;
}
public static function displayMobileBlock($params, $content, $smarty)
{
if (!empty($content) && \Context::getContext()->isMobile()) {
return $content;
}
return '';
}
public static function displayDesktopBlock($params, $content, $smarty)
{
if (!empty($content) && !\Context::getContext()->isMobile()) {
return $content;
}
return '';
}
public static function cmsImagesBlock($params, $content, $smarty)
{
$doc = new \DOMDocument();
$doc->loadHTML('<meta http-equiv="Content-Type" content="charset=utf-8">' . $content);
$context = \Context::getContext();
$images = $doc->getElementsByTagName('img');
$domains = \Tools::getDomains();
$medias = [
\Configuration::get('PS_MEDIA_SERVER_1'),
\Configuration::get('PS_MEDIA_SERVER_2'),
\Configuration::get('PS_MEDIA_SERVER_3'),
];
$internalUrls = [];
foreach ($domains as $domain => $options) {
$internalUrls[] = $domain;
}
foreach ($medias as $media) {
if ($media) {
$internalUrls[] = $media;
}
}
foreach ($images as $image) {
$newImg = $doc->createElement('img');
$src = urldecode($image->attributes->getNamedItem('src')->nodeValue);
if (!preg_match('/' . implode('|', $internalUrls) . '/i', $src)) {
$newImg->setAttribute('data-external-url', '');
}
foreach ($image->attributes as $attribute) {
$newImg->setAttribute($attribute->nodeName, $attribute->nodeValue);
}
$image->parentNode->replaceChild($newImg, $image);
}
$content = $doc->saveHTML();
$content = str_replace('<meta http-equiv="Content-Type" content="charset=utf-8">', '', $content);
$webpEnabled = isset($params['webpEnabled']) ? $params['webpEnabled'] : \Configuration::get(WebpConfiguration::THEMECORE_WEBP_ENABLED);
if ($webpEnabled && !empty($content)) {
$pictureGenerator = new WebpPictureGenerator($content);
$pictureGenerator
->loadContent()
->generatePictureTags();
return $pictureGenerator->getContent();
}
return $content;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData;
use Oksydan\Module\IsThemeCore\Core\StructuredData\Presenter\StructuredDataPresenterInterface;
use Oksydan\Module\IsThemeCore\Core\StructuredData\Provider\StructuredDataProviderInterface;
abstract class AbstractStructuredData
{
private StructuredDataProviderInterface $provider;
private StructuredDataPresenterInterface $presenter;
public function __construct(
StructuredDataProviderInterface $provider,
StructuredDataPresenterInterface $presenter
) {
$this->provider = $provider;
$this->presenter = $presenter;
}
/**
* Return formatted json data
*
* @return string
*/
public function getFormattedData(): string
{
$data = $this->provider->getData();
$jsonData = $this->presenter->present($data);
\Hook::exec('actionStructuredData' . ucfirst($this->getStructuredDataType()),
[
'jsonData' => &$jsonData,
'rawData' => $data,
]
);
if (empty($jsonData)) {
return '';
} else {
return json_encode($jsonData, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData;
class BreadcrumbStructuredData extends AbstractStructuredData implements StructuredDataInterface
{
public function getStructuredDataType(): string
{
return 'breadcrumb';
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Presenter;
class StructuredDataBreadcrumbPresenter implements StructuredDataPresenterInterface
{
private array $presentedData = [];
private array $breadcrumbData = [];
public function present($data): array
{
$this->breadcrumbData = $data;
$this->presentBreadcrumbData();
return $this->presentedData;
}
private function presentBreadcrumbData(): void
{
$breadcrumbs = $this->breadcrumbData['links'];
if ($this->breadcrumbData['count'] > 1) {
$this->presentedData['@context'] = 'http://schema.org';
$this->presentedData['@type'] = 'BreadcrumbList';
$this->presentedData['itemListElement'] = [];
foreach ($breadcrumbs as $i => $breadcrumb) {
$this->presentedData['itemListElement'][] = [
'@type' => 'ListItem',
'position' => $i + 1,
'name' => $breadcrumb['title'],
'item' => $breadcrumb['url'],
];
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Presenter;
interface StructuredDataPresenterInterface
{
/**
* Return formatted data
*
* @param array $data Data from provider
*
* @return array
*/
public function present($data);
}

View File

@@ -0,0 +1,166 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Presenter;
class StructuredDataProductPresenter implements StructuredDataPresenterInterface
{
private $presentedData = [];
private $productData;
private $context;
public function __construct(\Context $context)
{
$this->context = $context;
}
public function present($data): array
{
$this->productData = $data;
$this->getProductBasics();
$this->getProductIdentifier();
$this->getProductBrandData();
$this->getProductReviewsData();
$this->getProductOffers();
return $this->presentedData;
}
private function getProductBasics(): void
{
$this->presentedData['@context'] = 'http://schema.org/';
$this->presentedData['@type'] = 'Product';
$this->presentedData['name'] = $this->productData['name'];
$this->presentedData['category'] = $this->productData['category_name'];
if (!empty($this->productData['description_short'])) {
$this->presentedData['description'] = strip_tags($this->productData['description_short']);
}
if ($this->productData['default_image']) {
$this->presentedData['image'] = $this->productData['default_image']['large']['url'];
}
if ($this->productData['reference']) {
$this->presentedData['sku'] = $this->productData['reference'];
}
if (!empty($this->productData['weight']) && $this->productData['weight'] > 0) {
$this->presentedData['weight'] = [
'@context' => 'https://schema.org',
'@type' => 'QuantitativeValue',
'value' => $this->productData['weight'],
'unitCode' => $this->productData['weight_unit'],
];
}
}
private function getProductBrandData(): void
{
if (empty($this->productData['id_manufacturer'])) {
return;
}
$productManufacturer = new \Manufacturer((int) $this->productData['id_manufacturer'], $this->context->language->id);
if (!empty($productManufacturer->name)) {
$this->presentedData['brand'] = [
'@type' => 'Brand',
'name' => $productManufacturer->name,
];
}
}
private function getProductIdentifier(): void
{
if (!empty($this->productData['ean13'])) {
$this->presentedData['gtin13'] = $this->productData['ean13'];
} elseif (!empty($this->productData['upc'])) {
$this->presentedData['gtin13'] = '0' . $this->productData['upc'];
} elseif (!empty($this->productData['isbn'])) {
$this->presentedData['isbn'] = $this->productData['isbn'];
} elseif (!empty($this->productData['reference'])) {
$this->presentedData['mpn'] = $this->productData['reference'];
}
}
private function getProductOffers(): void
{
if (!$this->productData['show_price']) {
return;
}
$this->presentedData['offers'] = [
'@type' => 'Offer',
'name' => $this->productData['name'],
'price' => $this->productData['price_amount'],
'url' => $this->productData['url'],
'priceCurrency' => $this->context->currency->iso_code,
];
if (count($this->productData['images']) > 0) {
$images = [];
foreach ($this->productData['images'] as $img) {
$images[] = $img['large']['url'];
}
$this->presentedData['offers']['image'] = $images;
}
if ($this->productData['reference']) {
$this->presentedData['offers']['sku'] = $this->productData['reference'];
}
$this->presentedData['offers']['availability'] = $this->productData['quantity'] > 0 || $this->productData['allow_oosp'] ? 'http://schema.org/InStock' : 'http://schema.org/OutOfStock';
if ($this->productData['show_condition'] && isset($this->productData['condition'])) {
$this->presentedData['offers']['itemCondition'] = $this->productData['condition']['schema_url'];
}
if ($this->productData['specific_prices'] && $this->productData['specific_prices']['to'] > (new \DateTime())->format('Y-m-d H:i:s')) {
$date = new \DateTime($this->productData['specific_prices']['to']);
$this->presentedData['offers']['priceValidUntil'] = $date->format('Y-m-d');
}
}
private function getProductReviewsData(): void
{
if (empty($this->productData['productRating'])) {
return;
}
$reviews = [];
foreach ($this->productData['productRating']['reviews'] as $review) {
$datePublished = new \DateTime($review['date_add']);
$reviews[] = [
'@type' => 'Review',
'author' => [
'@type' => 'Person',
'name' => $review['customer_name'],
],
'name' => $review['title'],
'reviewBody' => $review['content'],
'datePublished' => $datePublished->format(\DateTime::ATOM),
'reviewRating' => [
'@type' => 'Rating',
'ratingValue' => $review['grade'],
],
];
}
$aggregateRating = [
'@type' => 'AggregateRating',
'ratingValue' => $this->productData['productRating']['averageGrade'],
'ratingCount' => $this->productData['productRating']['commentsNb'],
'reviewCount' => $this->productData['productRating']['commentsNb'],
];
if ($reviews) {
$this->presentedData['review'] = $reviews;
}
$this->presentedData['aggregateRating'] = $aggregateRating;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Presenter;
class StructuredDataShopPresenter implements StructuredDataPresenterInterface
{
private $presentedData = [];
private $shopData;
private $context;
public function __construct(\Context $context)
{
$this->context = $context;
}
public function present($data): array
{
$this->shopData = $data;
$this->presentShopData();
return $this->presentedData;
}
private function presentShopData(): void
{
$this->presentedData['@context'] = 'http://schema.org';
$this->presentedData['@type'] = 'Organization';
$this->presentedData['name'] = $this->shopData['name'];
$this->presentedData['url'] = $this->context->link->getPageLink('index');
$this->presentedData['logo'] = [
'@type' => 'ImageObject',
'url' => $this->shopData['logo'],
];
if ($this->shopData['phone']) {
$this->presentedData['contactPoint'] = [
'@type' => 'ContactPoint',
'telephone' => $this->shopData['phone'],
'contactType' => 'customer service',
];
}
$address = $this->shopData['address'];
$postalCode = $address['postcode'];
$city = $address['city'];
$country = $address['country'];
$addressRegion = $address['state'];
$streetAddress = $address['address1'];
if ($postalCode || $city || $country || $addressRegion || $streetAddress) {
$this->presentedData['address'] = [
'@type' => 'PostalAddress',
];
if ($postalCode) {
$this->presentedData['address']['postalCode'] = $postalCode;
}
if ($streetAddress) {
$this->presentedData['address']['streetAddress'] = $streetAddress;
}
if ($country || $city) {
$addressLocality = '';
if ($city) {
$addressLocality = $city;
}
if ($country) {
$addressLocality .= ($addressLocality != '' ? ', ' : '') . $country;
}
$this->presentedData['address']['addressLocality'] = $addressLocality;
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Presenter;
class StructuredDataWebsitePresenter implements StructuredDataPresenterInterface
{
private $presentedData = [];
private $websiteData;
private $context;
public function __construct(\Context $context)
{
$this->context = $context;
}
public function present($data): array
{
$this->websiteData = $data;
$this->presentShopData();
return $this->presentedData;
}
private function presentShopData(): void
{
$this->presentedData['@context'] = 'http://schema.org';
$this->presentedData['@type'] = 'WebSite';
$this->presentedData['url'] = $this->context->link->getPageLink('index');
$this->presentedData['image'] = [
'@type' => 'ImageObject',
'url' => $this->websiteData['logo'],
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData;
class ProductStructuredData extends AbstractStructuredData implements StructuredDataInterface
{
public function getStructuredDataType(): string
{
return 'product';
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Provider;
class StructuredDataBreadcrumbProvider implements StructuredDataProviderInterface
{
protected \Context $context;
public function __construct(\Context $context)
{
$this->context = $context;
}
public function getData(): array
{
return $this->context->controller->getBreadcrumb();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Provider;
class StructuredDataProductProvider implements StructuredDataProviderInterface
{
private array $data = [];
private \Context $context;
public function __construct(\Context $context)
{
$this->context = $context;
}
private function provideProductCommentsDataIfModuleEnabled(): void
{
$commentsData = [];
if (\Module::isEnabled('productcomments')) {
$productCommentRepository = $this->context->controller->getContainer()->get('product_comment_repository');
$commentsModerate = (bool) \Configuration::get('PRODUCT_COMMENTS_MODERATE');
$commentsNb = $productCommentRepository->getCommentsNumber($this->data['id'], $commentsModerate);
if ($commentsNb > 0) {
$averageGrade = $productCommentRepository->getAverageGrade($this->data['id'], $commentsModerate);
$reviewsData = $productCommentRepository->paginate($this->data['id'], 1, 50, $commentsModerate); // get 50 reviews
$commentsData = [
'averageGrade' => $averageGrade,
'commentsNb' => $commentsNb,
'reviews' => $reviewsData,
];
}
}
$this->data['productRating'] = $commentsData;
}
public function getProductData(): void
{
$this->data = $this->context->controller->getTemplateVarProduct()->jsonSerialize();
}
public function getData(): array
{
$this->getProductData();
$this->provideProductCommentsDataIfModuleEnabled();
return $this->data;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Provider;
interface StructuredDataProviderInterface
{
/**
* Provide data
*
* @return array
*/
public function getData();
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Provider;
class StructuredDataShopProvider implements StructuredDataProviderInterface
{
protected \Context $context;
public function __construct(\Context $context)
{
$this->context = $context;
}
public function getData(): array
{
return $this->context->smarty->getTemplateVars('shop');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData\Provider;
class StructuredDataWebsiteProvider implements StructuredDataProviderInterface
{
protected \Context $context;
public function __construct(\Context $context)
{
$this->context = $context;
}
public function getData(): array
{
return $this->context->smarty->getTemplateVars('shop');
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData;
class ShopStructuredData extends AbstractStructuredData implements StructuredDataInterface
{
public function getStructuredDataType(): string
{
return 'shop';
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData;
interface StructuredDataInterface
{
/**
* Return formatted json data
*
* @return string
*/
public function getFormattedData(): string;
public function getStructuredDataType(): string;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\StructuredData;
class WebsiteStructuredData extends AbstractStructuredData implements StructuredDataInterface
{
public function getStructuredDataType(): string
{
return 'website';
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\ThemeAssets;
use Symfony\Component\Yaml\Yaml;
class ThemeAssetConfigProvider
{
/**
* @var bool
*/
private $fileContentRead = false;
/**
* @var array
*/
private $fileParsed = [];
/**
* @var string
*/
public $themeAssetsFileDir;
public function __construct($themeDir)
{
$this->themeAssetsFileDir = $themeDir . 'config/assets.yml';
}
public function getFileParsed(): array
{
if (!$this->fileContentRead) {
if (file_exists($this->themeAssetsFileDir)) {
$this->fileParsed = Yaml::parse(file_get_contents($this->themeAssetsFileDir));
}
$this->fileContentRead = true;
}
return $this->fileParsed;
}
public function getCssAssets(): array
{
$cssAssets = [];
if (!empty($this->getFileParsed()['css'])) {
$cssAssets = $this->getFileParsed()['css'];
}
return $cssAssets;
}
public function getJsAssets(): array
{
$jsAssets = [];
if (!empty($this->getFileParsed()['js'])) {
$jsAssets = $this->getFileParsed()['js'];
}
return $jsAssets;
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\ThemeAssets;
class ThemeAssetsRegister
{
/**
* @var ThemeAssetConfigProvider
*/
private $assetsDataProvider;
/**
* @var string
*/
private $currentPageName;
/**
* @var string
*/
private $themeName;
/**
* @var array
*/
private $cssAssets = [];
/**
* @var array
*/
private $jsAssets = [];
public function __construct(ThemeAssetConfigProvider $assetsDataProvider, \Context $context)
{
$this->assetsDataProvider = $assetsDataProvider;
$this->context = $context;
$this->themeName = $this->context->shop->theme->getName();
$this->currentPageName = $this->context->controller->getPageName();
$this->themePath = 'themes/' . $this->themeName . '/assets/';
$this->cssAssets = $assetsDataProvider->getCssAssets();
$this->jsAssets = $assetsDataProvider->getJsAssets();
}
private function getFilteredCssAssetsByPage(): array
{
return $this->filterAssetsArrayByPage($this->cssAssets);
}
private function getFilteredJsAssetsByPage(): array
{
return $this->filterAssetsArrayByPage($this->jsAssets);
}
private function filterAssetsArrayByPage($assetsArray): array
{
$pageName = $this->currentPageName;
return array_filter($assetsArray, function ($asset) use ($pageName) {
if (empty($asset['include'])) {
return true;
}
if (in_array($pageName, $asset['include'])) {
return true;
}
foreach ($asset['include'] as $matchType) {
$regex = str_replace(
['\*'],
['.*', '.'],
preg_quote($matchType)
);
if (preg_match('/^' . $regex . '$/is', $pageName)) {
return true;
}
}
return false;
});
}
public function registerThemeAssets(): void
{
$this->registerJsAssets();
$this->registerCssAssets();
}
public function registerJsAssets(): void
{
$assetsToRegister = $this->getFilteredJsAssetsByPage();
$default_params = [
'position' => \AbstractAssetManager::DEFAULT_JS_POSITION,
'priority' => \AbstractAssetManager::DEFAULT_PRIORITY,
'inline' => false,
'attributes' => null,
'server' => 'local',
];
foreach ($assetsToRegister as $id => $asset) {
$params = array_merge($default_params, $asset);
$file = $params['server'] === 'local' ? $this->themePath . 'js/' . $asset['fileName'] : $asset['fileName'];
$this->context->controller->registerJavascript(
'theme-' . $id,
$file,
[
'position' => $params['position'],
'priority' => $params['priority'],
'inline' => $params['inline'],
'attributes' => $params['attributes'],
'server' => $params['server'],
]
);
}
}
public function registerCssAssets(): void
{
$assetsToRegister = $this->getFilteredCssAssetsByPage();
$default_params = [
'media' => \AbstractAssetManager::DEFAULT_MEDIA,
'priority' => \AbstractAssetManager::DEFAULT_PRIORITY,
'inline' => false,
'server' => 'local',
];
foreach ($assetsToRegister as $id => $asset) {
$params = array_merge($default_params, $asset);
$file = $params['server'] === 'local' ? $this->themePath . 'css/' . $asset['fileName'] : $asset['fileName'];
$this->context->controller->registerStylesheet(
'theme-' . $id,
$file,
[
'media' => $params['media'],
'priority' => $params['priority'],
'server' => $params['server'],
]
);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Webp;
class RelatedImageFileFinder
{
protected $allowedImagesExtensions = ['jpg', 'png', 'jpeg'];
public function setAllowedImagesExtensions($allowedImagesExtensions)
{
$this->allowedImagesExtensions = $allowedImagesExtensions;
return $this;
}
public function getAllowedImagesExtensions()
{
return $this->allowedImagesExtensions;
}
public function findFile($relatedFile)
{
$fileData = pathinfo($relatedFile);
$possibleFiles = [];
$extensions = $this->getAllowedImagesExtensions();
foreach ($extensions as $ext) {
$possibleFiles[] = $fileData['dirname'] . '/' . $fileData['filename'] . '.' . $ext;
}
foreach ($possibleFiles as $file) {
if (file_exists($file)) {
return $file;
}
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Webp;
use WebPConvert\Convert\ConverterFactory;
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\ConverterNotFoundException;
use WebPConvert\Convert\Exceptions\ConversionFailedException;
class WebpConvertLibraries
{
protected $converters = [
'cwebp' => ['label' => 'Cwebp binary'],
'vips' => ['label' => 'Vips PHP extension'],
'imagick' => ['label' => 'Imagick PHP extension'],
'gmagick' => ['label' => 'Gmagick PHP extension'],
'imagemagick' => ['label' => 'Imagemagick binary'],
'graphicsmagick' => ['label' => 'Graphicsmagick binary (gm)'],
'gd' => ['label' => 'Gd PHP extension'],
// NOT SUPPORTED
// 'ewww' => ['label' => 'EWWW cloud service'],
];
protected $exampleImgFile = _PS_MODULE_DIR_ . 'is_themecore/views/img/example.jpg';
protected $exampleImgFileDesc = _PS_MODULE_DIR_ . 'is_themecore/views/img/example.webp';
public function getConvertersList(): array
{
$converters = $this->converters;
foreach ($converters as $converterId => $converterOptions) {
$converters[$converterId]['id'] = $converterId;
try {
$converterInstance = ConverterFactory::makeConverter($converterId, $this->exampleImgFile, $this->exampleImgFileDesc, []);
$converterInstance->checkOperationality();
$converterInstance->doConvert();
$converters[$converterId]['disabled'] = false;
} catch (ConversionFailedException $conversionFailedException) {
$converters[$converterId]['disabled'] = true;
} catch (ConverterNotFoundException $converterNotFoundException) {
$converters[$converterId]['disabled'] = true;
}
}
return $converters;
}
public function getFirstAvailableConverter(): array
{
$list = $this->getConvertersList();
foreach ($list as $converter) {
if (!$converter['disabled']) {
return $converter;
}
}
return [];
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Webp;
use Symfony\Component\Finder\Finder;
class WebpFilesEraser
{
private $query = '';
private $finder;
private $files;
private $excludeList = ['node_modules', 'vendor', 'app', 'var', 'classes', 'controllers', 'download'];
private $filesCount = 0;
public function __construct()
{
$this->finder = new Finder();
}
public function setQuery($query)
{
$this->query = $query;
return $this;
}
public function getQuery()
{
return $this->query;
}
public function setExcludeList(array $excludeList)
{
$this->excludeList = $excludeList;
return $this;
}
public function getExcludeList()
{
return $this->excludeList;
}
private function setFilesCount()
{
$this->filesCount = iterator_count($this->files);
return $this;
}
public function getFilesCount()
{
return $this->filesCount;
}
private function findFiles()
{
$this->files = $this->finder
->files()
->ignoreUnreadableDirs()
->in($this->query)
->exclude($this->excludeList)
->name('*.webp');
}
public function eraseFiles()
{
$this->findFiles();
$this->setFilesCount();
foreach ($this->files as $file) {
try {
unlink($file->getPathname());
} catch (\Throwable $error) {
throw $error;
}
}
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Webp;
use WebPConvert\WebPConvert;
class WebpGenerator
{
protected $fileFinder;
protected $destinationFile = '';
protected $converter = false;
protected $debugEnabled = false;
protected $sharpYuv = false;
protected $quality = 90;
public function __construct(RelatedImageFileFinder $fileFinder)
{
$this->fileFinder = $fileFinder;
}
public function setQuality($quality)
{
$this->quality = $quality;
return $this;
}
public function getQuality(): int
{
return $this->quality;
}
public function setConverter($converter)
{
$this->converter = $converter;
return $this;
}
public function getConverter(): string
{
return $this->converter;
}
public function setSharpYuv($sharpYuv)
{
$this->sharpYuv = $sharpYuv;
return $this;
}
public function getSharpYuv(): bool
{
return $this->sharpYuv;
}
public function setDebugEnabled($debugEnabled)
{
$this->debugEnabled = $debugEnabled;
return $this;
}
public function getDebugEnabled(): bool
{
return $this->debugEnabled;
}
public function setDestinationFile($destinationFile)
{
$this->destinationFile = $destinationFile;
return $this;
}
public function getDestinationFile(): string
{
return $this->destinationFile;
}
public function findRelatedFile()
{
return $this->fileFinder->findFile($this->getDestinationFile());
}
public function convertAndServe()
{
$sourceFile = $this->findRelatedFile();
WebPConvert::serveConverted($sourceFile, $this->destinationFile, [
'fail' => 'original',
'show-report' => $this->getDebugEnabled(),
'serve-image' => [
'headers' => [
'cache-control' => true,
'vary-accept' => true,
// other headers can be toggled...
],
'cache-control-header' => 'max-age=2',
],
'convert' => [
'stack-converters' => [$this->getConverter()],
'quality' => $this->getQuality(),
'encoding' => 'auto',
'sharp-yuv' => $this->getSharpYuv(),
],
]);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Oksydan\Module\IsThemeCore\Core\Webp;
class WebpPictureGenerator
{
private $allowedExtensions = ['png', 'jpg', 'jpeg'];
protected $content = '';
private $doc;
public function __construct($content)
{
$this->content = $content;
$this->doc = new \DOMDocument();
}
public function loadContent()
{
$this->doc->loadHTML('<?xml encoding="utf-8" ?>' . $this->content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
return $this;
}
public function generatePictureTags(): void
{
$images = $this->doc->getElementsByTagName('img');
if (0 === count($images)) {
return;
}
foreach ($images as $image) {
if ($image->hasAttribute('data-external-url')) {
continue;
}
$this->generatePictureTagFromImg($image);
}
$this->content = $this->doc->saveHTML();
$this->content = str_replace('<?xml encoding="utf-8" ?>', '', $this->content);
}
private function generatePictureTagFromImg($image)
{
$lazyLoad = !empty($params['lazyload']) ? $params['lazyload'] : (bool) preg_match('/' . implode('|', ['lazyload', 'swiper-lazy']) . '/i', $image->ownerDocument->saveHTML($image));
$srcAttributePrefix = $lazyLoad ? 'data-' : '';
$containSrcset = $image->hasAttribute($srcAttributePrefix . 'srcset');
$srcAttribute = $srcAttributePrefix . ($containSrcset ? 'srcset' : 'src');
$src = $image->getAttribute($srcAttribute);
$rawSrcArray = explode(',', $src);
$imageSrcArray = [];
foreach ($rawSrcArray as $rawSrc) {
$srcWithMediaArray = explode(' ', $rawSrc);
$srcWithMediaArray = array_values(array_filter($srcWithMediaArray, function ($elem) {
return !empty($elem);
}));
$imageSrcArray[] = [
'file' => $srcWithMediaArray[0] ?? null,
'media' => $srcWithMediaArray[1] ?? null,
'ext' => isset($srcWithMediaArray[0]) ? pathinfo($srcWithMediaArray[0], PATHINFO_EXTENSION) : null,
];
}
$picture = $this->doc->createElement('picture');
$pict_clone = $picture->cloneNode();
$image->parentNode->replaceChild($pict_clone, $image);
$pict_clone->appendChild($image);
$source = $this->doc->createElement('source');
$source->setAttribute('type', 'image/webp');
$sourceWebp = '';
$lastKey = array_key_last($imageSrcArray);
foreach ($imageSrcArray as $key => $imageSrc) {
$ext = explode('?', $imageSrc['ext']);
$ext = $ext[0] ?? null;
if (!in_array($ext, $this->allowedExtensions)) {
continue;
}
$newWebpSrc = str_replace('.' . $imageSrc['ext'], '.webp', $imageSrc['file']);
$sourceWebp .= $newWebpSrc . ($imageSrc['media'] ? ' ' . $imageSrc['media'] : '');
if ($key != $lastKey) {
$sourceWebp .= ', ';
}
}
if ($sourceWebp) {
$source->setAttribute($lazyLoad ? 'data-srcset' : 'srcset', $sourceWebp);
$src_clone = $source->cloneNode();
$image->parentNode->replaceChild($src_clone, $image);
$src_clone->appendChild($image);
}
}
public function getContent(): string
{
return $this->content;
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,36 @@
<?php
namespace Oksydan\Module\IsThemeCore\Form\ChoiceProvider;
use Oksydan\Module\IsThemeCore\Core\ListingDisplay\ThemeListDisplay;
use PrestaShop\PrestaShop\Core\Form\FormChoiceProviderInterface;
class ListDisplayChoiceProvider implements FormChoiceProviderInterface
{
/**
* @var ThemeListDisplay
*/
protected $themeListDisplay;
/**
* @param ThemeListDisplay $themeListDisplay
*/
public function __construct(ThemeListDisplay $themeListDisplay)
{
$this->themeListDisplay = $themeListDisplay;
}
/**
* @return array
*/
public function getChoices(): array
{
$choices = [];
foreach ($this->themeListDisplay->getDisplayOptions() as $display) {
$choices[$display] = $display;
}
return $choices;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Oksydan\Module\IsThemeCore\Form\ChoiceProvider;
use Oksydan\Module\IsThemeCore\Core\Webp\WebpConvertLibraries;
use PrestaShop\PrestaShop\Core\Form\FormChoiceProviderInterface;
class WebpLibraryChoiceProvider implements FormChoiceProviderInterface
{
/**
* @var WebpConvertLibraries
*/
protected $webpConvertLibraries;
/**
* @param WebpConvertLibraries $webpConvertLibraries
*/
public function __construct(WebpConvertLibraries $webpConvertLibraries)
{
$this->webpConvertLibraries = $webpConvertLibraries;
}
/**
* @return array
*/
public function getChoices(): array
{
$choices = [];
foreach ($this->webpConvertLibraries->getConvertersList() as $converter) {
$choices[$converter['label']] = $converter['id'];
}
return $choices;
}
/**
* @return array
*/
public function getChoicesFull(): array
{
$choices = [];
foreach ($this->webpConvertLibraries->getConvertersList() as $converter) {
$choices[$converter['id']] = $converter;
}
return $choices;
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Oksydan\Module\IsThemeCore\Form\Settings;
use PrestaShop\PrestaShop\Core\Configuration\AbstractMultistoreConfiguration;
use PrestaShopBundle\Service\Form\MultistoreCheckboxEnabler;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Configuration is used to save data to configuration table and retrieve from it
*/
final class GeneralConfiguration extends AbstractMultistoreConfiguration
{
private const CONFIGURATION_FIELDS = [
'list_display_settings',
'early_hints',
'preload_css',
'load_party_town',
'debug_party_town',
];
/**
* @var string
*/
public const THEMECORE_DISPLAY_LIST = 'THEMECORE_DISPLAY_LIST';
public const THEMECORE_EARLY_HINTS = 'THEMECORE_EARLY_HINTS';
public const THEMECORE_PRELOAD_CSS = 'THEMECORE_PRELOAD_CSS';
public const THEMECORE_LOAD_PARTY_TOWN = 'THEMECORE_LOAD_PARTY_TOWN';
public const THEMECORE_DEBUG_PARTY_TOWN = 'THEMECORE_DEBUG_PARTY_TOWN';
/**
* @var array<string, string>
*/
private array $fields = [
'list_display_settings' => self::THEMECORE_DISPLAY_LIST,
'early_hints' => self::THEMECORE_EARLY_HINTS,
'preload_css' => self::THEMECORE_PRELOAD_CSS,
'load_party_town' => self::THEMECORE_LOAD_PARTY_TOWN,
'debug_party_town' => self::THEMECORE_DEBUG_PARTY_TOWN,
];
/**
* {@inheritdoc}
*
* @return array<string, mixed>
*/
public function getConfiguration(): array
{
$configurationValues = [];
foreach ($this->fields as $field => $configurationKey) {
$configurationValues[$field] = $this->configuration->get($configurationKey);
}
return $configurationValues;
}
/**
* {@inheritdoc}
*
* @param array<string, mixed> $configuration
*
* @return array<int, array<string, mixed>>
*/
public function updateConfiguration(array $configuration): array
{
$errors = [];
if (!$this->validateConfiguration($configuration)) {
$errors[] = [
'key' => 'Invalid configuration',
'parameters' => [],
'domain' => 'Admin.Notifications.Warning',
];
} else {
$shopConstraint = $this->getShopConstraint();
try {
foreach ($this->fields as $field => $configurationKey) {
$this->updateConfigurationValue($configurationKey, $field, $configuration, $shopConstraint);
}
} catch (\Exception $exception) {
$errors[] = [
'key' => $exception->getMessage(),
'parameters' => [],
'domain' => 'Admin.Notifications.Warning',
];
}
}
return $errors;
}
/**
* Ensure the parameters passed are valid.
*
* @param array<string, mixed> $configuration
*
* @return bool Returns true if no exception are thrown
*/
public function validateConfiguration(array $configuration): bool
{
foreach ($this->fields as $field => $configurationKey) {
$multistoreKey = MultistoreCheckboxEnabler::MULTISTORE_FIELD_PREFIX . $field;
$this->fields[$multistoreKey] = '';
}
foreach ($configuration as $key => $value) {
if (!key_exists($key, $this->fields)) {
return false;
}
}
return true;
}
/**
* @return OptionsResolver
*/
protected function buildResolver(): OptionsResolver
{
return (new OptionsResolver())
->setDefined(self::CONFIGURATION_FIELDS)
->setAllowedTypes('list_display_settings', ['string', 'null'])
->setAllowedTypes('early_hints', 'bool')
->setAllowedTypes('preload_css', 'bool')
->setAllowedTypes('load_party_town', 'bool')
->setAllowedTypes('debug_party_town', 'bool');
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Oksydan\Module\IsThemeCore\Form\Settings;
use PrestaShop\PrestaShop\Core\Configuration\DataConfigurationInterface;
use PrestaShop\PrestaShop\Core\Form\FormDataProviderInterface;
/**
* Class GeneralFormDataProvider
*/
class GeneralFormDataProvider implements FormDataProviderInterface
{
/**
* @var DataConfigurationInterface
*/
private $generalConfiguration;
/**
* @param DataConfigurationInterface $generalConfiguration
*/
public function __construct(DataConfigurationInterface $generalConfiguration)
{
$this->generalConfiguration = $generalConfiguration;
}
/**
* {@inheritdoc}
*
* @return array<string, mixed> The form data as an associative array
*/
public function getData(): array
{
return $this->generalConfiguration->getConfiguration();
}
/**
* {@inheritdoc}
*
* @param array<string, mixed> $data
*
* @return array<int, array<string, mixed>> An array of errors messages if data can't persisted
*/
public function setData(array $data): array
{
return $this->generalConfiguration->updateConfiguration($data);
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Oksydan\Module\IsThemeCore\Form\Settings;
use PrestaShopBundle\Form\Admin\Type\MultistoreConfigurationType;
use PrestaShopBundle\Form\Admin\Type\SwitchType;
use PrestaShopBundle\Form\Admin\Type\TranslatorAwareType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Translation\TranslatorInterface;
class GeneralType extends TranslatorAwareType
{
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var array
*/
private $displayListChoices;
/**
* GeneralType constructor.
*
* @param TranslatorInterface $translator
* @param array $locales
* @param array $displayListChoices
*/
public function __construct(
TranslatorInterface $translator,
array $locales,
array $displayListChoices
) {
parent::__construct($translator, $locales);
$this->displayListChoices = $displayListChoices;
}
/**
* {@inheritdoc}
*
* @param FormBuilderInterface<string, mixed> $builder
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('list_display_settings',
ChoiceType::class,
[
'choices' => $this->displayListChoices,
'label' => $this->trans('Default list display', 'Modules.isthemecore.Admin'),
'multistore_configuration_key' => GeneralConfiguration::THEMECORE_DISPLAY_LIST,
]
)
->add('early_hints',
SwitchType::class,
[
'required' => false,
'label' => $this->trans('Early hints (HTTP 103) enabled', 'Modules.isthemecore.Admin'),
'help' => $this->trans('Cloudflare CDN, Early hints option have to enabled. <a href="https://developers.cloudflare.com/cache/about/early-hints/">More information</a>', 'Modules.isthemecore.Admin'),
'multistore_configuration_key' => GeneralConfiguration::THEMECORE_EARLY_HINTS,
]
)
->add('preload_css',
SwitchType::class,
[
'required' => false,
'label' => $this->trans('Preload css enabled, only working with CCC for css option enabled', 'Modules.isthemecore.Admin'),
'multistore_configuration_key' => GeneralConfiguration::THEMECORE_PRELOAD_CSS,
]
)
->add('load_party_town',
SwitchType::class,
[
'required' => false,
'label' => $this->trans('Load partytown script', 'Modules.isthemecore.Admin'),
'help' => $this->trans('Be aware that partytown is still beta. Make sure that everything is working as expected before pushing it to your production store.', 'Modules.isthemecore.Admin'),
'multistore_configuration_key' => GeneralConfiguration::THEMECORE_LOAD_PARTY_TOWN,
]
)
->add('debug_party_town',
SwitchType::class,
[
'required' => false,
'label' => $this->trans('Enable debug mode for partytown', 'Modules.isthemecore.Admin'),
'multistore_configuration_key' => GeneralConfiguration::THEMECORE_DEBUG_PARTY_TOWN,
]
);
}
/**
* {@inheritdoc}
*
* @see MultistoreConfigurationTypeExtension
*/
public function getParent(): string
{
return MultistoreConfigurationType::class;
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Oksydan\Module\IsThemeCore\Form\Settings;
use PrestaShop\PrestaShop\Adapter\Configuration;
use PrestaShop\PrestaShop\Core\Configuration\DataConfigurationInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Configuration is used to save data to configuration table and retrieve from it
*/
final class WebpConfiguration implements DataConfigurationInterface
{
private const CONFIGURATION_FIELDS = [
'webp_enabled',
'webp_quality',
'webp_converter',
'webp_sharpyuv',
];
/**
* @var string
*/
public const THEMECORE_WEBP_ENABLED = 'THEMECORE_WEBP_ENABLED';
public const THEMECORE_WEBP_QUALITY = 'THEMECORE_WEBP_QUALITY';
public const THEMECORE_WEBP_CONVERTER = 'THEMECORE_WEBP_CONVERTER';
public const THEMECORE_WEBP_SHARPYUV = 'THEMECORE_WEBP_SHARPYUV';
/**
* @var array<string, string>
*/
private $fields = [
'webp_enabled' => self::THEMECORE_WEBP_ENABLED,
'webp_quality' => self::THEMECORE_WEBP_QUALITY,
'webp_converter' => self::THEMECORE_WEBP_CONVERTER,
'webp_sharpyuv' => self::THEMECORE_WEBP_SHARPYUV,
];
/**
* @var Configuration
*/
protected $configuration;
public function __construct(Configuration $configuration)
{
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*
* @return array<string, mixed>
*/
public function getConfiguration(): array
{
$configurationValues = [];
foreach ($this->fields as $field => $configurationKey) {
$configurationValues[$field] = $this->configuration->get($configurationKey);
}
return $configurationValues;
}
/**
* {@inheritdoc}
*
* @param array<string, mixed> $configuration
*
* @return array<int, array<string, mixed>>
*/
public function updateConfiguration(array $configuration): array
{
$errors = [];
if (!$this->validateConfiguration($configuration)) {
$errors[] = [
'key' => 'Invalid configuration',
'parameters' => [],
'domain' => 'Admin.Notifications.Warning',
];
} else {
try {
foreach ($this->fields as $field => $configurationKey) {
$this->configuration->set($configurationKey, $configuration[$field]);
}
} catch (\Exception $exception) {
$errors[] = [
'key' => $exception->getMessage(),
'parameters' => [],
'domain' => 'Admin.Notifications.Warning',
];
}
}
return $errors;
}
/**
* Ensure the parameters passed are valid.
*
* @param array<string, mixed> $configuration
*
* @return bool Returns true if no exception are thrown
*/
public function validateConfiguration(array $configuration): bool
{
foreach ($configuration as $key => $value) {
if (!key_exists($key, $this->fields)) {
return false;
}
}
return true;
}
/**
* @return OptionsResolver
*/
protected function buildResolver(): OptionsResolver
{
return (new OptionsResolver())
->setDefined(self::CONFIGURATION_FIELDS)
->setAllowedTypes('webp_enabled', 'bool')
->setAllowedTypes('webp_quality', 'string')
->setAllowedTypes('webp_converter', 'string')
->setAllowedTypes('webp_sharpyuv', 'bool');
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Oksydan\Module\IsThemeCore\Form\Settings;
use PrestaShop\PrestaShop\Core\Configuration\DataConfigurationInterface;
use PrestaShop\PrestaShop\Core\Form\FormDataProviderInterface;
/**
* Class WebpFormDataProvider
*/
class WebpFormDataProvider implements FormDataProviderInterface
{
/**
* @var DataConfigurationInterface
*/
private $webpConfiguration;
/**
* @param DataConfigurationInterface $webpConfiguration
*/
public function __construct(DataConfigurationInterface $webpConfiguration)
{
$this->webpConfiguration = $webpConfiguration;
}
/**
* {@inheritdoc}
*
* @return array<string, mixed> The form data as an associative array
*/
public function getData(): array
{
return $this->webpConfiguration->getConfiguration();
}
/**
* {@inheritdoc}
*
* @param array<string, mixed> $data
*
* @return array<int, array<string, mixed>> An array of errors messages if data can't persisted
*/
public function setData(array $data): array
{
return $this->webpConfiguration->updateConfiguration($data);
}
}

View File

@@ -0,0 +1,247 @@
<?php
declare(strict_types=1);
namespace Oksydan\Module\IsThemeCore\Form\Settings;
use PrestaShopBundle\Form\Admin\Type\IconButtonType;
use PrestaShopBundle\Form\Admin\Type\MultistoreConfigurationType;
use PrestaShopBundle\Form\Admin\Type\SwitchType;
use PrestaShopBundle\Form\Admin\Type\TranslatorAwareType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Range;
class WebpType extends TranslatorAwareType
{
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var array
*/
private $convertersList;
/**
* @var array
*/
private $convertersListFull;
/**
* @var RouterInterface
*/
private $router;
/**
* WebpType constructor.
*
* @param TranslatorInterface $translator
* @param array $locales
* @param array $convertersList
* @param array $convertersListFull
*/
public function __construct(
TranslatorInterface $translator,
array $locales,
array $convertersList,
array $convertersListFull,
RouterInterface $router
) {
parent::__construct($translator, $locales);
$this->convertersList = $convertersList;
$this->convertersListFull = $convertersListFull;
$this->router = $router;
}
private function allWebpConvertersDisabled(): bool
{
return array_reduce($this->convertersListFull, function ($carry, $item) {
return $carry && $item['disabled'];
}, true);
}
/**
* {@inheritdoc}
*
* @param FormBuilderInterface<string, mixed> $builder
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$webpDisabled = $this->allWebpConvertersDisabled();
$extraAttributes = [];
if ($webpDisabled) {
$extraAttributes = [
'alert_message' => $this->trans('Webp converters not available contact your admin or hosting provider.', 'Modules.isthemecore.Admin'),
'alert_type' => 'danger',
'alert_position' => 'append',
];
}
$builder
->add('webp_enabled',
SwitchType::class,
array_merge(
[
'required' => false,
'label' => $this->trans('Enable WEBP', 'Modules.isthemecore.Admin'),
'disabled' => $webpDisabled,
],
$extraAttributes
)
)
->add('webp_sharpyuv',
SwitchType::class,
[
'required' => false,
'label' => $this->trans('Enable better RGB->YUV color conversion', 'Modules.isthemecore.Admin'),
'disabled' => $webpDisabled,
]
)
->add('webp_quality',
TextType::class,
[
'required' => false,
'label' => $this->trans('Webp quality', 'Modules.isthemecore.Admin'),
'help' => $this->trans('Range 1-100', 'Modules.isthemecore.Admin'),
'disabled' => $webpDisabled,
'constraints' => [
$this->getRangeConstraint(1, 100),
$this->getNotBlankConstraint(),
],
]
)
->add('webp_converter',
ChoiceType::class,
[
'choices' => $this->convertersList,
'label' => $this->trans('Webp converter options', 'Modules.isthemecore.Admin'),
'disabled' => $webpDisabled,
'expanded' => true,
'multiple' => false,
'choice_attr' => function ($choice) {
return ['disabled' => $this->convertersListFull[$choice]['disabled']];
},
'choice_label' => function ($choice) {
return $this->convertersListFull[$choice]['label'] . ($this->convertersListFull[$choice]['disabled'] ? '<span class="ml-1 badge badge-danger">' . $this->trans('not available', 'Modules.isthemecore.Admin') . '</span>' : '');
},
]
)
->add('erase_all_webp', IconButtonType::class, [
'label' => $this->trans('Erase all webp images', 'Modules.isthemecore.Admin'),
'type' => 'link',
'icon' => 'delete',
'attr' => [
'class' => 'btn-danger',
'href' => $this->router->generate(
'is_themecore_module_settings_webp_erase_all',
[
'type' => 'all',
]
),
],
])
->add('erase_product_webp', IconButtonType::class, [
'label' => $this->trans('Erase all product webp images', 'Modules.isthemecore.Admin'),
'type' => 'link',
'icon' => 'delete',
'attr' => [
'class' => 'btn-danger',
'href' => $this->router->generate(
'is_themecore_module_settings_webp_erase_all',
[
'type' => 'product',
]
),
],
])
->add('erase_modules_webp', IconButtonType::class, [
'label' => $this->trans('Erase all modules webp images', 'Modules.isthemecore.Admin'),
'type' => 'link',
'icon' => 'delete',
'attr' => [
'class' => 'btn-danger',
'href' => $this->router->generate(
'is_themecore_module_settings_webp_erase_all',
[
'type' => 'module',
]
),
],
])
->add('erase_cms_webp', IconButtonType::class, [
'label' => $this->trans('Erase all CMS webp images', 'Modules.isthemecore.Admin'),
'type' => 'link',
'icon' => 'delete',
'attr' => [
'class' => 'btn-danger',
'href' => $this->router->generate(
'is_themecore_module_settings_webp_erase_all',
[
'type' => 'cms',
]
),
],
])
->add('erase_themes_webp', IconButtonType::class, [
'label' => $this->trans('Erase all themes webp images', 'Modules.isthemecore.Admin'),
'type' => 'link',
'icon' => 'delete',
'attr' => [
'class' => 'btn-danger',
'href' => $this->router->generate(
'is_themecore_module_settings_webp_erase_all',
[
'type' => 'themes',
]
),
],
]);
}
/**
* {@inheritdoc}
*
* @see MultistoreConfigurationTypeExtension
*/
public function getParent(): string
{
return MultistoreConfigurationType::class;
}
/**
* @return NotBlank
*/
private function getNotBlankConstraint()
{
return new NotBlank([
'message' => $this->trans('This field cannot be empty.', 'Modules.isthemecore.Admin'),
]);
}
/**
* @return Range
*/
private function getRangeConstraint(int $min = 1, int $max = 100)
{
return new Range([
'min' => $min,
'max' => $max,
'invalidMessage' => $this->trans(
'This field value have to be between %min% and %max%.',
'Modules.isthemecore.Admin',
[
'%min%' => $min,
'%max%' => $max,
]
),
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,25 @@
<?php
namespace Oksydan\Module\IsThemeCore\Hook;
abstract class AbstractHook
{
public const HOOK_LIST = [];
protected $module;
protected $context;
public function __construct(\Is_themecore $module)
{
$this->module = $module;
$this->context = \Context::getContext();
}
/**
* @return array
*/
public function getAvailableHooks()
{
return static::HOOK_LIST;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Oksydan\Module\IsThemeCore\Hook;
use Oksydan\Module\IsThemeCore\Core\ThemeAssets\ThemeAssetConfigProvider;
use Oksydan\Module\IsThemeCore\Core\ThemeAssets\ThemeAssetsRegister;
class Assets extends AbstractHook
{
public const HOOK_LIST = [
'actionFrontControllerSetMedia',
'actionProductSearchAfter',
];
/**
* Removing ps_faceted search module assets
*/
public function hookActionProductSearchAfter(): void
{
$this->context->controller->unregisterJavascript('facetedsearch_front');
$this->context->controller->unregisterStylesheet('facetedsearch_front');
$needsJQueryUi = \Module::isEnabled('pm_advancedsearch4') && $this->context->controller instanceof \ProductListingFrontController;
if (!$needsJQueryUi) {
$this->context->controller->unregisterJavascript('jquery-ui');
$this->context->controller->unregisterStylesheet('jquery-ui');
$this->context->controller->unregisterStylesheet('jquery-ui-theme');
}
}
public function hookActionFrontControllerSetMedia()
{
$assetsRegister = new ThemeAssetsRegister(
new ThemeAssetConfigProvider(_PS_THEME_DIR_),
$this->context
);
$assetsRegister->registerThemeAssets();
\Media::addJsDef([
'listDisplayAjaxUrl' => $this->context->link->getModuleLink($this->module->name, 'ajaxTheme'),
]);
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Oksydan\Module\IsThemeCore\Hook;
use Oksydan\Module\IsThemeCore\Core\Breadcrumbs\ThemeBreadcrumbs;
use Oksydan\Module\IsThemeCore\Core\ListingDisplay\ThemeListDisplay;
use Oksydan\Module\IsThemeCore\Core\Partytown\PartytownScript;
use Oksydan\Module\IsThemeCore\Core\Partytown\PartytownScriptUriResolver;
use Oksydan\Module\IsThemeCore\Core\StructuredData\BreadcrumbStructuredData;
use Oksydan\Module\IsThemeCore\Core\StructuredData\ProductStructuredData;
use Oksydan\Module\IsThemeCore\Core\StructuredData\ShopStructuredData;
use Oksydan\Module\IsThemeCore\Core\StructuredData\StructuredDataInterface;
use Oksydan\Module\IsThemeCore\Core\StructuredData\WebsiteStructuredData;
use Oksydan\Module\IsThemeCore\Form\Settings\GeneralConfiguration;
use Oksydan\Module\IsThemeCore\Form\Settings\WebpConfiguration;
class Header extends AbstractHook
{
public const HOOK_LIST = [
'actionFrontControllerInitBefore',
'displayHeader',
];
public function hookActionFrontControllerInitBefore(): void
{
$themeListDisplay = new ThemeListDisplay();
$this->context->smarty->assign([
'listingDisplayType' => $themeListDisplay->getDisplay(),
'preloadCss' => \Configuration::get(GeneralConfiguration::THEMECORE_PRELOAD_CSS),
'webpEnabled' => \Configuration::get(WebpConfiguration::THEMECORE_WEBP_ENABLED),
'loadPartytown' => (bool) \Configuration::get(GeneralConfiguration::THEMECORE_LOAD_PARTY_TOWN),
'debugPartytown' => (bool) \Configuration::get(GeneralConfiguration::THEMECORE_DEBUG_PARTY_TOWN),
]);
}
public function hookDisplayHeader(): string
{
$themeListDisplay = new ThemeListDisplay();
$breadcrumbs = (new ThemeBreadcrumbs())->getBreadcrumb();
if ($breadcrumbs['count']) {
$this->context->smarty->assign([
'breadcrumb' => $breadcrumbs,
]);
}
$this->context->smarty->assign([
'jsonData' => $this->getStructuredData(),
'partytownScript' => $this->getPartytownScript(),
'partytownScriptUri' => $this->getPartytownScriptUri(),
]);
return $this->module->fetch('module:is_themecore/views/templates/hook/head.tpl');
}
private function getPartytownScriptUri(): string
{
try {
$uriResolver = $this->module->get(PartytownScriptUriResolver::class);
} catch (\Exception $e) {
$uriResolver = null;
}
if ($uriResolver) {
return $uriResolver->getScriptUri();
}
return '';
}
private function getPartytownScript(): string
{
try {
$partytownScript = $this->module->get(PartytownScript::class);
} catch (\Exception $e) {
$partytownScript = null;
}
if ($partytownScript instanceof PartytownScript) {
return $partytownScript->getScriptContent();
}
return '';
}
private function getStructuredData(): array
{
$dataArray = [];
if ($this->context->controller instanceof \ProductControllerCore && $this->context->controller->getProduct()->id !== null) {
try {
$productData = $this->module->get(ProductStructuredData::class);
} catch (\Exception $e) {
$productData = null;
}
if ($productData instanceof StructuredDataInterface) {
$dataArray[] = $productData->getFormattedData();
}
}
try {
$breadcrumbData = $this->module->get(BreadcrumbStructuredData::class);
} catch (\Exception $e) {
$breadcrumbData = null;
}
if ($breadcrumbData instanceof StructuredDataInterface) {
$dataArray[] = $breadcrumbData->getFormattedData();
}
try {
$shopData = $this->module->get(ShopStructuredData::class);
} catch (\Exception $e) {
$shopData = null;
}
if ($shopData instanceof StructuredDataInterface) {
$dataArray[] = $shopData->getFormattedData();
}
if ($this->context->controller->getPageName() === 'index') {
try {
$website = $this->module->get(WebsiteStructuredData::class);
} catch (\Exception $e) {
$website = null;
}
if ($website instanceof StructuredDataInterface) {
$dataArray[] = $website->getFormattedData();
}
}
return $dataArray;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Oksydan\Module\IsThemeCore\Hook;
class Htaccess extends AbstractHook
{
public const HOOK_LIST = [
'actionHtaccessCreate',
'objectShopUrlAddAfter',
'objectShopUrlUpdateAfter',
'objectShopUrlDeleteAfter',
];
public function hookActionHtaccessCreate()
{
$generator = $this->module->get('oksydan.module.is_themecore.core.htaccess.htaccess_generator');
$generator->generate();
$generator->writeFile();
}
public function hookObjectShopUrlAddAfter()
{
$this->hookActionHtaccessCreate();
}
public function hookObjectShopUrlUpdateAfter()
{
$this->hookActionHtaccessCreate();
}
public function hookObjectShopUrlDeleteAfter()
{
$this->hookActionHtaccessCreate();
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Oksydan\Module\IsThemeCore\Hook;
use Oksydan\Module\IsThemeCore\Form\Settings\GeneralConfiguration;
use Oksydan\Module\IsThemeCore\Form\Settings\WebpConfiguration;
class HtmlOutput extends AbstractHook
{
public const HOOK_LIST = [
'actionOutputHTMLBefore',
];
public const REL_LIST = [
'preload',
'preconnect',
];
public const PRELOAD_TYPES_TO_EARLY_HINT = [
'image',
'stylesheet',
// 'font', //disabled for now causing higher LCP and weird FOUC
];
private $headers = [];
public function hookActionOutputHTMLBefore(array $params): void
{
$earlyHintsEnabled = \Configuration::get(GeneralConfiguration::THEMECORE_EARLY_HINTS, false);
$webpEnabled = \Configuration::get(WebpConfiguration::THEMECORE_WEBP_ENABLED, false);
if (!$earlyHintsEnabled && !$webpEnabled) {
return;
}
$preConfig = libxml_use_internal_errors(true);
$html = $params['html'];
$doc = new \DOMDocument();
$doc->loadHTML(
'<meta http-equiv="Content-Type" content="charset=utf-8">' . $html,
LIBXML_NOERROR | LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
$links = $doc->getElementsByTagName('link');
foreach ($links as $link) {
$rel = $link->hasAttribute('rel') ? $link->attributes->getNamedItem('rel')->nodeValue : false;
$as = $link->hasAttribute('as') ? $link->attributes->getNamedItem('as')->nodeValue : false;
if ($webpEnabled && $rel === 'preload' && $as === 'image') {
$newLink = $doc->createElement('link');
$src = urldecode($link->attributes->getNamedItem('href')->nodeValue);
$newLink->setAttribute('href', str_replace(['.png', '.jpg', '.jpeg'], '.webp', $src));
foreach ($link->attributes as $attribute) {
if ($attribute->nodeName !== 'href') {
$newLink->setAttribute($attribute->nodeName, $attribute->nodeValue);
}
}
$link->parentNode->replaceChild($newLink, $link);
}
if ($earlyHintsEnabled && in_array($rel, self::REL_LIST)) {
if (isset($newLink)) {
$link = $newLink;
unset($newLink);
}
switch ($rel) {
case 'preload':
$this->handlePreloadFromNodeElement($link);
break;
case 'preconnect':
$this->handlePreconnectFromNodeElement($link);
break;
}
}
}
if ($webpEnabled) {
$content = $doc->saveHTML();
$content = str_replace('<meta http-equiv="Content-Type" content="charset=utf-8">', '', $content);
$params['html'] = $content;
}
if (!empty($this->headers)) {
header('Link: ' . implode(', ', $this->headers));
}
libxml_use_internal_errors($preConfig);
}
private function handlePreloadFromNodeElement($nodeElement)
{
$preloadAs = $nodeElement->attributes->getNamedItem('as')->nodeValue;
if (in_array($preloadAs, self::PRELOAD_TYPES_TO_EARLY_HINT)) {
$url = $nodeElement->attributes->getNamedItem('href')->nodeValue;
$this->headers[] = "<$url>; rel=preload; as=$preloadAs";
}
}
private function handlePreconnectFromNodeElement($nodeElement)
{
$url = $nodeElement->attributes->getNamedItem('href')->nodeValue;
$this->headers[] = "<$url>; rel=preconnect";
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Oksydan\Module\IsThemeCore\Hook;
class Smarty extends AbstractHook
{
public const HOOK_LIST = [
'actionDispatcherBefore',
];
public function hookActionDispatcherBefore(): void
{
$this->context->smarty->registerPlugin('function', 'generateImagesSources', ['Oksydan\Module\IsThemeCore\Core\Smarty\SmartyHelperFunctions', 'generateImagesSources']);
$this->context->smarty->registerPlugin('function', 'generateImageSvgPlaceholder', ['Oksydan\Module\IsThemeCore\Core\Smarty\SmartyHelperFunctions', 'generateImageSvgPlaceholder']);
$this->context->smarty->registerPlugin('function', 'appendParamToUrl', ['Oksydan\Module\IsThemeCore\Core\Smarty\SmartyHelperFunctions', 'appendParamToUrl']);
$this->context->smarty->registerPlugin('block', 'images_block', ['Oksydan\Module\IsThemeCore\Core\Smarty\SmartyHelperFunctions', 'imagesBlock']);
$this->context->smarty->registerPlugin('block', 'cms_images_block', ['Oksydan\Module\IsThemeCore\Core\Smarty\SmartyHelperFunctions', 'cmsImagesBlock']);
$this->context->smarty->registerPlugin('block', 'display_mobile', ['Oksydan\Module\IsThemeCore\Core\Smarty\SmartyHelperFunctions', 'displayMobileBlock']);
$this->context->smarty->registerPlugin('block', 'display_desktop', ['Oksydan\Module\IsThemeCore\Core\Smarty\SmartyHelperFunctions', 'displayDesktopBlock']);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Oksydan\Module\IsThemeCore;
use Oksydan\Module\IsThemeCore\Hook\AbstractHook;
use Oksydan\Module\IsThemeCore\Hook\Assets;
use Oksydan\Module\IsThemeCore\Hook\Header;
use Oksydan\Module\IsThemeCore\Hook\Htaccess;
use Oksydan\Module\IsThemeCore\Hook\HtmlOutput;
use Oksydan\Module\IsThemeCore\Hook\Smarty;
class HookDispatcher
{
public const HOOK_CLASSES = [
Header::class,
Assets::class,
Smarty::class,
HtmlOutput::class,
Htaccess::class,
];
/**
* Hook instances.
*
* @var AbstractHook[]
*/
protected $hooks = [];
public function __construct(\Is_themecore $module)
{
foreach (static::HOOK_CLASSES as $hookClass) {
/** @var AbstractHook $hook */
$hook = new $hookClass($module);
$this->hooks[] = $hook;
}
}
/**
* Get available hooks
*
* @return string[]
*/
public function getAvailableHooks()
{
$availableHooks = [];
foreach ($this->hooks as $hook) {
$availableHooks = array_merge($availableHooks, $hook->getAvailableHooks());
}
return $availableHooks;
}
public function dispatch($hookName, array $params = [])
{
foreach ($this->hooks as $hook) {
if (method_exists($hook, $hookName)) {
return $hook->{$hookName}($params);
}
}
return false;
}
}

View File

@@ -0,0 +1,11 @@
<?php
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;