[11/06/2024] Les premières modifs + installation de quelques modules indispensables

This commit is contained in:
2024-06-11 14:57:59 +02:00
parent 5ac5653ae5
commit 77cf2c7cc6
1626 changed files with 171457 additions and 131 deletions

5
domokits/.gitignore vendored
View File

@@ -17,7 +17,6 @@
/templates/pdf/default
# Thelia config
/local/config
/local/setup
/local/media
/local/session
@@ -25,9 +24,6 @@
!local/media/images/store/thelia.svg
!local/media/images/store/banner.png
# Thelia modules
/local/modules/*
### Please add your dependancies here
###> symfony/framework-bundle ###
@@ -46,3 +42,4 @@
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
/log/

View File

@@ -18,7 +18,8 @@
"thelia/thelia-library-module": "^1.1.7",
"thelia/product-loop-attribute-filter-module": "~2.0.0",
"thelia/reset-password-module": "~1.0.1",
"thelia/re-captcha-module": "~3.0.1"
"thelia/re-captcha-module": "~3.0.1",
"stripe/stripe-php": "6.*"
},
"suggest": {
"vlopes/maintenance-module": "Add a way to put your site in maintenance mode",

View File

@@ -0,0 +1,7 @@
name: "Auto Release"
on:
push:
branches: [ master, main ]
jobs:
release:
uses: thelia-modules/ReusableWorkflow/.github/workflows/auto_release.yml@main

View File

@@ -0,0 +1,77 @@
<?php
/*************************************************************************************/
/* This file is part of the Thelia package. */
/* */
/* Copyright (c) OpenStudio */
/* email : dev@thelia.net */
/* web : http://www.thelia.net */
/* */
/* For the full copyright and license information, please view the LICENSE.txt */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace BetterSeo;
use BetterSeo\Model\BetterSeoQuery;
use Propel\Runtime\Connection\ConnectionInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator;
use Symfony\Component\Finder\Finder;
use Thelia\Install\Database;
use Thelia\Module\BaseModule;
class BetterSeo extends BaseModule
{
/** @var string */
const DOMAIN_NAME = 'betterseo.bo.default';
/**
* @param ConnectionInterface|null $con
* @throws \Propel\Runtime\Exception\PropelException
*/
public function postActivation(ConnectionInterface $con = null):void
{
if (!self::getConfigValue('is_initialized',null)){
$database = new Database($con);
$database->insertSql(null, [__DIR__ . "/Config/thelia.sql"]);
self::setConfigValue('is_initialized', 1);
}
}
public function update($currentVersion, $newVersion, ConnectionInterface $con = null):void
{
$sqlToExecute = [];
$finder = new Finder();
$sort = function (\SplFileInfo $a, \SplFileInfo $b) {
$a = strtolower(substr($a->getRelativePathname(), 0, -4));
$b = strtolower(substr($b->getRelativePathname(), 0, -4));
return version_compare($a, $b);
};
$files = $finder->name('*.sql')
->in(__DIR__ . "/Config/Update/")
->sort($sort);
foreach ($files as $file) {
if (version_compare($file->getFilename(), $currentVersion, ">")) {
$sqlToExecute[$file->getFilename()] = $file->getRealPath();
}
}
$database = new Database($con);
foreach ($sqlToExecute as $version => $sql) {
$database->insertSql(null, [$sql]);
}
}
/**
* Defines how services are loaded in your modules.
*/
public static function configureServices(ServicesConfigurator $servicesConfigurator): void
{
$servicesConfigurator->load(self::getModuleCode().'\\', __DIR__)
->exclude([THELIA_MODULE_DIR.ucfirst(self::getModuleCode()).'/I18n/*'])
->autowire(true)
->autoconfigure(true);
}
}

View File

@@ -0,0 +1 @@
ALTER TABLE `better_seo_i18n` ADD `h1` TEXT NOT NULL AFTER `canonical_field`;

View File

@@ -0,0 +1 @@
ALTER TABLE `better_seo_i18n` ADD `mesh_text_1` TEXT NOT NULL AFTER `h1`, ADD `mesh_url_1` TEXT NOT NULL AFTER `mesh_text_1`, ADD `mesh_text_2` TEXT NOT NULL AFTER `mesh_url_1`, ADD `mesh_url_2` TEXT NOT NULL AFTER `mesh_text_2`, ADD `mesh_text_3` TEXT NOT NULL AFTER `mesh_url_2`, ADD `mesh_url_3` TEXT NOT NULL AFTER `mesh_text_3`, ADD `mesh_text_4` TEXT NOT NULL AFTER `mesh_url_3`, ADD `mesh_url_4` TEXT NOT NULL AFTER `mesh_text_4`, ADD `mesh_text_5` TEXT NOT NULL AFTER `mesh_url_4`, ADD `mesh_url_5` TEXT NOT NULL AFTER `mesh_text_5`;

View File

@@ -0,0 +1,10 @@
ALTER TABLE `better_seo_i18n` CHANGE `mesh_text_1` `mesh_text_1` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_url_1` `mesh_url_1` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_text_2` `mesh_text_2` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_url_2` `mesh_url_2` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_text_3` `mesh_text_3` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_url_3` `mesh_url_3` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_text_4` `mesh_text_4` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_url_4` `mesh_url_4` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_text_5` `mesh_text_5` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_url_5` `mesh_url_5` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;

View File

@@ -0,0 +1,6 @@
ALTER TABLE `better_seo_i18n`
ADD `mesh_1` TEXT NOT NULL AFTER `mesh_url_5`,
ADD `mesh_2` TEXT NOT NULL AFTER `mesh_1`,
ADD `mesh_3` TEXT NOT NULL AFTER `mesh_2`,
ADD `mesh_4` TEXT NOT NULL AFTER `mesh_3`,
ADD `mesh_5` TEXT NOT NULL AFTER `mesh_4`;

View File

@@ -0,0 +1,5 @@
ALTER TABLE `better_seo_i18n` CHANGE `mesh_1` `mesh_1` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_2` `mesh_2` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_3` `mesh_3` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_4` `mesh_4` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;
ALTER TABLE `better_seo_i18n` CHANGE `mesh_5` `mesh_5` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE `better_seo_i18n` ADD `json_data` TEXT NOT NULL AFTER `mesh_5`;

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns="http://thelia.net/schema/dic/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/config http://thelia.net/schema/dic/config/thelia-1.0.xsd">
<forms>
<form name="betterseo_form" class="BetterSeo\Form\BetterSeoForm" />
</forms>
<hooks>
<hook id="betterseo.addfields.hook" class="BetterSeo\Hook\SeoFormHook">
<tag name="hook.event_listener" event="tab-seo.bottom" type="back" method="onTabSeoUpdateForm" />
</hook>
<hook id="betterseo.meta.hook" class="BetterSeo\Hook\MetaHook" scope="request">
<tag name="hook.event_listener" event="main.head-bottom" type="front" method="onMainHeadBottom" />
<argument type="service" id="request_stack" />
</hook>
</hooks>
</config>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="http://thelia.net/schema/dic/module"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/module http://thelia.net/schema/dic/module/module-2_2.xsd">
<fullnamespace>BetterSeo\BetterSeo</fullnamespace>
<descriptive locale="en_US">
<title>Set noindex, nofollow, h1, tag on pages, and manage mesh links</title>
</descriptive>
<descriptive locale="fr_FR">
<title>Ajoute la balise noindex, nofollow, h1, sur les pages, plus gestion des liens maillés</title>
</descriptive>
<languages>
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>2.1.2</version>
<authors>
<author>
<name>Nicolas Barbey</name>
<email>nabrbey@openstudio.fr</email>
</author>
<author>
<name>Gilles Bourgeat</name>
<email>gilles.bourgeat@gmail.com</email>
</author>
</authors>
<type>classic</type>
<thelia>2.5.0</thelia>
<stability>rc</stability>
<mandatory>0</mandatory>
<hidden>0</hidden>
</module>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="betterseo.save" path="/admin/module/betterseo/save">
<default key="_controller">BetterSeo\Controller\BetterSeoController::saveAction</default>
</route>
</routes>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<database defaultIdMethod="native" name="TheliaMain"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../vendor/thelia/propel/resources/xsd/database.xsd" >
<table name="better_seo" namespace="BetterSeo\Model">
<column autoIncrement="true" name="id" primaryKey="true" required="true" type="INTEGER" />
<column name="object_id" required="true" type="INTEGER"/>
<column name="object_type" required="true" />
<column name="noindex" required="true" default="0" type="TINYINT" size="4" />
<column name="nofollow" required="true" default="0" type="TINYINT" size="4" />
<column name="canonical_field" type="LONGVARCHAR" />
<column name="h1" type="LONGVARCHAR" />
<column name="mesh_text_1" type="LONGVARCHAR" />
<column name="mesh_url_1" type="LONGVARCHAR" />
<column name="mesh_text_2" type="LONGVARCHAR" />
<column name="mesh_url_2" type="LONGVARCHAR" />
<column name="mesh_text_3" type="LONGVARCHAR" />
<column name="mesh_url_3" type="LONGVARCHAR" />
<column name="mesh_text_4" type="LONGVARCHAR" />
<column name="mesh_url_4" type="LONGVARCHAR" />
<column name="mesh_text_5" type="LONGVARCHAR" />
<column name="mesh_url_5" type="LONGVARCHAR" />
<column name="mesh_1" type="LONGVARCHAR" />
<column name="mesh_2" type="LONGVARCHAR" />
<column name="mesh_3" type="LONGVARCHAR" />
<column name="mesh_4" type="LONGVARCHAR" />
<column name="mesh_5" type="LONGVARCHAR" />
<column name="json_data" type="LONGVARCHAR"/>
<behavior name="i18n">
<parameter name="i18n_columns"
value="noindex,
nofollow,
canonical_field,
h1,
mesh_text_1,
mesh_url_1,
mesh_text_2,
mesh_url_2,
mesh_text_3,
mesh_url_3,
mesh_text_4,
mesh_url_4,
mesh_text_5,
mesh_url_5,
mesh_1,
mesh_2,
mesh_3,
mesh_4,
mesh_5,
json_data"
/>
</behavior>
</table>
<external-schema filename="local/config/schema.xml" referenceOnly="true" />
</database>

View File

@@ -0,0 +1,2 @@
# Sqlfile -> Database map
thelia.sql=thelia

View File

@@ -0,0 +1,58 @@
# This is a fix for InnoDB in MySQL >= 4.1.x
# It "suspends judgement" for fkey relationships until are tables are set.
SET FOREIGN_KEY_CHECKS = 0;
-- ---------------------------------------------------------------------
-- better_seo
-- ---------------------------------------------------------------------
DROP TABLE IF EXISTS `better_seo`;
CREATE TABLE `better_seo`
(
`id` INTEGER NOT NULL AUTO_INCREMENT,
`object_id` INTEGER NOT NULL,
`object_type` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
-- ---------------------------------------------------------------------
-- better_seo_i18n
-- ---------------------------------------------------------------------
DROP TABLE IF EXISTS `better_seo_i18n`;
CREATE TABLE `better_seo_i18n`
(
`id` INTEGER NOT NULL,
`locale` VARCHAR(5) DEFAULT 'en_US' NOT NULL,
`noindex` TINYINT(4) DEFAULT 0 NOT NULL,
`nofollow` TINYINT(4) DEFAULT 0 NOT NULL,
`canonical_field` TEXT,
`h1` TEXT,
`mesh_text_1` TEXT,
`mesh_url_1` TEXT,
`mesh_text_2` TEXT,
`mesh_url_2` TEXT,
`mesh_text_3` TEXT,
`mesh_url_3` TEXT,
`mesh_text_4` TEXT,
`mesh_url_4` TEXT,
`mesh_text_5` TEXT,
`mesh_url_5` TEXT,
`mesh_1` TEXT,
`mesh_2` TEXT,
`mesh_3` TEXT,
`mesh_4` TEXT,
`mesh_5` TEXT,
`json_data` TEXT,
PRIMARY KEY (`id`,`locale`),
CONSTRAINT `better_seo_i18n_FK_1`
FOREIGN KEY (`id`)
REFERENCES `better_seo` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB;
# This restores the fkey checks, after having unset them earlier
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,64 @@
<?php
namespace BetterSeo\Controller;
use BetterSeo\Form\BetterSeoForm;
use BetterSeo\Model\BetterSeo;
use BetterSeo\Model\BetterSeoQuery;
use Symfony\Component\HttpFoundation\Request;
use Thelia\Controller\Admin\BaseAdminController;
use Thelia\Model\LangQuery;
use Thelia\Tools\URL;
class BetterSeoController extends BaseAdminController
{
/**
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Propel\Runtime\Exception\PropelException
*/
public function saveAction(Request $request)
{
$form = $this->createForm(BetterSeoForm::getName());
$seoForm = $this->validateForm($form);
$object_id = $request->get('object_id');
$object_type = $request->get('object_type');
$lang = LangQuery::create()
->filterById($request->get('lang_id'))
->findOne();
if (null === $objectSeo = BetterSeoQuery::create()
->filterByObjectId($object_id)
->filterByObjectType($object_type)
->findOne()
) {
$objectSeo = (new BetterSeo())
->setObjectId($object_id)
->setObjectType($object_type);
}
$objectSeo
->setLocale($lang->getLocale())
->setJsonData($seoForm->get('json_data')->getData())
->setNoindex(null === $seoForm->get('noindex_checkbox')->getData() ? 0 : 1)
->setNofollow(null === $seoForm->get('nofollow_checkbox')->getData() ? 0 : 1)
->setH1(null === $seoForm->get('h1')->getData() ? '' : $seoForm->get('h1')->getData());
for ($i = 1; $i <= 5; $i++) {
call_user_func([$objectSeo, 'setMeshUrl' . $i], $seoForm->get('mesh_url_' . $i)->getData());
call_user_func([$objectSeo, 'setMeshText' . $i], $seoForm->get('mesh_text_' . $i)->getData());
call_user_func([$objectSeo, 'setMesh' . $i], $seoForm->get('mesh_' . $i)->getData());
}
$objectSeo->save();
return $this->generateRedirect(
URL::getInstance()->absoluteUrl(
$request->getSession()->getReturnToUrl(),
['current_tab' => 'seo']
)
);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace BetterSeo\EventListeners;
use AlternateHreflang\Event\AlternateHreflangEvent;
use BetterSeo\Model\BetterSeoQuery;
use CanonicalUrl\Event\CanonicalUrlEvent;
use CanonicalUrl\Event\CanonicalUrlEvents;
use Sitemap\Event\SitemapEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Thelia\Core\HttpFoundation\Request;
class SeoListener implements EventSubscriberInterface
{
/** @var Request */
protected $request;
public function __construct(RequestStack $requestStack)
{
$this->request = $requestStack->getCurrentRequest();
}
public function removeHrefLang(AlternateHreflangEvent $event)
{
$objectType = $this->request->get('_view');
$objectId = $this->request->get($objectType.'_id');
$betterSeoObject = $this->getBetterSeoObject($objectType, $objectId);
}
public function checkSiteMap(SitemapEvent $event)
{
$objectId = $event->getRewritingUrl()->getViewId();
$objectType = $event->getRewritingUrl()->getView();
$betterSeoObject = $this->getBetterSeoObject($objectType, $objectId);
if (null !== $betterSeoObject){
if ($betterSeoObject->getNoindex() === 1){
$event->setHide(true);
}
}
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
$events = [];
if (class_exists('Sitemap\Event\SitemapEvent')){
$events[SitemapEvent::SITEMAP_EVENT] = ['checkSiteMap',128];
}
if (class_exists('AlternateHreflang\Event\AlternateHreflangEvent')){
$events[AlternateHreflangEvent::BASE_EVENT_NAME] = ['removeHrefLang',128];
}
return $events;
}
protected function getBetterSeoObject($objectType, $objectId)
{
$lang = $this->request->getSession()->getLang()->getLocale();
$betterSeoObject = BetterSeoQuery::create()
->filterByObjectType($objectType)
->filterByObjectId($objectId)
->findOne();
if (null !== $betterSeoObject){
$betterSeoObject->setLocale($lang);
}
return $betterSeoObject;
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace BetterSeo\Form;
use BetterSeo\BetterSeo;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Thelia\Core\Translation\Translator;
use Thelia\Form\BaseForm;
use Thelia\Type\JsonType;
class BetterSeoForm extends BaseForm
{
protected function buildForm()
{
$form = $this->formBuilder;
$form
->add(
'noindex_checkbox',
IntegerType::class,
array(
'required' => false,
'label' => Translator::getInstance()->trans(
'noindex',
array(),
BetterSeo::DOMAIN_NAME
),
'label_attr' => array(
'for' => 'noindex_checkbox'
)
)
)
->add(
'nofollow_checkbox',
IntegerType::class,
array(
'required' => false,
'label' => Translator::getInstance()->trans(
'nofollow',
array(),
BetterSeo::DOMAIN_NAME
),
'label_attr' => array(
'for' => 'nofollow_checkbox'
)
)
)
->add(
'h1',
TextType::class,
array(
'required' => false,
'label' => Translator::getInstance()->trans(
'h1',
array(),
BetterSeo::DOMAIN_NAME
),
'label_attr' => array(
'for' => 'h1'
)
)
)
->add(
'json_data',
TextareaType::class,
[
'required' => false,
'label' => Translator::getInstance()->trans(
'JSON structured data',
[],
BetterSeo::DOMAIN_NAME
),
'label_attr' => array(
'for' => 'json_data'
)
]
);
for ($i = 1; $i <= 5; $i++) {
$form->add(
'mesh_text_' . $i,
TextType::class,
array(
'required' => false,
'label' => Translator::getInstance()->trans(
'text',
array(),
BetterSeo::DOMAIN_NAME
),
'label_attr' => array(
'for' => 'mesh_text_' . $i
)
)
)
->add(
'mesh_url_' . $i,
UrlType::class,
array(
'required' => false,
'label' => Translator::getInstance()->trans(
'url',
array(),
BetterSeo::DOMAIN_NAME
),
'label_attr' => array(
'for' => 'mesh_url_' . $i
)
)
)
->add(
'mesh_' . $i,
TextType::class,
array(
'required' => false,
'label' => Translator::getInstance()->trans(
'text',
array(),
BetterSeo::DOMAIN_NAME
),
'label_attr' => array(
'for' => 'mesh_' . $i
)
)
);
}
}
public static function getName()
{
return 'betterseo_form';
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace BetterSeo\Hook;
use Symfony\Component\HttpFoundation\RequestStack;
use Thelia\Core\Event\Hook\HookRenderEvent;
use Thelia\Core\Hook\BaseHook;
use Thelia\Core\HttpFoundation\Request;
class MetaHook extends BaseHook
{
protected $request;
public function __construct(RequestStack $requestStack)
{
$this->request = $requestStack->getCurrentRequest();
}
public function onMainHeadBottom(HookRenderEvent $event)
{
$view = $this->request->get('_view');
if ($view && preg_match('#^[a-zA-Z0-9\-_\.]+$#', $view)) {
$id = $this->request->get($view . '_id');
$lang = $this->request->getSession()->getLang();
$event->add(
$this->render('meta_hook.html', [
'object_id' => $id,
'object_type' => $view,
'lang_id' => $lang->getId()
])
);
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace BetterSeo\Hook;
use BetterSeo\Model\BetterSeo;
use BetterSeo\Model\BetterSeoQuery;
use Thelia\Core\Event\Hook\HookRenderEvent;
use Thelia\Core\Hook\BaseHook;
use Thelia\Model\BrandQuery;
use Thelia\Model\CategoryQuery;
use Thelia\Model\ContentQuery;
use Thelia\Model\FolderQuery;
use Thelia\Model\Lang;
use Thelia\Model\LangQuery;
use Thelia\Model\ProductQuery;
class SeoFormHook extends BaseHook
{
public function onTabSeoUpdateForm(HookRenderEvent $event)
{
$objectId = $event->getArgument('id');
$objectType = $event->getArgument('type');
$event->add(
$this->render(
"seo-additional-fields.html",
[
'object_id' => $objectId,
'object_type' => $objectType,
]
)
);
}
}

View File

@@ -0,0 +1,11 @@
<?php
return array(
'Save' => 'Save',
'noindex_nofollow.help' => 'Management of the meta robots noindex, nofollow. Allow to not index this page for search engines. Be careful before checking this, check that your page does not generate traffic on Google Analytics. You risk losing SEO.',
'label.noindex' => 'Management of the meta robots noindex, nofollow, h1 tag and mesh links',
'h1' => 'H1',
);

View File

@@ -0,0 +1,19 @@
<?php
return array(
'Save' => 'Enregistrer',
'noindex_nofollow.help' => 'Gestion de la meta robots noindex, nofollow. Permet de ne pas indexer cette page pour les moteurs de recherche. Attention avant de cocher cela, bien vérifier que votre page ne génère pas de trafic sur Google Analytics. Vous risquez de perdre du référencement.',
'label.noindex' => 'Gestion de la meta robots noindex, nofollow, balise H1 et liens maillés :',
'h1' => 'H1',
'Link' => 'Lien',
'text' => 'texte',
'url' => 'url',
'Link text' => 'Texte du lien',
'Link URL' => 'URL du lien',
'Mesh links' => 'Liens maillés',
'Mesh' => 'Texte de maillage',
'Text' => 'Texte',
);

View File

@@ -0,0 +1,5 @@
<?php
return array(
);

View File

@@ -0,0 +1,5 @@
<?php
return array(
);

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,87 @@
<?php
namespace BetterSeo\Loop;
use BetterSeo\Model\BetterSeo;
use BetterSeo\Model\BetterSeoQuery;
use BetterSeo\Model\Map\BetterSeoI18nTableMap;
use Thelia\Core\Template\Element\BaseI18nLoop;
use Thelia\Core\Template\Element\LoopResult;
use Thelia\Core\Template\Element\LoopResultRow;
use Thelia\Core\Template\Element\PropelSearchLoopInterface;
use Thelia\Core\Template\Loop\Argument\Argument;
use Thelia\Core\Template\Loop\Argument\ArgumentCollection;
use Thelia\Model\LangQuery;
class BetterSeoLoop extends BaseI18nLoop implements PropelSearchLoopInterface
{
protected function getArgDefinitions()
{
return new ArgumentCollection(
Argument::createAlphaNumStringTypeArgument('object_id'),
Argument::createAlphaNumStringTypeArgument('object_type'),
Argument::createIntTypeArgument('lang_id')
);
}
public function buildModelCriteria()
{
$objectId = $this->getObjectId();
$objectType = $this->getObjectType();
$langId = $this->getLangId();
$lang = LangQuery::create()
->filterById($langId)
->findOne();
$query = BetterSeoQuery::create()
->filterByObjectId($objectId)
->filterByObjectType($objectType)
->useBetterSeoI18nQuery()
->filterByLocale($lang->getLocale())
->endUse()
->withColumn(BetterSeoI18nTableMap::NOINDEX, 'noindex')
->withColumn(BetterSeoI18nTableMap::NOFOLLOW, 'nofollow')
->withColumn(BetterSeoI18nTableMap::H1, 'h1')
->withColumn(BetterSeoI18nTableMap::JSON_DATA, 'json_data');
for ($i = 1; $i <= 5; $i++) {
$query->withColumn(constant(BetterSeoI18nTableMap::class . '::MESH_TEXT_' . $i), 'mesh_text_' . $i);
$query->withColumn(constant(BetterSeoI18nTableMap::class . '::MESH_URL_' . $i), 'mesh_url_' . $i);
$query->withColumn(constant(BetterSeoI18nTableMap::class . '::MESH_' . $i), 'mesh_' . $i);
}
return $query;
}
/**
* @param LoopResult $loopResult
* @return LoopResult
* @throws \Propel\Runtime\Exception\PropelException
*/
public function parseResults(LoopResult $loopResult)
{
/** @var BetterSeo $data */
foreach ($loopResult->getResultDataCollection() as $data) {
$loopResultRow = new LoopResultRow($data);
$loopResultRow->set('ID', $data->getId());
$loopResultRow->set('OBJECT_ID', $data->getObjectId());
$loopResultRow->set('OBJECT_TYPE', $data->getObjectType());
$loopResultRow->set('NOINDEX', $data->getVirtualColumn('noindex'));
$loopResultRow->set('NOFOLLOW', $data->getVirtualColumn('nofollow'));
$loopResultRow->set('H1', $data->getVirtualColumn('h1'));
$loopResultRow->set('JSON_DATA', $data->getVirtualColumn('json_data'));
for ($i = 1; $i <= 5; $i++) {
$loopResultRow->set('MESH_TEXT_' . $i, $data->getVirtualColumn('mesh_text_' . $i));
$loopResultRow->set('MESH_URL_' . $i, $data->getVirtualColumn('mesh_url_' . $i));
$loopResultRow->set('MESH_' . $i, $data->getVirtualColumn('mesh_' . $i));
}
$loopResult->addRow($loopResultRow);
}
return $loopResult;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace BetterSeo\Model;
use BetterSeo\Model\Base\BetterSeo as BaseBetterSeo;
class BetterSeo extends BaseBetterSeo
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace BetterSeo\Model;
use BetterSeo\Model\Base\BetterSeoI18n as BaseBetterSeoI18n;
class BetterSeoI18n extends BaseBetterSeoI18n
{
}

View File

@@ -0,0 +1,21 @@
<?php
namespace BetterSeo\Model;
use BetterSeo\Model\Base\BetterSeoI18nQuery as BaseBetterSeoI18nQuery;
/**
* Skeleton subclass for performing query and update operations on the 'better_seo_i18n' table.
*
*
*
* You should add additional methods to this class to meet the
* application requirements. This class will only be generated as
* long as it does not already exist in the output directory.
*
*/
class BetterSeoI18nQuery extends BaseBetterSeoI18nQuery
{
} // BetterSeoI18nQuery

View File

@@ -0,0 +1,21 @@
<?php
namespace BetterSeo\Model;
use BetterSeo\Model\Base\BetterSeoQuery as BaseBetterSeoQuery;
/**
* Skeleton subclass for performing query and update operations on the 'better_seo' table.
*
*
*
* You should add additional methods to this class to meet the
* application requirements. This class will only be generated as
* long as it does not already exist in the output directory.
*
*/
class BetterSeoQuery extends BaseBetterSeoQuery
{
} // BetterSeoQuery

View File

@@ -0,0 +1,75 @@
# Better Seo
Add Noindex checkbox and Canonical Url, h1 field and manage mesh links, in the Seo tab in back
**For this module to work properly you need to install ```Sitemap``` module, ```AlternateHreflang``` module and ```CanonicalUrl``` module.**
## Installation
### Manually
* Copy the module into ```<thelia_root>/local/modules/``` directory and be sure that the name of the module is BetterSeo.
* Activate it in your thelia administration panel
### Composer
Add it in your main thelia composer.json file
```
composer require thelia/better-seo-module:~1.4.1
```
## Loop
[better_seo_loop]
### Input arguments
|Argument |Description |
|--- |--- |
|**object_id** | The id of the object to display, exemple: object_id="12" |
|**object_type** | The type of the object to display (product, category, brand, folder, content) exemple object_type="brand"|
|**lang_id** | The id of the language|
### Output arguments
|Variable |Description |
|--- |--- |
|$ID | the id in seo_noindex table |
|$OBJECT_ID | the id of the object |
|$OBJECT_TYPE | the type of the object |
|$NOINDEX | if the page of the object is index or not (value 0 or 1) |
|$NOFOLLOW | if the page of the object is follow or not (value 0 or 1) |
|$CANONICAL | Canonical Url |
|$H1 | H1 |
|$MESH_TEXT_1 | mesh text 1 |
|$MESH_URL_1 | mesh url 1 |
|$MESH_TEXT_2 | mesh text 2 |
|$MESH_URL_2 | mesh url 2 |
|$MESH_TEXT_3 | mesh text 3 |
|$MESH_URL_3 | mesh url 3 |
|$MESH_TEXT_4 | mesh text 4 |
|$MESH_URL_4 | mesh url 4 |
|$MESH_TEXT_5 | mesh text 5 |
|$MESH_URL_5 | mesh url 5 |
|$MESH_1 | mesh 1 |
|$MESH_2 | mesh 2 |
|$MESH_3 | mesh 3 |
|$MESH_4 | mesh 4 |
|$MESH_5 | mesh 5 |
|$JSON_DATA | JSON data for ld json |
### Exemple
{loop type="better_seo_loop" name="exemple.loop" object_id="42" object_type="category" lang_id="1"}
To use ld json you need to add this part to the head of your pages (product, category, brand, folder, content)
{loop name="loop-name" type="better_seo_loop" object_id=$object_id object_type=$object_type lang_id=$langId}
<script type="application/ld+json">
{$JSON_DATA nofilter}
</script>
{/loop}

View File

@@ -0,0 +1,318 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace BetterSeo\Smarty\Plugins;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Thelia\Core\Event\Image\ImageEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Exception\TaxEngineException;
use Thelia\Model\Category;
use Thelia\Model\CategoryQuery;
use Thelia\Model\ConfigQuery;
use Thelia\Model\Content;
use Thelia\Model\ContentQuery;
use Thelia\Model\Folder;
use Thelia\Model\FolderQuery;
use Thelia\Model\Lang;
use Thelia\Model\LangQuery;
use Thelia\Model\Product;
use Thelia\Model\ProductImageQuery;
use Thelia\Model\ProductPriceQuery;
use Thelia\Model\ProductQuery;
use Thelia\Model\ProductSaleElementsQuery;
use Thelia\TaxEngine\TaxEngine;
use TheliaSmarty\Template\AbstractSmartyPlugin;
use TheliaSmarty\Template\SmartyPluginDescriptor;
class BetterSeoMicroDataPlugin extends AbstractSmartyPlugin
{
protected $request;
protected $taxEngine;
protected $dispatcher;
public function __construct(RequestStack $requestStack, TaxEngine $taxEngine, EventDispatcherInterface $dispatcher)
{
$this->request = $requestStack->getCurrentRequest();
$this->taxEngine = $taxEngine;
$this->dispatcher = $dispatcher;
}
public function getPluginDescriptors()
{
return [
new SmartyPluginDescriptor('function', 'BetterSeoMicroData', $this, 'betterSeoMicroData'),
];
}
/**
* @param $params
*
* @return array|int|string
*
* @throws \Exception
* @throws \Propel\Runtime\Exception\PropelException
*/
public function betterSeoMicroData($params)
{
$type = $params['type'] ?? $this->request->get('_view');
$lang = $this->request->getSession()->getLang();
if (!$lang) {
$lang = LangQuery::create()->filterByByDefault(1)->findOne();
}
$microdata = null;
switch ($type) {
case 'product':
$id = $params['id'] ?? $this->request->get('product_id');
$product = ProductQuery::create()->filterById($id)->findOne();
$relatedProducts = null;
if (array_key_exists('related_products', $params)){
$relatedProducts = \is_array($params['related_products']) ? $params['related_products'] : $this->explode($params['related_products']);
}
$microdata = $this->getProductMicroData($product, $lang, $relatedProducts);
break;
case 'category':
$id = $params['id'] ?? $this->request->get('category_id');
if ($id) {
$category = CategoryQuery::create()->filterById($id)->findOne();
$microdata = $this->getCategoryMicroData($category, $lang);
}
break;
case 'folder':
$id = $params['id'] ?? $this->request->get('folder_id');
if ($id) {
$folder = FolderQuery::create()->filterById($id)->findOne();
$microdata = $this->getFolderMicroData($folder, $lang);
}
break;
case 'content':
$id = $params['id'] ?? $this->request->get('content_id');
if ($id) {
$microdata = $this->getContentMicroData($id, $lang);
}
break;
}
$scriptsTag = '';
$scriptsTag .= '<script type="application/ld+json">'.json_encode($this->getStoreMicroData(), JSON_UNESCAPED_UNICODE).'</script>';
if (null !== $microdata) {
$scriptsTag .= '<script type="application/ld+json">'.json_encode($microdata, JSON_UNESCAPED_UNICODE).'</script>';
}
return $scriptsTag;
}
/**
* @return array
*/
protected function getStoreMicroData()
{
$microData = [
'@context' => 'https://schema.org/',
'@type' => 'Organization',
'name' => ConfigQuery::read('store_name'),
'description' => ConfigQuery::read('store_description'),
'url' => ConfigQuery::read('url_site'),
'address' => [
'@type' => 'PostalAddress',
'streetAddress' => ConfigQuery::read('store_address1').' '.ConfigQuery::read('store_address2').' '.ConfigQuery::read('store_address3'),
'addressLocality' => ConfigQuery::read('store_city'),
'postalCode' => ConfigQuery::read('store_zipcode'),
],
];
return $microData;
}
/**
* @param array $relatedProducts
*
* @return array
*
* @throws \Exception
* @throws \Propel\Runtime\Exception\PropelException
*/
protected function getProductMicroData(Product $product, Lang $lang, $relatedProducts = [])
{
$product->setLocale($lang->getLocale());
$image = ProductImageQuery::create()->filterByProductId($product->getId())->orderByPosition()->find()[0];
$pse = ProductSaleElementsQuery::create()->filterByProductId($product->getId())->filterByIsDefault(1)->findOne();
$psePrice = ProductPriceQuery::create()->filterByProductSaleElementsId($pse->getId())->findOne();
$taxCountry = $this->taxEngine->getDeliveryCountry();
try {
$taxedPrice = $product->getTaxedPrice(
$taxCountry,
$psePrice->getPrice()
);
if ($pse->getPromo()) {
$taxedPrice = $product->getTaxedPromoPrice(
$taxCountry,
$psePrice->getPromoPrice()
);
}
} catch (TaxEngineException $e) {
$taxedPrice = null;
}
$imagePath = null;
if ($image) {
$baseSourceFilePath = ConfigQuery::read('images_library_path');
if ($baseSourceFilePath === null) {
$baseSourceFilePath = THELIA_LOCAL_DIR.'media'.DS.'images';
} else {
$baseSourceFilePath = THELIA_ROOT.$baseSourceFilePath;
}
$event = new ImageEvent();
$sourceFilePath = $baseSourceFilePath.'/product/'.$image->getFile();
$event->setSourceFilepath($sourceFilePath);
$event->setCacheSubdirectory('product');
try {
$this->dispatcher->dispatch($event, TheliaEvents::IMAGE_PROCESS);
$imagePath = $event->getFileUrl();
} catch (\Exception $e) {
$imagePath = $image->getFile();
}
}
$microData = [
'@context' => 'https://schema.org/',
'@type' => 'Product',
'name' => $product->getTitle(),
'image' => $imagePath,
'description' => $product->getDescription(),
'sku' => $product->getRef(),
'offers' => [
'url' => $product->getUrl(),
'priceCurrency' => $this->request->getSession()->getCurrency()->getCode(),
'price' => $taxedPrice,
'itemCondition' => 'https://schema.org/NewCondition',
'availability' => $pse->getQuantity() > 0 ? 'http://schema.org/InStock' : 'http://schema.org/OutOfStock',
],
];
if ($pse->getEanCode()) {
$microData['gtin13'] = $pse->getEanCode();
}
if ($brand = $product->getBrand()) {
$microData['brand']['@type'] = 'Brand';
$microData['brand']['name'] = $brand->getTitle();
}
if ($relatedProducts) {
foreach ($relatedProducts as $relatedProductId) {
$relatedProduct = ProductQuery::create()->filterById($relatedProductId)->findOne();
$microData['isRelatedTo'][] = $this->getProductMicroData($relatedProduct, $lang);
}
}
return $microData;
}
/**
* @return array
*/
protected function getCategoryMicroData(Category $category, Lang $lang)
{
$category->setLocale($lang->getLocale());
$products = $category->getProducts();
$itemListElement = [];
$i = 1;
foreach ($products as $product) {
$itemListElement[] = [
'@type' => 'ListItem',
'position' => $i++,
'url' => $product->getUrl(),
];
}
$microData = [
'@context' => 'https://schema.org/',
'@type' => 'ItemList',
'url' => $category->getUrl(),
'numberOfItems' => \count($products),
'itemListElement' => $itemListElement,
];
return $microData;
}
/**
* @return array
*/
protected function getFolderMicroData(Folder $folder, Lang $lang)
{
$folder->setLocale($lang->getLocale());
$microData = [
'@context' => 'https://schema.org/',
'@type' => 'Guide',
'url' => $folder->getUrl(),
"name" => $folder->getTitle(),
"abstract" => $folder->getChapo(),
];
return $microData;
}
/**
* @return array
*/
protected function getContentMicroData($contentId, Lang $lang)
{
$content = ContentQuery::create()->filterById($contentId)->findOne();
if (null === $content) {
return null;
}
$content->setLocale($lang->getLocale());
$microData = [
'@context' => 'https://schema.org/',
'@type' => 'Article',
'url' => $content->getUrl(),
"name" => $content->getTitle(),
"abstract" => $content->getChapo(),
];
$defaultFoIdlder = $content->getDefaultFolderId();
if (null !== $defaultFoIdlder) {
$default_folder = FolderQuery::create()->findOneById($defaultFoIdlder);
if (null !== $default_folder) {
$default_folder->setLocale($lang->getLocale());
$microData['isPartOf'] = [
'name' => $default_folder->getTitle(),
'url' => $default_folder->getUrl()
];
}
}
return $microData;
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "thelia/better-seo-module",
"license": "LGPL-3.0+",
"type": "thelia-module",
"require": {
"thelia/installer": "~1.1"
},
"extra": {
"installer-name": "BetterSeo"
}
}

View File

@@ -0,0 +1,196 @@
{$pageUrl|default:null}
{$noindex_val = null}
{$nofollow_val = null}
{$json_data = null}
{$h1 = null}
{for $i=1 to 5}
{assign var="mesh_text_$i" value=null}
{assign var="mesh_url_$i" value=null}
{assign var="mesh_$i" value=null}
{/for}
{loop type="better_seo_loop" name="better_seo_data" object_id=$object_id object_type=$object_type lang_id=$edit_language_id}
{$noindex_val = $NOINDEX}
{$nofollow_val = $NOFOLLOW}
{$json_data = $JSON_DATA}
{$h1 = $H1}
{for $i=1 to 5}
{assign var="mesh_text_$i" value={$MESH_TEXT_{$i}}}
{assign var="mesh_url_$i" value={$MESH_URL_{$i}}}
{assign var="mesh_$i" value={$MESH_{$i}}}
{/for}
{/loop}
{form name = "betterseo_form"}
<form method="POST" action="{url path="/admin/module/betterseo/save" object_id=$object_id object_type=$object_type lang_id=$edit_language_id}">
<div class="panel panel-default">
<div class="panel-heading">
<label>{intl l="label.noindex" d="betterseo.bo.default"}</label>
</div>
<div class="panel-body">
{form_hidden_fields form=$form}
{if $form_error}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger">{$form_error_message}</div>
</div>
</div>
{/if}
<div class="panel-body text-right">
<button type="submit" name="save_mode" value="stay" class="form-submit-button btn btn-sm btn-default btn-success">
{intl l='Save'} <span class="glyphicon glyphicon-ok"></span>
</button>
</div>
{form_field field="noindex_checkbox"}
<div class="{if $error} has-error{/if}">
<input id="{$label_attr.for}" type="checkbox" name="{$name}" value="1" {if $noindex_val == 1} checked {/if}/>
<label class="control-label danger" for="{$label_attr.for}">{intl l=$label d="betterseo.bo.default"}</label>
{if $error}
<div class="text-danger">
{$message}
</div>
{/if}
</div>
{/form_field}
{form_field field="nofollow_checkbox"}
<div class="{if $error} has-error{/if}">
<input id="{$label_attr.for}" type="checkbox" name="{$name}" value="1" {if $nofollow_val == 1} checked {/if}/>
<label class="control-label danger" for="{$label_attr.for}">{intl l=$label d="betterseo.bo.default"}</label>
{if $error}
<div class="text-danger">
{$message}
</div>
{/if}
</div>
{/form_field}
<div class="help-block">
{intl l="noindex_nofollow.help" d="betterseo.bo.default"}
</div>
{form_field field="h1"}
<div class="{if $error} has-error{/if}">
<label class="control-label danger" for="{$label_attr.for}">{intl l=$label d="betterseo.bo.default"}</label>
<input type="text" id="{$label_attr.for}" class="form-control" name="{$name}" value="{$h1}"/>
{if $error}
<div class="text-danger">
{$message}
</div>
{/if}
<div class="help-block">
</div>
</div>
{/form_field}
{form_field field="json_data"}
<div class="form-group">
<label for="{$label_attr.for}" class="control-label">{$label}</label>
<textarea name={$name} id="{$label_attr.for}" cols="30" rows="10" class="form-control">{$json_data}</textarea>
</div>
{/form_field}
<hr/>
<table class="table table-striped">
<caption>{intl l="Mesh" d="betterseo.bo.default"}</caption>
<thead>
<tr>
{for $i=1 to 5}
<th>
{intl l="Text" d="betterseo.bo.default"} {$i}
</th>
{/for}
</tr>
</thead>
<tbody>
<tr>
{for $i=1 to 5}
{$textVar = "MESH_$i"}
<td>
{form_field field="mesh_$i"}
<div class="{if $error} has-error{/if}">
<input type="text" id="{$label_attr.for}" class="form-control" name="{$name}" value="{$mesh_{$i}}"/>
{if $error}
<div class="text-danger">
{$message}
</div>
{/if}
</div>
{/form_field}
</td>
{/for}
</tr>
</tbody>
</table>
<hr/>
<table class="table table-striped">
<caption>{intl l="Mesh links" d="betterseo.bo.default"}</caption>
<thead>
<tr>
<th>
</th>
<th>
{intl l="Link text" d="betterseo.bo.default"}
</th>
<th>
{intl l="Link URL" d="betterseo.bo.default"}
</th>
</tr>
</thead>
<tbody>
{for $i=1 to 5}
{$urlVar = "MESH_URL_$i"}
{$textVar = "MESH_TEXT_$i"}
<tr>
<td>
{intl l="Link" d="betterseo.bo.default"} {$i}
</td>
<td>
{form_field field="mesh_text_$i"}
<div class="{if $error} has-error{/if}">
<label class="control-label danger" for="{$label_attr.for}">{intl l=$label d="betterseo.bo.default"}</label>
<input type="text" id="{$label_attr.for}" class="form-control" name="{$name}" value="{$mesh_text_{$i}}"/>
{if $error}
<div class="text-danger">
{$message}
</div>
{/if}
</div>
{/form_field}
</td>
<td>
{form_field field="mesh_url_$i"}
<div class="{if $error} has-error{/if}">
<label class="control-label danger" for="{$label_attr.for}">{intl l=$label d="betterseo.bo.default"}</label>
<input type="url" id="{$label_attr.for}" class="form-control" name="{$name}" value="{$mesh_url_{$i}}"/>
{if $error}
<div class="text-danger">
{$message}
</div>
{/if}
</div>
{/form_field}
</td>
</tr>
{/for}
</tbody>
</table>
</div>
<div class="panel-body text-right">
<button type="submit" name="save_mode" value="stay" class="form-submit-button btn btn-sm btn-default btn-success">
{intl l='Save'} <span class="glyphicon glyphicon-ok"></span>
</button>
</div>
</div>
</form>
{/form}

View File

@@ -0,0 +1,18 @@
{BetterSeoMicroData}
{loop type="better_seo_loop" name="better_seo_meta_loop" object_id=$object_id object_type=$object_type lang_id=$lang_id}
{if $NOINDEX == 1 and $NOFOLLOW == 1}
<meta name="robots" content="noindex, nofollow">
{elseif $NOINDEX == 1}
<meta name="robots" content="noindex, follow">
{elseif $NOFOLLOW == 1}
<meta name="robots" content="nofollow">
{/if}
{if $JSON_DATA}
<script type="application/ld+json">
{$JSON_DATA nofilter}
</script>
{/if}
{/loop}

View File

@@ -0,0 +1,20 @@
# 1.2.0
- Add canonical override in seo form
# 1.1.1
- Fix exception when Thelia was not configured
# 1.1.0
- Adds the unit tests in the case of a single domain
- Adds the case there is a subfolder
# 1.0.1
- Fix ```installer-name``` in the composer.json file
# 1.0.2
- Fix hook scope for compatibility with the other modules

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace CanonicalUrl;
use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator;
use Thelia\Module\BaseModule;
class CanonicalUrl extends BaseModule
{
/** @var string */
const DOMAIN_NAME = 'canonicalurl';
const SEO_CANONICAL_META_KEY = 'seo_canonical_meta';
/**
* Defines how services are loaded in your modules.
*/
public static function configureServices(ServicesConfigurator $servicesConfigurator): void
{
$servicesConfigurator->load(self::getModuleCode().'\\', __DIR__)
->exclude([THELIA_MODULE_DIR.ucfirst(self::getModuleCode()).'/I18n/*'])
->autowire(true)
->autoconfigure(true);
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns="http://thelia.net/schema/dic/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/config http://thelia.net/schema/dic/config/thelia-1.0.xsd">
<hooks>
<hook id="canonicalurl.meta.hook" class="CanonicalUrl\Hook\MetaHook" scope="request">
<tag name="hook.event_listener" event="main.head-bottom" type="front" method="onMainHeadBottom" />
<argument type="service" id="event_dispatcher" />
</hook>
<hook id="canonicalurl.seo.update.form" class="CanonicalUrl\Hook\SeoUpdateFormHook">
<tag name="hook.event_listener" event="tab-seo.update-form" type="backoffice" method="addInputs"/>
</hook>
</hooks>
</config>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="http://thelia.net/schema/dic/module"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/module http://thelia.net/schema/dic/module/module-2_2.xsd">
<fullnamespace>CanonicalUrl\CanonicalUrl</fullnamespace>
<descriptive locale="en_US">
<title>Adds the canonical Url in the metas of your site</title>
</descriptive>
<descriptive locale="fr_FR">
<title>Ajoute l'Url canonique dans les metas de votre site</title>
</descriptive>
<languages>
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>2.1.6</version>
<authors>
<author>
<name>Gilles Bourgeat</name>
<email>gilles.bourgeat@gmail.com</email>
<website>https://github.com/gillesbourgeat</website>
</author>
<author>
<name>Franck Allimant</name>
<company>CQFDev</company>
<email>franck@cqfdev.fr</email>
<website>www.cqfdev.fr</website>
</author>
<author>
<name>Vincent Lopes-Vicente</name>
<company>OpenStudio</company>
<email>vlopes@openstudio.fr</email>
</author>
</authors>
<type>classic</type>
<thelia>2.5.0</thelia>
<stability>prod</stability>
</module>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
</routes>

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace CanonicalUrl\Event;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Class CanonicalUrlEvent.
*
* @author Gilles Bourgeat <gilles.bourgeat@gmail.com>
*/
class CanonicalUrlEvent extends Event
{
/** @var string|null */
protected $url = null;
/**
* @return string|null
*/
public function getUrl()
{
return $this->url;
}
/**
* @param string|null $url
*
* @return $this
*/
public function setUrl($url)
{
if ($url !== null && $url[0] !== '/' && filter_var($url, \FILTER_VALIDATE_URL) === false) {
throw new \InvalidArgumentException('The value "'.(string) $url.'" is not a valid Url or Uri.');
}
$this->url = $url;
return $this;
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace CanonicalUrl\Event;
/**
* Class CanonicalUrlEvents.
*
* @author Gilles Bourgeat <gilles.bourgeat@gmail.com>
*/
class CanonicalUrlEvents
{
const GENERATE_CANONICAL = 'canonical.url.generate.canonical';
}

View File

@@ -0,0 +1,249 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace CanonicalUrl\EventListener;
use BetterSeo\Model\BetterSeoQuery;
use CanonicalUrl\CanonicalUrl;
use CanonicalUrl\Event\CanonicalUrlEvent;
use CanonicalUrl\Event\CanonicalUrlEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Thelia\Core\HttpFoundation\Request;
use Thelia\Core\HttpFoundation\Session\Session;
use Thelia\Log\Tlog;
use Thelia\Model\ConfigQuery;
use Thelia\Model\Lang;
use Thelia\Model\LangQuery;
use Thelia\Model\MetaDataQuery;
/**
* Class CanonicalUrlListener.
*
* @author Gilles Bourgeat <gilles.bourgeat@gmail.com>
*/
class CanonicalUrlListener implements EventSubscriberInterface
{
/** @var RequestStack */
protected $requestStack;
/** @var Session */
protected $session;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function generateUrlCanonical(CanonicalUrlEvent $event): void
{
/** @var Request $request */
if (null === $request = $this->requestStack->getCurrentRequest()) {
return;
}
if ($event->getUrl() !== null) {
return;
}
if (null !== $canonicalOverride = $this->getCanonicalOverride()) {
try {
$event->setUrl($canonicalOverride);
return;
} catch (\InvalidArgumentException $e) {
Tlog::getInstance()->addWarning($e->getMessage());
}
}
$parseUrlByCurrentLocale = $this->getParsedUrlByCurrentLocale();
if (empty($parseUrlByCurrentLocale['host'])) {
return;
}
// Be sure to use the proper domain name
$canonicalUrl = $parseUrlByCurrentLocale['scheme'].'://'.$parseUrlByCurrentLocale['host'];
// preserving a potential subdirectory, e.g. http://somehost.com/mydir/index.php/...
$canonicalUrl .= $request->getBaseUrl();
// Remove script name from path, e.g. http://somehost.com/index.php/...
$canonicalUrl = preg_replace("!/index(_dev)?\.php!", '', $canonicalUrl);
$path = $request->getPathInfo();
if (!empty($path) && $path != '/') {
$canonicalUrl .= $path;
$canonicalUrl = rtrim($canonicalUrl, '/');
} else if (isset($parseUrlByCurrentLocale['query'])) {
$canonicalUrl .= '/?'. (array_key_exists("query", $parseUrlByCurrentLocale)) ? $parseUrlByCurrentLocale['query'] : "";
}
try {
$event->setUrl($canonicalUrl);
} catch (\InvalidArgumentException $e) {
Tlog::getInstance()->addWarning($e->getMessage());
}
}
/**
* @return array
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
CanonicalUrlEvents::GENERATE_CANONICAL => [
'generateUrlCanonical', 128,
],
];
}
/**
* @return array
*
* At least one element will be present within the array.
* Potential keys within this array are:
* scheme - e.g. http
* host
* port
* user
* pass
* path
* query - after the question mark ?
* fragment - after the hashmark #
*/
protected function getParsedUrlByCurrentLocale()
{
/** @var Request $request */
$request = $this->requestStack->getCurrentRequest();
// for one domain by lang
if ((int) ConfigQuery::read('one_domain_foreach_lang', 0) === 1) {
// We always query the DB here, as the Lang configuration (then the related URL) may change during the
// user session lifetime, and improper URLs could be generated. This is quite odd, okay, but may happen.
$langUrl = LangQuery::create()->findPk($request->getSession()->getLang()->getId())->getUrl();
if (!empty($langUrl) && false !== $parse = parse_url($langUrl)) {
return $parse;
}
}
// Configured site URL
$urlSite = ConfigQuery::read('url_site');
if (!empty($urlSite) && false !== $parse = parse_url($urlSite)) {
return $parse;
}
// return current URL
return parse_url($request->getUri());
}
/**
* @return string|null
*/
protected function getCanonicalOverride()
{
/** @var Request $request */
$request = $this->requestStack->getCurrentRequest();
$lang = $request->getSession()->getLang();
$routeParameters = $this->getRouteParameters();
if (null === $routeParameters) {
return null;
}
$url = null;
$metaCanonical = MetaDataQuery::create()
->filterByMetaKey(CanonicalUrl::SEO_CANONICAL_META_KEY)
->filterByElementKey($routeParameters['view'])
->filterByElementId($routeParameters['id'])
->findOne();
if (null !== $metaCanonical) {
$canonicalValues = json_decode($metaCanonical->getValue(), true);
$url = isset($canonicalValues[$lang->getLocale()]) && ! empty($canonicalValues[$lang->getLocale()]) ? $canonicalValues[$lang->getLocale()] :null;
}
// Try to get old field of BetterSeoModule
if (null === $url && class_exists("BetterSeo\BetterSeo")) {
try {
$betterSeoData = BetterSeoQuery::create()
->filterByObjectType($routeParameters['view'])
->filterByObjectId($routeParameters['id'])
->findOne();
$url = $betterSeoData->setLocale($lang->getLocale())
->getCanonicalField();
} catch (\Throwable $exception) {
//Catch if field doesn't exist but do nothing
}
}
if (null === $url) {
return null;
}
if (false === filter_var($url, \FILTER_VALIDATE_URL)) {
return rtrim($this->getSiteBaseUrlForLocale($lang), "/")."/".$url;
}
return $url;
}
protected function getSiteBaseUrlForLocale(Lang $lang = null)
{
if (null === $lang) {
$lang = $this->requestStack->getCurrentRequest()->getSession()->getLang();
}
if ((int) ConfigQuery::read('one_domain_foreach_lang', 0) === 1) {
// We always query the DB here, as the Lang configuration (then the related URL) may change during the
// user session lifetime, and improper URLs could be generated. This is quite odd, okay, but may happen.
$langUrl = LangQuery::create()->findPk($lang->getId())->getUrl();
return $langUrl;
}
// Configured site URL
$urlSite = ConfigQuery::read('url_site');
return $urlSite;
}
/**
* @return array|null
*/
protected function getRouteParameters()
{
/** @var Request $request */
$request = $this->requestStack->getCurrentRequest();
$view = $request->get('view');
if (null === $view) {
$view = $request->get('_view');
}
if (null === $view) {
return null;
}
$id = $request->get($view.'_id');
if (null === $id) {
return null;
}
return compact('view', 'id');
}
}

View File

@@ -0,0 +1,112 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace CanonicalUrl\EventListener;
use CanonicalUrl\CanonicalUrl;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Thelia\Action\BaseAction;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Core\Event\TheliaFormEvent;
use Thelia\Core\Event\UpdateSeoEvent;
use Thelia\Core\HttpFoundation\Request;
use Thelia\Model\MetaDataQuery;
class SeoFormListener extends BaseAction implements EventSubscriberInterface
{
/** @var RequestStack */
protected $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public static function getSubscribedEvents()
{
return [
TheliaEvents::FORM_AFTER_BUILD.'.thelia_seo' => ['addCanonicalField', 128],
TheliaEvents::CATEGORY_UPDATE_SEO => ['saveCategorySeoFields', 128],
TheliaEvents::BRAND_UPDATE_SEO => ['saveBrandSeoFields', 128],
TheliaEvents::CONTENT_UPDATE_SEO => ['saveContentSeoFields', 128],
TheliaEvents::FOLDER_UPDATE_SEO => ['saveFolderSeoFields', 128],
TheliaEvents::PRODUCT_UPDATE_SEO => ['saveProductSeoFields', 128],
];
}
public function saveCategorySeoFields(UpdateSeoEvent $event, $eventName, EventDispatcherInterface $dispatcher): void
{
$this->saveSeoFields($event, $eventName, $dispatcher, 'category');
}
public function saveBrandSeoFields(UpdateSeoEvent $event, $eventName, EventDispatcherInterface $dispatcher): void
{
$this->saveSeoFields($event, $eventName, $dispatcher, 'brand');
}
public function saveContentSeoFields(UpdateSeoEvent $event, $eventName, EventDispatcherInterface $dispatcher): void
{
$this->saveSeoFields($event, $eventName, $dispatcher, 'content');
}
public function saveFolderSeoFields(UpdateSeoEvent $event, $eventName, EventDispatcherInterface $dispatcher): void
{
$this->saveSeoFields($event, $eventName, $dispatcher, 'folder');
}
public function saveProductSeoFields(UpdateSeoEvent $event, $eventName, EventDispatcherInterface $dispatcher): void
{
$this->saveSeoFields($event, $eventName, $dispatcher, 'product');
}
protected function saveSeoFields(UpdateSeoEvent $event, $eventName, EventDispatcherInterface $dispatcher, $elementKey): void
{
$form = $this->requestStack->getCurrentRequest()->get('thelia_seo');
if (null === $form || !\array_key_exists('id', $form) || !\array_key_exists('canonical', $form)) {
return;
}
$canonicalValues = [];
$canonicalMetaData = MetaDataQuery::create()
->filterByMetaKey(CanonicalUrl::SEO_CANONICAL_META_KEY)
->filterByElementKey($elementKey)
->filterByElementId($form['id'])
->findOneOrCreate();
if (!$canonicalMetaData->isNew()) {
$canonicalValues = json_decode($canonicalMetaData->getValue(), true);
}
$locale = $form['locale'];
$canonicalValues[$locale] = $form['canonical'];
$canonicalMetaData
->setIsSerialized(0)
->setValue(json_encode($canonicalValues))
->save();
}
public function addCanonicalField(TheliaFormEvent $event): void
{
$event->getForm()->getFormBuilder()
->add(
'canonical',
TextType::class
);
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace CanonicalUrl\Hook;
use CanonicalUrl\Event\CanonicalUrlEvent;
use CanonicalUrl\Event\CanonicalUrlEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Thelia\Core\Event\Hook\HookRenderEvent;
use Thelia\Core\Hook\BaseHook;
/**
* Class MetaHook.
*
* @author Gilles Bourgeat <gilles.bourgeat@gmail.com>
*/
class MetaHook extends BaseHook
{
/** @var EventDispatcherInterface */
protected $eventDispatcher;
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
public function onMainHeadBottom(HookRenderEvent $hookRender): void
{
$event = new CanonicalUrlEvent();
$this->eventDispatcher->dispatch(
$event,
CanonicalUrlEvents::GENERATE_CANONICAL,
);
if ($event->getUrl()) {
$hookRender->add('<link rel="canonical" href="'.$event->getUrl().'">');
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace CanonicalUrl\Hook;
use CanonicalUrl\CanonicalUrl;
use Thelia\Core\Event\Hook\HookRenderEvent;
use Thelia\Core\Hook\BaseHook;
use Thelia\Model\MetaDataQuery;
class SeoUpdateFormHook extends BaseHook
{
public function addInputs(HookRenderEvent $event): void
{
$id = $event->getArgument('id');
$type = $event->getArgument('type');
$canonical = null;
$canonicalMetaData = MetaDataQuery::create()
->filterByMetaKey(CanonicalUrl::SEO_CANONICAL_META_KEY)
->filterByElementKey($type)
->filterByElementId($id)
->findOneOrCreate();
$canonicalMetaDataValues = json_decode($canonicalMetaData->getValue(), true);
$lang = $this->getSession()->getAdminEditionLang();
if (isset($canonicalMetaDataValues[$lang->getLocale()])) {
$canonical = $canonicalMetaDataValues[$lang->getLocale()];
}
$event->add($this->render(
'hook-seo-update-form.html',
[
'form' => $event->getArgument('form'),
'canonical' => $canonical,
]
));
}
}

View File

@@ -0,0 +1,15 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Surcharge de l\'url canonique' => 'Canonical url override',
];

View File

@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -0,0 +1,47 @@
# Canonical Url
This module generates a canonical URL for every page of your shop. Once activated, you'll find a `<link rel="canonical" href="..." />` tag in the header of your pages.
## Examples
- If the page URL is not rewritten, the canonical URL will contain all the URL parameters. Example for : For URL ```http://demo.thelia.net/?view=product&locale=en_US&product_id=18```
```html
<link rel="canonical" href="http://demo.thelia.net/?view=product&locale=en_US&product_id=18" />
```
Obviously, this is far from ideal. Consider activating URL rewriting !
- When the page URL contains the script name (index.php), it will be removed from the canonical URL. Example, the canonical URL of ```http://demo.thelia.net/index.php?view=product&locale=en_US&product_id=18``` is :
```html
<link rel="canonical" href="http://demo.thelia.net/?view=product&locale=en_US&product_id=18" />
```
When a rewritten URL contains parameters, these parameters a removed. For ```http://demo.thelia.net/index.php/en_en-your-path.html?page=44```, the canonical URL is :
```html
<link rel="canonical" href="http://demo.thelia.net/en_en-your-path" />
```
- If the page URL contains a domain which is not the main shop domain, this domain is replaced by the main shop domain. For ```http://demo458.thelia.net/index.php/en_en-your-path.html?page=44``` the canonical URL is :
```html
<link rel="canonical" href="http://demo.thelia.net/en_en-your-path" />
```
## Installation
### Manually
* Copy the module into ```<thelia_root>/local/modules/``` directory and be sure that the name of the module is CanonicalUrl.
* Activate it in your thelia administration panel
### Composer
Add it in your main thelia composer.json file
```
composer require thelia/canonical-url-module:~2.1
```
## Usage
You just have to activate the module and check the meta tags of your shop.
The canonical will be generated automatically but you can define a canonical url in seo form for each item if you want override the generated url.

View File

@@ -0,0 +1,245 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace CanonicalUrl\Tests;
use CanonicalUrl\Event\CanonicalUrlEvent;
use CanonicalUrl\EventListener\CanonicalUrlListener;
use Symfony\Component\HttpFoundation\Request;
/**
* Class CanonicalUrlTest.
*
* @author Gilles Bourgeat <gilles.bourgeat@gmail.com>
*/
class CanonicalUrlTest extends \PHPUnit_Framework_TestCase
{
protected function setUp(): void
{
/*$config = $this->getMock('Thelia\Model\ConfigQuery');
$config->expects($this->any())
->method('read')
->with('allow_slash_ended_uri')
->will($this->returnValue(true));*/
}
public function testRemoveFileIndex(): void
{
$this->performList('http://myhost.com/test', [
'http://myhost.com/index.php/test',
'http://myhost.com/index.php/test/',
'http://myhost.com/index.php/test?page=22&list=1',
'http://myhost.com/index.php/test/?page=22&list=1',
]);
}
public function testRemoveFileIndexDev(): void
{
$this->performList('http://myhost.com/test', [
'http://myhost.com/index_dev.php/test',
'http://myhost.com/index_dev.php/test/',
'http://myhost.com/index_dev.php/test?page=22&list=1',
'http://myhost.com/index_dev.php/test/?page=22&list=1',
], $this->fakeServer(
'/var/www/web/index_dev.php',
'/index_dev.php'
));
}
public function testHTTPWithSubDomain(): void
{
$this->performList('http://mysubdomain.myhost.com/test', [
'http://mysubdomain.myhost.com/index.php/test/?page=22&list=1',
]);
}
public function testHTTPS(): void
{
$this->performList('https://myhost.com/test', [
'https://myhost.com/index.php/test/?page=22&list=1',
]);
}
public function testHTTPSWithSubDomain(): void
{
$this->performList('https://mysubdomain.myhost.com/test', [
'https://mysubdomain.myhost.com/index.php/test/?page=22&list=1',
]);
}
public function testHTTPWithSubdirectory(): void
{
$this->performList('http://myhost.com/web/test', [
'http://myhost.com/web/index.php/test',
'http://myhost.com/web/index.php/test/',
'http://myhost.com/web/index.php/test?page=22&list=1',
'http://myhost.com/web/index.php/test?page=22&list=1/',
], $this->fakeServer(
'/var/www/web/index.php',
'/web/index.php'
));
$this->performList('http://myhost.com/web/test', [
'http://myhost.com/web/index_dev.php/test',
'http://myhost.com/web/index_dev.php/test/',
'http://myhost.com/web/index_dev.php/test?page=22&list=1',
'http://myhost.com/web/index_dev.php/test?page=22&list=1/',
], $this->fakeServer(
'/var/www/web/index_dev.php',
'/web/index_dev.php'
));
}
public function testHTTPWithMultipleSubdirectory(): void
{
$this->performList('http://myhost.com/web/web2/web3/test', [
'http://myhost.com/web/web2/web3/index.php/test/?page=22&list=1',
], $this->fakeServer(
'/var/www/web/web2/web3/index.php',
'/web/web2/web3/index.php'
));
$this->performList('http://myhost.com/web/web2/web3/test', [
'http://myhost.com/web/web2/web3/index_dev.php/test/?page=22&list=1',
], $this->fakeServer(
'/var/www/web/web2/web3/index_dev.php',
'/web/web2/web3/index_dev.php'
));
}
public function testHTTPSWithSubdirectory(): void
{
$this->performList('https://myhost.com/web/test', [
'https://myhost.com/web/index.php/test/?page=22&list=1',
], $this->fakeServer(
'/var/www/web/index.php',
'/web/index.php'
));
}
public function testHTTPSWithMultipleSubdirectory(): void
{
$this->performList('https://myhost.com/web/web2/web3/test', [
'https://myhost.com/web/web2/web3/index.php/test/?page=22&list=1',
], $this->fakeServer(
'/var/www/web/web2/web3/index.php',
'/web/web2/web3/index.php'
));
}
public function testWithNoPath(): void
{
$this->performList('http://myhost.com/?list=22&page=1', [
'http://myhost.com?list=22&page=1',
'http://myhost.com/?list=22&page=1',
'http://myhost.com/index.php?list=22&page=1',
'http://myhost.com/index.php/?list=22&page=1',
]);
$this->performList('http://myhost.com/?list=22&page=1', [
'http://myhost.com/index_dev.php?list=22&page=1',
'http://myhost.com/index_dev.php/?list=22&page=1',
], $this->fakeServer(
'/var/www/web/index_dev.php',
'/index_dev.php'
));
}
public function testWithNoPathAndMultipleSubdirectory(): void
{
$this->performList('http://myhost.com/web/?list=22&page=1', [
'http://myhost.com/web/index.php?list=22&page=1',
'http://myhost.com/web/?list=22&page=1',
'http://myhost.com/web/index.php?list=22&page=1',
'http://myhost.com/web/index.php/?list=22&page=1',
], $this->fakeServer(
'/var/www/web/index.php',
'/web/index.php'
));
$this->performList('http://myhost.com/web/?list=22&page=1', [
'http://myhost.com/web/index_dev.php?list=22&page=1',
'http://myhost.com/web/index_dev.php/?list=22&page=1',
], $this->fakeServer(
'/var/www/web/index_dev.php',
'/web/index_dev.php'
));
}
public function testWithNotRewrittenUrl(): void
{
$this->performList('http://myhost.com/web/?view=category&lang=fr_FR&category_id=48', [
'http://myhost.com/web/index.php?view=category&lang=fr_FR&category_id=48',
'http://myhost.com/web/?lang=fr_FR&view=category&category_id=48',
'http://myhost.com/web/index.php?&category_id=48&lang=fr_FR&view=category',
'http://myhost.com/web/index.php/?category_id=48&view=category&lang=fr_FR',
], $this->fakeServer(
'/var/www/web/index.php',
'/web/index.php'
));
}
public function testOverrideCanonicalEvent(): void
{
$canonicalUrlListener = new CanonicalUrlListener(Request::create('https://myhost.com/test'));
$event = new CanonicalUrlEvent();
// override canonical
$canonical = 'http://myscanonical.com';
$event->setUrl($canonical);
$canonicalUrlListener->generateUrlCanonical($event);
$this->assertEquals($canonical, $event->getUrl());
}
/**
* @param string $scriptFileName
* @param string $scriptName
*
* @return array
*/
protected function fakeServer(
$scriptFileName = '/var/www/web/index.php',
$scriptName = '/index.php'
) {
return [
'SCRIPT_FILENAME' => $scriptFileName,
'SCRIPT_NAME' => $scriptName,
];
}
/**
* @param string $canonicalExpected canonical expected
* @param array $list array of uri
*/
protected function performList($canonicalExpected, array $list, array $server = []): void
{
if (empty($server)) {
$server = $this->fakeServer();
}
foreach ($list as $uri) {
$canonicalUrlListener = new CanonicalUrlListener(
Request::create($uri, 'GET', [], [], [], $server)
);
$event = new CanonicalUrlEvent();
$canonicalUrlListener->generateUrlCanonical($event);
$this->assertEquals($canonicalExpected, $event->getUrl());
}
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "thelia/canonical-url-module",
"license": "LGPL-3.0+",
"type": "thelia-module",
"require": {
"thelia/installer": "~1.1"
},
"extra": {
"installer-name": "CanonicalUrl"
}
}

View File

@@ -0,0 +1,6 @@
{form_field form=$form field='canonical' }
<div class="form-group">
<label for="{$name}">{intl l="Surcharge de l'url canonique" d="canonicalurl.bo.default"}</label>
<input type="text" id="{$name}" name="{$name}" value="{$canonical}" class="form-control">
</div>
{/form_field}

View File

@@ -0,0 +1,6 @@
# 2.3.0-alpha1
- Moved the images from the directory 'media' in the module to thelia/local/media/images/carousel.
- The current images will be automatically copied in the new directory during the update of the module
- Removed AdminIncludes directory
- All html,js and css files are now in 'templates'

View File

@@ -0,0 +1,124 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel;
use Propel\Runtime\Connection\ConnectionInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Thelia\Install\Database;
use Thelia\Model\ConfigQuery;
use Thelia\Module\BaseModule;
/**
* Class Carousel.
*
* @author Franck Allimant <franck@cqfdev.fr>
*/
class Carousel extends BaseModule
{
public const DOMAIN_NAME = 'carousel';
/**
* @return bool true to continue module activation, false to prevent it
*/
public function preActivation(ConnectionInterface $con = null)
{
if (!self::getConfigValue('is_initialized', false)) {
$database = new Database($con);
$database->insertSql(null, [__DIR__.'/Config/TheliaMain.sql']);
self::setConfigValue('is_initialized', true);
}
return true;
}
public function destroy(ConnectionInterface $con = null, $deleteModuleData = false): void
{
$database = new Database($con);
$database->insertSql(null, [__DIR__.'/Config/sql/destroy.sql']);
}
public function getUploadDir()
{
$uploadDir = ConfigQuery::read('images_library_path');
if ($uploadDir === null) {
$uploadDir = THELIA_LOCAL_DIR.'media'.DS.'images';
} else {
$uploadDir = THELIA_ROOT.$uploadDir;
}
return $uploadDir.DS.self::DOMAIN_NAME;
}
/**
* @param string $currentVersion
* @param string $newVersion
*
* @author Thomas Arnaud <tarnaud@openstudio.fr>
*/
public function update($currentVersion, $newVersion, ConnectionInterface $con = null): void
{
$uploadDir = $this->getUploadDir();
$fileSystem = new Filesystem();
if (!$fileSystem->exists($uploadDir) && $fileSystem->exists(__DIR__.DS.'media'.DS.'carousel')) {
$finder = new Finder();
$finder->files()->in(__DIR__.DS.'media'.DS.'carousel');
$fileSystem->mkdir($uploadDir);
/** @var SplFileInfo $file */
foreach ($finder as $file) {
copy($file, $uploadDir.DS.$file->getRelativePathname());
}
$fileSystem->remove(__DIR__.DS.'media');
}
$finder = (new Finder())->files()->name('#.*?\.sql#')->sortByName()->in(__DIR__.DS.'Config'.DS.'update');
if (0 === $finder->count()) {
return;
}
$database = new Database($con);
// apply update only if table exists
if ($database->execute("SHOW TABLES LIKE 'carousel'")->rowCount() === 0) {
return;
}
/** @var SplFileInfo $updateSQLFile */
foreach ($finder as $updateSQLFile) {
if (version_compare($currentVersion, str_replace('.sql', '', $updateSQLFile->getFilename()), '<')) {
$database->insertSql(null, [$updateSQLFile->getPathname()]);
}
}
}
/**
* Defines how services are loaded in your modules.
*/
public static function configureServices(ServicesConfigurator $servicesConfigurator): void
{
$servicesConfigurator->load(self::getModuleCode().'\\', __DIR__)
->exclude([THELIA_MODULE_DIR.ucfirst(self::getModuleCode()).'/I18n/*'])
->autowire(true)
->autoconfigure(true);
}
}

View File

@@ -0,0 +1,51 @@
# This is a fix for InnoDB in MySQL >= 4.1.x
# It "suspends judgement" for fkey relationships until are tables are set.
SET FOREIGN_KEY_CHECKS = 0;
-- ---------------------------------------------------------------------
-- carousel
-- ---------------------------------------------------------------------
DROP TABLE IF EXISTS `carousel`;
CREATE TABLE `carousel`
(
`id` INTEGER NOT NULL AUTO_INCREMENT,
`file` VARCHAR(255),
`position` INTEGER,
`disable` INTEGER,
`group` VARCHAR(255),
`url` VARCHAR(255),
`limited` INTEGER,
`start_date` DATETIME,
`end_date` DATETIME,
`created_at` DATETIME,
`updated_at` DATETIME,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
-- ---------------------------------------------------------------------
-- carousel_i18n
-- ---------------------------------------------------------------------
DROP TABLE IF EXISTS `carousel_i18n`;
CREATE TABLE `carousel_i18n`
(
`id` INTEGER NOT NULL,
`locale` VARCHAR(5) DEFAULT 'en_US' NOT NULL,
`alt` VARCHAR(255),
`title` VARCHAR(255),
`description` LONGTEXT,
`chapo` TEXT,
`postscriptum` TEXT,
PRIMARY KEY (`id`,`locale`),
CONSTRAINT `carousel_i18n_fk_2ec1b2`
FOREIGN KEY (`id`)
REFERENCES `carousel` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB;
# This restores the fkey checks, after having unset them earlier
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns="http://thelia.net/schema/dic/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/config http://thelia.net/schema/dic/config/thelia-1.0.xsd">
<hooks>
<hook id="carousel.hook">
<tag name="hook.event_listener" event="home.body" type="front" templates="render:carousel.html" />
<tag name="hook.event_listener" event="module.configuration" type="back" templates="render:module_configuration.html" />
<tag name="hook.event_listener" event="module.config-js" type="back" templates="js:assets/js/module-configuration.js" />
</hook>
<hook id="carousel.hook.back" class="Carousel\Hook\BackHook">
<tag name="hook.event_listener" event="main.top-menu-tools" type="back" />
</hook>
</hooks>
</config>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="http://thelia.net/schema/dic/module"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/module http://thelia.net/schema/dic/module/module-2_1.xsd">
<fullnamespace>Carousel\Carousel</fullnamespace>
<descriptive locale="en_US">
<title>An image carousel</title>
</descriptive>
<descriptive locale="fr_FR">
<title>Un carrousel d'images</title>
</descriptive>
<languages>
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>2.5.4</version>
<author>
<name>Manuel Raynaud, Franck Allimant</name>
<email>manu@raynaud.io, franck@cqfdev.fr</email>
</author>
<type>classic</type>
<thelia>2.5.4</thelia>
<stability>alpha</stability>
</module>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
<!--
if a /admin/module/carousel/ route is provided, a "Configuration" button will be displayed
for the module in the module list. Clicking this button will invoke this route.
<route id="my_route_id" path="/admin/module/carousel">
<default key="_controller">Carousel\Full\Class\Name\Of\YourConfigurationController::methodName</default>
</route>
<route id="my_route_id" path="/admin/module/carousel/route-name">
<default key="_controller">Carousel\Full\Class\Name\Of\YourAdminController::methodName</default>
</route>
<route id="my_route_id" path="/my/route/name">
<default key="_controller">Carousel\Full\Class\Name\Of\YourOtherController::methodName</default>
</route>
...add as many routes as required.
<route>
...
</route>
-->
<route id="carousel.upload.image" path="/admin/module/carousel/upload" methods="post">
<default key="_controller">Carousel\Controller\ConfigurationController::uploadImage</default>
</route>
<route id="carousel.update" path="/admin/module/carousel/update" methods="post">
<default key="_controller">Carousel\Controller\ConfigurationController::updateAction</default>
</route>
<route id="carousel.delete" path="/admin/module/carousel/delete" methods="post">
<default key="_controller">Carousel\Controller\ConfigurationController::deleteAction</default>
</route>
</routes>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<database defaultIdMethod="native" name="TheliaMain" namespace="Carousel\Model">
<!--
See propel documentation on http://propelorm.org for all information about schema file
-->
<table name="carousel">
<column autoIncrement="true" name="id" primaryKey="true" required="true" type="INTEGER" />
<column name="file" type="VARCHAR" size="255" />
<column name="position" type="INTEGER" />
<column name="disable" type="INTEGER" />
<column name="group" size="255" type="VARCHAR" />
<column name="alt" size="255" type="VARCHAR" />
<column name="url" size="255" type="VARCHAR" />
<column name="title" size="255" type="VARCHAR" />
<column name="description" type="CLOB" />
<column name="chapo" type="LONGVARCHAR" />
<column name="postscriptum" type="LONGVARCHAR" />
<column name="limited" type="INTEGER" />
<column name="start_date" type="TIMESTAMP" />
<column name="end_date" type="TIMESTAMP" />
<behavior name="timestampable" />
<behavior name="i18n">
<parameter name="i18n_columns" value="alt, title, description, chapo, postscriptum" />
</behavior>
</table>
<external-schema filename="local/config/schema.xml" referenceOnly="true" />
</database>

View File

@@ -0,0 +1,6 @@
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `carousel`;
DROP TABLE IF EXISTS `carousel_i18n`;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,2 @@
# Sqlfile -> Database map
TheliaMain.sql=TheliaMain

View File

@@ -0,0 +1 @@
ALTER TABLE `carousel` ADD (`disable` INTEGER, `group` VARCHAR(255),`limited` INTEGER, `start_date` DATETIME, `end_date` DATETIME);

View File

@@ -0,0 +1,196 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel\Controller;
use Carousel\Form\CarouselImageForm;
use Carousel\Form\CarouselUpdateForm;
use Carousel\Model\Carousel;
use Carousel\Model\CarouselQuery;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Thelia\Controller\Admin\BaseAdminController;
use Thelia\Core\Event\File\FileCreateOrUpdateEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Core\Form\TheliaFormFactory;
use Thelia\Core\HttpFoundation\Request;
use Thelia\Core\Security\AccessManager;
use Thelia\Core\Security\Resource\AdminResources;
use Thelia\Form\Exception\FormValidationException;
use Thelia\Model\Lang;
use Thelia\Model\LangQuery;
use Thelia\Tools\URL;
/**
* Class ConfigurationController.
*
* @author manuel raynaud <mraynaud@openstudio.fr>
*/
class ConfigurationController extends BaseAdminController
{
public function uploadImage(
Request $request,
TheliaFormFactory $formFactory,
EventDispatcherInterface $eventDispatcher
) {
if (null !== $response = $this->checkAuth(AdminResources::MODULE, ['carousel'], AccessManager::CREATE)) {
return $response;
}
$form = $formFactory->createForm(CarouselImageForm::class);
$error_message = null;
try {
$formData = $this->validateForm($form)->getData();
/** @var UploadedFile $fileBeingUploaded */
$fileBeingUploaded = $formData['file'];
$fileModel = new Carousel();
$fileCreateOrUpdateEvent = new FileCreateOrUpdateEvent(1);
$fileCreateOrUpdateEvent->setModel($fileModel);
$fileCreateOrUpdateEvent->setUploadedFile($fileBeingUploaded);
$eventDispatcher->dispatch(
$fileCreateOrUpdateEvent,
TheliaEvents::IMAGE_SAVE
);
// Compensate issue #1005
$langs = LangQuery::create()->find();
/** @var Lang $lang */
foreach ($langs as $lang) {
$fileCreateOrUpdateEvent->getModel()->setLocale($lang->getLocale())->setTitle('')->save();
}
$response = $this->redirectToConfigurationPage();
} catch (FormValidationException $e) {
$error_message = $this->createStandardFormValidationErrorMessage($e);
}
if (null !== $error_message) {
$this->setupFormErrorContext(
'carousel upload',
$error_message,
$form
);
$response = $this->render(
'module-configure',
[
'module_code' => 'Carousel',
]
);
}
return $response;
}
/**
* @param Form $form
* @param string $fieldName
* @param int $id
*
* @return string
*/
protected function getFormFieldValue($form, $fieldName, $id)
{
$value = $form->get(sprintf('%s%d', $fieldName, $id))->getData();
return $value;
}
public function updateAction(
TheliaFormFactory $formFactory
) {
if (null !== $response = $this->checkAuth(AdminResources::MODULE, ['carousel'], AccessManager::UPDATE)) {
return $response;
}
$form = $formFactory->createForm(CarouselUpdateForm::class);
$error_message = null;
try {
$updateForm = $this->validateForm($form);
$carousels = CarouselQuery::create()->findAllByPosition();
$locale = $this->getCurrentEditionLocale();
/** @var Carousel $carousel */
foreach ($carousels as $carousel) {
$id = $carousel->getId();
$carousel
->setPosition($this->getFormFieldValue($updateForm, 'position', $id))
->setDisable($this->getFormFieldValue($updateForm, 'disable', $id))
->setUrl($this->getFormFieldValue($updateForm, 'url', $id))
->setLocale($locale)
->setTitle($this->getFormFieldValue($updateForm, 'title', $id))
->setAlt($this->getFormFieldValue($updateForm, 'alt', $id))
->setChapo($this->getFormFieldValue($updateForm, 'chapo', $id))
->setDescription($this->getFormFieldValue($updateForm, 'description', $id))
->setPostscriptum($this->getFormFieldValue($updateForm, 'postscriptum', $id))
->setGroup($this->getFormFieldValue($updateForm, 'group', $id))
->setLimited($this->getFormFieldValue($updateForm, 'limited', $id))
->setStartDate($this->getFormFieldValue($updateForm, 'start_date', $id))
->setEndDate($this->getFormFieldValue($updateForm, 'end_date', $id))
->save();
}
$response = $this->redirectToConfigurationPage();
} catch (FormValidationException $e) {
$error_message = $this->createStandardFormValidationErrorMessage($e);
}
if (null !== $error_message) {
$this->setupFormErrorContext(
'carousel upload',
$error_message,
$form
);
$response = $this->render('module-configure', ['module_code' => 'Carousel']);
}
return $response;
}
public function deleteAction(
Request $request
) {
if (null !== $response = $this->checkAuth(AdminResources::MODULE, ['carousel'], AccessManager::DELETE)) {
return $response;
}
$imageId = $request->get('image_id');
if ($imageId != '') {
$carousel = CarouselQuery::create()->findPk($imageId);
if (null !== $carousel) {
$carousel->delete();
}
}
return $this->redirectToConfigurationPage();
}
protected function redirectToConfigurationPage()
{
return new RedirectResponse(URL::getInstance()->absoluteUrl('/admin/module/Carousel'));
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel\Form;
use Carousel\Carousel;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\Image;
use Thelia\Core\Translation\Translator;
use Thelia\Form\BaseForm;
/**
* Class CarouselImageForm.
*
* @author manuel raynaud <mraynaud@openstudio.fr>
*/
class CarouselImageForm extends BaseForm
{
protected function buildForm(): void
{
$translator = Translator::getInstance();
$this->formBuilder
->add(
'file',
FileType::class,
[
'constraints' => [
new Image(),
],
'label' => $translator->trans('Carousel image', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'file',
],
]
);
}
}

View File

@@ -0,0 +1,222 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel\Form;
use Carousel\Carousel;
use Carousel\Model\CarouselQuery;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Thelia\Form\BaseForm;
/**
* Class CarouselUpdateForm.
*
* @author manuel raynaud <mraynaud@openstudio.fr>
*/
class CarouselUpdateForm extends BaseForm
{
protected function buildForm(): void
{
$formBuilder = $this->formBuilder;
$carousels = CarouselQuery::create()->orderByPosition()->find();
/** @var \Carousel\Model\Carousel $carousel */
foreach ($carousels as $carousel) {
$id = $carousel->getId();
$formBuilder->add(
'position'.$id,
TextType::class,
[
'label' => $this->translator->trans('Image position in carousel', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'position'.$id,
],
'required' => false,
'attr' => [
'placeholder' => $this->translator->trans(
'Image position in carousel',
[],
Carousel::DOMAIN_NAME
),
],
]
)->add(
'alt'.$id,
TextType::class,
[
'label' => $this->translator->trans('Alternative image text', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'alt'.$id,
],
'required' => false,
'attr' => [
'placeholder' => $this->translator->trans(
'Displayed when image is not visible',
[],
Carousel::DOMAIN_NAME
),
],
]
)->add(
'group'.$id,
TextType::class,
[
'label' => $this->translator->trans('Group image', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'group'.$id,
],
'required' => false,
'attr' => [
'placeholder' => $this->translator->trans(
'Group of images',
[],
Carousel::DOMAIN_NAME
),
],
]
)->add(
'url'.$id,
UrlType::class,
[
'label' => $this->translator->trans('Image URL', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'url'.$id,
],
'required' => false,
'attr' => [
'placeholder' => $this->translator->trans(
'Please enter a valid URL',
[],
Carousel::DOMAIN_NAME
),
],
]
)->add(
'title'.$id,
TextType::class,
[
'constraints' => [],
'required' => false,
'label' => $this->translator->trans('Title', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'title_field'.$id,
],
'attr' => [
'placeholder' => $this->translator->trans('A descriptive title', [], Carousel::DOMAIN_NAME),
],
]
)->add(
'chapo'.$id,
TextareaType::class,
[
'constraints' => [],
'required' => false,
'label' => $this->translator->trans('Summary', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'summary_field'.$id,
'help' => $this->translator->trans(
'A short description, used when a summary or an introduction is required',
[],
Carousel::DOMAIN_NAME
),
],
'attr' => [
'rows' => 3,
'placeholder' => $this->translator->trans('Short description text', [], Carousel::DOMAIN_NAME),
],
]
)->add(
'description'.$id,
TextareaType::class,
[
'constraints' => [],
'required' => false,
'label' => $this->translator->trans('Detailed description', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'detailed_description_field'.$id,
'help' => $this->translator->trans('The detailed description.', [], Carousel::DOMAIN_NAME),
],
'attr' => [
'rows' => 5,
],
]
)->add(
'disable'.$id,
CheckboxType::class,
[
'required' => false,
'label' => $this->translator->trans('Disable image', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'enable'.$id,
],
]
)->add(
'limited'.$id,
CheckboxType::class,
[
'required' => false,
'label' => $this->translator->trans('Limited', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'limited'.$id,
],
]
)->add(
'start_date'.$id,
DateTimeType::class,
[
'label' => $this->translator->trans('Start date', [], Carousel::DOMAIN_NAME),
'widget' => 'single_text',
'required' => false,
]
)->add(
'end_date'.$id,
DateTimeType::class,
[
'label' => $this->translator->trans('End date', [], Carousel::DOMAIN_NAME),
'widget' => 'single_text',
'required' => false,
]
)->add(
'postscriptum'.$id,
TextareaType::class,
[
'constraints' => [],
'required' => false,
'label' => $this->translator->trans('Conclusion', [], Carousel::DOMAIN_NAME),
'label_attr' => [
'for' => 'conclusion_field'.$id,
'help' => $this->translator->trans(
'A short text, used when an additional or supplemental information is required.',
[],
Carousel::DOMAIN_NAME
),
],
'attr' => [
'placeholder' => $this->translator->trans('Short additional text', [], Carousel::DOMAIN_NAME),
'rows' => 3,
],
]
);
}
}
public static function getName()
{
return 'carousel_update';
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel\Hook;
use Carousel\Carousel;
use Thelia\Core\Event\Hook\HookRenderBlockEvent;
use Thelia\Core\Hook\BaseHook;
use Thelia\Tools\URL;
/**
* Class BackHook.
*
* @author Emmanuel Nurit <enurit@openstudio.fr>
*/
class BackHook extends BaseHook
{
/**
* Add a new entry in the admin tools menu.
*
* should add to event a fragment with fields : id,class,url,title
*/
public function onMainTopMenuTools(HookRenderBlockEvent $event): void
{
$event->add(
[
'id' => 'tools_menu_carousel',
'class' => '',
'url' => URL::getInstance()->absoluteUrl('/admin/module/Carousel'),
'title' => $this->trans('Edit your carousel', [], Carousel::DOMAIN_NAME),
]
);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Add an image to the carousel' => 'Ein Bild zu Karussell hinzufügen',
'Add this image to the carousel' => 'Dieses Bild zu Karussell hinzufügen',
'Carousel image' => 'Karussell-Bild',
'Carousel images' => 'Karussell-Bilder',
'Delete a carousel image' => 'Ein Karussell-Bild löschen',
'Do you really want to remove this image from the carousel ?' => 'Wollen Sie dieses Bild wirklich aus dem Karussell entfernen?',
'Edit your carousel.' => 'Karussell bearbeiten.',
'Remove this image' => 'Dieses Bild entfernen',
'Your carousel contains no image. Please add one using the form above.' => 'Das Karussell enthält kein Bild. Bitte fügen Sie mit dem Formular oben eines hinzu.',
'Position' => 'Position',
];

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Add an image to the carousel' => 'Add an image to the carousel',
'Add this image to the carousel' => 'Add this image to the carousel',
'Carousel image' => 'Carousel image',
'Carousel images' => 'Carousel images',
'Delete a carousel image' => 'Delete a carousel image',
'Do you really want to remove this image from the carousel ?' => 'Do you really want to remove this image from the carousel ?',
'Edit your carousel.' => 'Edit your carousel.',
'Remove this image' => 'Remove this image',
'Your carousel contains no image. Please add one using the form above.' => 'Your carousel contains no image. Please add one using the form above.',
'Position' => 'Position',
'YYYY-MM-DD' => 'YYYY-MM-DD',
];

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Add an image to the carousel' => 'Ajouter une image au carrousel',
'Add this image to the carousel' => 'Ajouter l\'image au carrousel',
'Carousel image' => 'Image du carrousel',
'Carousel images' => 'Images du carrousel',
'Delete a carousel image' => 'Supprimer une image du carrousel',
'Do you really want to remove this image from the carousel ?' => 'Voulez-vous vraiment retirer cette image du carrousel ?',
'Edit your carousel.' => 'Modifier votre carrousel',
'Remove this image' => 'Supprimer cette image',
'Your carousel contains no image. Please add one using the form above.' => 'Votre carrousel ne contient aucune image. Ajoutez votre première image avec le formulaire ci-dessus',
'Position' => 'Position',
'YYYY-MM-DD' => 'AAAA-MM-JJ',
];

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Add an image to the carousel' => 'Добавить изображение в карусель',
'Add this image to the carousel' => 'Добавить это изображение в карусель',
'Carousel image' => 'Изображение карусели',
'Carousel images' => 'Изображения карусели',
'Delete a carousel image' => 'Удалить изображение карусели',
'Do you really want to remove this image from the carousel ?' => 'Вы действительно хотите удалить это изображение из карусели ?',
'Edit your carousel.' => 'Редактировать вашу карусель.',
'Remove this image' => 'Удалить это изображение',
'Your carousel contains no image. Please add one using the form above.' => 'Ваша карусель не содержит изображений. Пожалуйста, добавьте одно используя форму ниже.',
'Position' => 'Позиция',
];

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Add an image to the carousel' => 'Slayt için bir resim ekle',
'Add this image to the carousel' => 'slayt için bu resim ekleme',
'Carousel image' => 'slayt görüntü',
'Carousel images' => 'slayt görüntüleri',
'Delete a carousel image' => 'Bir slayt resmi silme',
'Do you really want to remove this image from the carousel ?' => 'Bu görüntüyü slayttan kaldırmak istiyor musunuz?',
'Edit your carousel.' => 'slayt düzenleyin.',
'Remove this image' => 'Bu resmi kaldırma',
'Your carousel contains no image. Please add one using the form above.' => 'Senin slayt hiçbir görüntü içermiyor . Lütfen yukarıdaki formu kullanarak ekleyin.',
'Position' => 'Pozisyon',
];

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'A descriptive title' => 'Beschreibungstitel',
'A short description, used when a summary or an introduction is required' => 'Eine kurze beschreibung, benutzt wenn eine Zusammenfassung order eine Einleitung ist nötig',
'A short text, used when an additional or supplemental information is required.' => 'Ein kurzer Text, der verwendet wird, wenn eine zusätzliche oder ergänzende Information erforderlich ist.',
'Alternative image text' => 'Alternativer Bildtext',
'Carousel image' => 'Karussell-Bild',
'Conclusion' => 'Abschluss',
'Detailed description' => 'Detaillierte Beschreibung',
'Displayed when image is not visible' => 'Angezeigt, wenn das Bild nicht sichtbar ist',
'Image URL' => 'Bild-URL',
'Image position in carousel' => 'Position des Bildes im Karussell',
'Please enter a valid URL' => 'Bitte geben Sie eine gültige URL ein',
'Short additional text' => 'Kurzer zusätzlicher Text',
'Short description text' => 'Kurzes Beschreibungstext',
'Summary' => 'Zusammenfassung',
'The detailed description.' => 'Die detaillierte Beschreibung.',
'Title' => 'Titel',
];

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'A descriptive title' => 'A descriptive title',
'A short description, used when a summary or an introduction is required' => 'A short description, used when a summary or an introduction is required',
'A short text, used when an additional or supplemental information is required.' => 'A short text, used when an additional or supplemental information is required.',
'Alternative image text' => 'Alternative image text',
'Carousel image' => 'Carousel image',
'Conclusion' => 'Conclusion',
'Detailed description' => 'Detailed description',
'Displayed when image is not visible' => 'Displayed when image is not visible',
'Edit your carousel' => 'Edit your carousel',
'Image URL' => 'Image URL',
'Image position in carousel' => 'Image position in carousel',
'Please enter a valid URL' => 'Please enter a valid URL',
'Short additional text' => 'Short additional text',
'Short description text' => 'Short description text',
'Summary' => 'Summary',
'The detailed description.' => 'The detailed description.',
'Title' => 'Title',
'YYYY-MM-DD' => 'AAAA-MM-JJ',
];

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'A descriptive title' => 'Un titre descriptif',
'A short description, used when a summary or an introduction is required' => 'Une courte description, utilisée lorsqu\'un résumé ou une introduction est requise',
'A short text, used when an additional or supplemental information is required.' => 'Un texte court, utilisé quand une conclusion ou une information complémentaire est nécessaire.',
'Alternative image text' => 'Texte alternatif de l\'image',
'Carousel image' => 'Image du carrousel',
'Conclusion' => 'Conclusion',
'Detailed description' => 'Description détaillée',
'Disable image' => 'Désactiver l\'image',
'Displayed when image is not visible' => 'Affiché lorsque l\'image n\'est pas visible',
'Edit your carousel' => 'Modifier votre carousel',
'End date' => 'Date de fin',
'Group image' => 'Groupe de l\'image',
'Group of images' => 'Nom du groupe auquel l\'image appartient',
'Image URL' => 'URL de l\'image',
'Image position in carousel' => 'Position de l\'image dans le carrousel',
'Limited' => 'Afficher l\'image entre les dates ci-dessous',
'Please enter a valid URL' => 'Merci d\'indiquer une URL valide',
'Short additional text' => 'Un court texte supplémentaire',
'Short description text' => 'Un court texte de description',
'Start date' => 'Date de début',
'Summary' => 'Résumé',
'The detailed description.' => 'La description détaillée.',
'Title' => 'Titre',
];

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'A descriptive title' => 'Un titolo descrittivo',
'A short description, used when a summary or an introduction is required' => 'Una breve descrizione, utilizzata quando è necessario un sommario o un\'introduzione',
'Conclusion' => 'Conclusione',
'Detailed description' => 'Descrizione dettagliata',
'Summary' => 'Riassunto',
'The detailed description.' => 'La descrizione dettagliata.',
'Title' => 'Titolo',
];

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'A descriptive title' => 'Описательный заголовок',
'A short description, used when a summary or an introduction is required' => 'Краткое описание, используется когда необходимо',
'A short text, used when an additional or supplemental information is required.' => 'Краткий текст используемый, когда необходима дополнительной информации.',
'Alternative image text' => 'Альтернативный текст изображения',
'Carousel image' => 'Изображение карусели',
'Conclusion' => 'Заключение',
'Detailed description' => 'Детальное описание',
'Displayed when image is not visible' => 'Отображается когда изображения не видно',
'Edit your carousel' => 'Редактировать вашу карусель',
'Image URL' => 'URL изображения',
'Image position in carousel' => 'Позиция изображения в карусели',
'Please enter a valid URL' => 'Пожалуйста введите корректный URL',
'Short additional text' => 'Краткий дополнительный текст',
'Short description text' => 'Текст краткого описания',
'Summary' => 'Краткое описание',
'The detailed description.' => 'Детальное описание',
'Title' => 'Заголовок',
];

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'A descriptive title' => 'Açıklayıcı bir başlık',
'A short description, used when a summary or an introduction is required' => 'Bir Özeti veya giriş gerekli olduğunda kullanılan kısa bir açıklama',
'A short text, used when an additional or supplemental information is required.' => 'Bir ek ya da tamamlayıcı bilgi gerekli olduğunda kullanılan kısa bir metin.',
'Alternative image text' => 'Alternatif resim metini',
'Carousel image' => 'slayt görüntü',
'Conclusion' => 'Sonuç',
'Detailed description' => 'Detaylııklama',
'Displayed when image is not visible' => 'resim görünür olmadığında görüntülenen',
'Image URL' => 'Resim Bağlantı [Link]',
'Image position in carousel' => 'slayt bulunduğu resim',
'Please enter a valid URL' => 'Lütfen geçerli bir URL girin',
'Short additional text' => 'Kısa ek metin',
'Short description text' => 'Kısa açıklama metni',
'Summary' => 'Özet',
'The detailed description.' => 'Ayrıntılııklama.',
'Title' => 'Başlık',
];

View File

@@ -0,0 +1,237 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel\Loop;
use Carousel\Model\CarouselQuery;
use Propel\Runtime\ActiveQuery\Criteria;
use Thelia\Core\Event\Image\ImageEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Core\Template\Element\LoopResult;
use Thelia\Core\Template\Element\LoopResultRow;
use Thelia\Core\Template\Loop\Argument\Argument;
use Thelia\Core\Template\Loop\Argument\ArgumentCollection;
use Thelia\Core\Template\Loop\Image;
use Thelia\Log\Tlog;
use Thelia\Type\EnumListType;
use Thelia\Type\EnumType;
use Thelia\Type\TypeCollection;
/**
* Class CarouselLoop.
*
* @author manuel raynaud <mraynaud@openstudio.fr>
*/
class Carousel extends Image
{
protected function getArgDefinitions()
{
return new ArgumentCollection(
Argument::createIntTypeArgument('width'),
Argument::createIntTypeArgument('height'),
Argument::createIntTypeArgument('rotation', 0),
Argument::createAnyTypeArgument('background_color'),
Argument::createIntTypeArgument('quality'),
new Argument(
'resize_mode',
new TypeCollection(
new EnumType(['crop', 'borders', 'none'])
),
'none'
),
new Argument(
'order',
new TypeCollection(
new EnumListType(['alpha', 'alpha-reverse', 'manual', 'manual-reverse', 'random'])
),
'manual'
),
Argument::createAnyTypeArgument('effects'),
Argument::createBooleanTypeArgument('allow_zoom', false),
Argument::createBooleanTypeArgument('filter_disable_slides', true),
Argument::createAlphaNumStringTypeArgument('group'),
Argument::createAlphaNumStringTypeArgument('format')
);
}
/**
* @throws \Propel\Runtime\Exception\PropelException
*
* @return LoopResult
*/
public function parseResults(LoopResult $loopResult)
{
/** @var \Carousel\Model\Carousel $carousel */
foreach ($loopResult->getResultDataCollection() as $carousel) {
$imgSourcePath = $carousel->getUploadDir().DS.$carousel->getFile();
if (!file_exists($imgSourcePath)) {
Tlog::getInstance()->error(sprintf('Carousel source image file %s does not exists.', $imgSourcePath));
continue;
}
$startDate = $carousel->getStartDate();
$endDate = $carousel->getEndDate();
if ($carousel->getLimited()) {
$now = new \DateTime();
if ($carousel->getDisable()) {
if ($now > $startDate && $now < $endDate) {
$carousel
->setDisable(0)
->save();
}
} else {
if ($now < $startDate || $now > $endDate) {
$carousel
->setDisable(1)
->save();
}
}
}
if ($this->getFilterDisableSlides() && $carousel->getDisable()) {
continue;
}
$loopResultRow = new LoopResultRow($carousel);
$event = new ImageEvent();
$event->setSourceFilepath($imgSourcePath)
->setCacheSubdirectory('carousel');
switch ($this->getResizeMode()) {
case 'crop':
$resize_mode = \Thelia\Action\Image::EXACT_RATIO_WITH_CROP;
break;
case 'borders':
$resize_mode = \Thelia\Action\Image::EXACT_RATIO_WITH_BORDERS;
break;
case 'none':
default:
$resize_mode = \Thelia\Action\Image::KEEP_IMAGE_RATIO;
}
// Prepare tranformations
$width = $this->getWidth();
$height = $this->getHeight();
$rotation = $this->getRotation();
$background_color = $this->getBackgroundColor();
$quality = $this->getQuality();
$effects = $this->getEffects();
$format = $this->getFormat();
if (null !== $width) {
$event->setWidth($width);
}
if (null !== $height) {
$event->setHeight($height);
}
$event->setResizeMode($resize_mode);
if (null !== $rotation) {
$event->setRotation($rotation);
}
if (null !== $background_color) {
$event->setBackgroundColor($background_color);
}
if (null !== $quality) {
$event->setQuality($quality);
}
if (null !== $effects) {
$event->setEffects($effects);
}
if (null !== $format) {
$event->setFormat($format);
}
$event->setAllowZoom($this->getAllowZoom());
// Dispatch image processing event
$this->dispatcher->dispatch($event, TheliaEvents::IMAGE_PROCESS);
if ($startDate) {
$startDate = $startDate->format('Y-m-d').'T'.$startDate->format('H:i');
}
if ($endDate) {
$endDate = $endDate->format('Y-m-d').'T'.$endDate->format('H:i');
}
$loopResultRow
->set('ID', $carousel->getId())
->set('LOCALE', $this->locale)
->set('IMAGE_URL', $event->getFileUrl())
->set('ORIGINAL_IMAGE_URL', $event->getOriginalFileUrl())
->set('IMAGE_PATH', $event->getCacheFilepath())
->set('ORIGINAL_IMAGE_PATH', $event->getSourceFilepath())
->set('TITLE', $carousel->getVirtualColumn('i18n_TITLE'))
->set('CHAPO', $carousel->getVirtualColumn('i18n_CHAPO'))
->set('DESCRIPTION', $carousel->getVirtualColumn('i18n_DESCRIPTION'))
->set('POSTSCRIPTUM', $carousel->getVirtualColumn('i18n_POSTSCRIPTUM'))
->set('ALT', $carousel->getVirtualColumn('i18n_ALT'))
->set('URL', $carousel->getUrl())
->set('POSITION', $carousel->getPosition())
->set('DISABLE', $carousel->getDisable())
->set('GROUP', $carousel->getGroup())
->set('LIMITED', $carousel->getLimited())
->set('START_DATE', $startDate)
->set('END_DATE', $endDate)
;
$loopResult->addRow($loopResultRow);
}
return $loopResult;
}
/**
* this method returns a Propel ModelCriteria.
*
* @return \Propel\Runtime\ActiveQuery\ModelCriteria
*/
public function buildModelCriteria()
{
$search = CarouselQuery::create();
$group = $this->getGroup();
$this->configureI18nProcessing($search, ['ALT', 'TITLE', 'CHAPO', 'DESCRIPTION', 'POSTSCRIPTUM']);
$orders = $this->getOrder();
// Results ordering
foreach ($orders as $order) {
switch ($order) {
case 'alpha':
$search->addAscendingOrderByColumn('i18n_TITLE');
break;
case 'alpha-reverse':
$search->addDescendingOrderByColumn('i18n_TITLE');
break;
case 'manual-reverse':
$search->orderByPosition(Criteria::DESC);
break;
case 'manual':
$search->orderByPosition(Criteria::ASC);
break;
case 'random':
$search->clearOrderByColumns();
$search->addAscendingOrderByColumn('RAND()');
break 2;
break;
}
}
if ($group) {
$search->filterByGroup($group);
}
return $search;
}
}

View File

@@ -0,0 +1,120 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel\Model;
use Carousel\Model\Base\Carousel as BaseCarousel;
use Propel\Runtime\ActiveQuery\ModelCriteria;
use Propel\Runtime\Connection\ConnectionInterface;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Thelia\Files\FileModelInterface;
use Thelia\Files\FileModelParentInterface;
use Thelia\Form\BaseForm;
class Carousel extends BaseCarousel implements FileModelInterface
{
public function preDelete(ConnectionInterface $con = null)
{
$carousel = new \Carousel\Carousel();
$fs = new Filesystem();
try {
$fs->remove($carousel->getUploadDir().DS.$this->getFile());
return true;
} catch (IOException $e) {
return false;
}
}
/**
* Set file parent id.
*
* @param int $parentId parent id
*
* @return $this
*/
public function setParentId($parentId)
{
return $this;
}
/**
* Get file parent id.
*
* @return int parent id
*/
public function getParentId()
{
return $this->getId();
}
/**
* @return FileModelParentInterface the parent file model
*/
public function getParentFileModel()
{
return new static();
}
/**
* Get the ID of the form used to change this object information.
*
* @return BaseForm the form
*/
public function getUpdateFormId()
{
return 'carousel.image';
}
/**
* @return string the path to the upload directory where files are stored, without final slash
*/
public function getUploadDir()
{
$carousel = new \Carousel\Carousel();
return $carousel->getUploadDir();
}
/**
* @return string the URL to redirect to after update from the back-office
*/
public function getRedirectionUrl()
{
return '/admin/module/Carousel';
}
/**
* Get the Query instance for this object.
*
* @return ModelCriteria
*/
public function getQueryInstance()
{
return CarouselQuery::create();
}
/**
* @param bool $visible true if the file is visible, false otherwise
*
* @return FileModelInterface
*/
public function setVisible($visible)
{
// Not implemented
return $this;
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel\Model;
use Carousel\Model\Base\CarouselI18n as BaseCarouselI18n;
class CarouselI18n extends BaseCarouselI18n
{
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel\Model;
use Carousel\Model\Base\CarouselI18nQuery as BaseCarouselI18nQuery;
/**
* Skeleton subclass for performing query and update operations on the 'carousel_i18n' table.
*
* You should add additional methods to this class to meet the
* application requirements. This class will only be generated as
* long as it does not already exist in the output directory.
*/
class CarouselI18nQuery extends BaseCarouselI18nQuery
{
} // CarouselI18nQuery

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Carousel\Model;
use Carousel\Model\Base\CarouselQuery as BaseCarouselQuery;
/**
* Skeleton subclass for performing query and update operations on the 'carousel' table.
*
* You should add additional methods to this class to meet the
* application requirements. This class will only be generated as
* long as it does not already exist in the output directory.
*/
class CarouselQuery extends BaseCarouselQuery
{
public function findAllByPosition()
{
return $this->orderByPosition()
->find();
}
} // CarouselQuery

View File

@@ -0,0 +1,69 @@
# Carousel
This module for Thelia add a customizable carousel on your home page. You can upload you own image and overload the default template in your template for using the carousel.
## Installation
* Copy the module into ```<thelia_root>/local/modules/``` directory and be sure that the name of the module is Carousel.
* Activate it in your thelia administration panel
## Usage
In the configuration panel of this module, you can upload as many images as you want.
## Hook
The carousel is installed in the "Home page - main area" (home.body) hook.
## Loop
Customize images with the `carousel` loop, which has the same arguments as the `image` loop. You can define a width, a height, and many other parameters
### Input arguments
|Argument |Description |
|--- |--- |
|**width** | A width in pixels, for resizing image. If only the width is provided, the image ratio is preserved. Example : width="200" |
|**height** | A height in pixels, for resizing image. If only the height is provided, the image ratio is preserved. example : height="200" |
|**rotation** |The rotation angle in degrees (positive or negative) applied to the image. The background color of the empty areas is the one specified by 'background_color'. example : rotation="90" |
|**background_color** |The color applied to empty image parts during processing. Use $rgb or $rrggbb color format. example : background_color="$cc8000"|
|**quality** |The generated image quality, from 0(!) to 100%. The default value is 75% (you can hange this in the Administration panel). example : quality="70"|
|**resize_mode** | If 'crop', the image will have the exact specified width and height, and will be cropped if required. If 'borders', the image will have the exact specified width and height, and some borders may be added. The border color is the one specified by 'background_color'. If 'none' or missing, the image ratio is preserved, and depending od this ratio, may not have the exact width and height required. resize_mode="crop"|
|**effects** |One or more comma separated effects definitions, that will be applied to the image in the specified order. Please see below a detailed description of available effects. Expected values :<ul><li>gamma:value : change the image Gamma to the specified value. Example: gamma:0.7.</li><li>grayscale or greyscale : switch image to grayscale.</li><li>colorize:color : apply a color mask to the image. The color format is $rgb or $rrggbb. Example: colorize:$ff2244.</li><li>negative : transform the image in its negative equivalent.</li><li>vflip or vertical_flip : flip the image vertically.</li><li>hflip or horizontal_flip : flip the image horizontally.</li></ul>example : effects="greyscale,gamma:0.7,vflip" |
|**group** |The name of an image group. Return only images from the specified group|
|**filter_disable_slides** |if true (the default), the disabled slides will not be displayed|
### Ouput arguments
|Variable |Description |
|--- |--- |
|$ID |the image ID |
|$IMAGE_URL |The absolute URL to the generated image |
|$ORIGINAL_IMAGE_URL |The absolute URL to the original image |
|$IMAGE_PATH |The absolute path to the generated image file |
|$ORIGINAL_IMAGE_PATH |The absolute path to the original image file |
|$ALT |alt text |
|$TITLE |the slide title |
|$CHAPO |the slide summary |
|$DESCRIPTION |the slide description |
|$POSTSCRIPTUM |the slide conclusion |
|$LOCALE |the textual elements locale |
|$POSITION |the slide position in the carousel |
|$URL |the related URL |
|$LIMITED| true if slide is disabled, false otherwise |
|$START_DATE| limited slide display start date |
|$END_DATE| limited slide display end date |
|$DISABLE| true if slide display is limited |
|$GROUP| name of the group the slide belong to |
### Exemple
```
{loop type="carousel" name="carousel.front" width="1200" height="390" resize_mode="borders"}
<img src="{$IMAGE_URL}" alt="{$ALT}">
{/loop}
```
## How to override ?
If you want your own carousel in your tempalte, create the directory ```modules/Carousel``` then create the template ```carousel.html``` in this directory. Here you can create your own carousel and the replace the default template provided in the module.

View File

@@ -0,0 +1,11 @@
{
"name": "thelia/carousel-module",
"license": "LGPL-3.0+",
"type": "thelia-module",
"require": {
"thelia/installer": "~1.1"
},
"extra": {
"installer-name": "Carousel"
}
}

View File

@@ -0,0 +1,6 @@
$(function() {
// Set proper image ID in delete from
$('a.image-delete').click(function(ev) {
$('#image_delete_id').val($(this).data('id'));
});
});

View File

@@ -0,0 +1,179 @@
<div class="general-block-decorator">
<div class="row">
<div class="col-md-12 title title-without-tabs">
{intl l='Edit your carousel.' d='carousel.bo.default'}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-container">
{form name=Carousel\Form\CarouselImageForm::getName()}
<form method="POST" action="{url path="/admin/module/carousel/upload"}" {form_enctype} class="clearfix">
{form_hidden_fields}
{form_field field='file'}
<div class="form-group {if $error}has-error{/if}">
<label for="{$label_attr.for|default:null}" class="control-label">{intl d='carousel.bo.default' l='Add an image to the carousel'}</label>
<div class="input-group">
<input type="file" id="{$label_attr.for|default:null}" {if $required}required="required"{/if} name="{$name}" value="{$value}" title="{intl l='Carousel image' d='carousel.bo.default'}" placeholder="{intl l='Carousel image' d='carousel.bo.default'}" class="form-control">
<span class="input-group-btn">
<input type="submit" class="form-submit-button btn btn-sm btn-success" value="{intl d='carousel.bo.default' l='Add this image to the carousel'}" >
</span>
</div>
</div>
{/form_field}
</form>
{/form}
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 title title-without-tabs">
{intl l='Carousel images' d='carousel.bo.default'}
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-container">
{ifloop rel="carousel.image"}
{form name="Carousel\Form\CarouselUpdateForm"}
<form method="post" action="{url path="/admin/module/carousel/update"}" {form_enctype} class="clearfix">
{include
file = "includes/inner-form-toolbar.html"
page_url = "{url path='/admin/module/Carousel'}"
close_url = "{url path='/admin/modules'}"
}
{form_hidden_fields}
{loop name="carousel.image" type="carousel" width="550" height="200" resize_mode="borders" backend_context="1" lang="$edit_language_id" filter_disable_slides=false}
<div class="well well-sm">
<div class="row">
<div class="col-md-6">
<p>
<a href="{$ORIGINAL_IMAGE_URL}" class="thumbnail" target="_blank">
<img src="{$IMAGE_URL}" alt="{$ALT}">
</a>
</p>
<div class="btn-group">
<a class="btn btn-default btn-sm image-delete" href="#delete_carousel_dialog" data-toggle="modal" data-id="{$ID}">
<i class="glyphicon glyphicon-trash"></i> {intl d='carousel.bo.default' l='Remove this image'}
</a>
</div>
<div class="pull-right row" style="width:170px">
<div class="col-xs-5" style="padding-top:5px">
<label for="position{$ID}">{intl d='carousel.bo.default' l='Position'}:</label>
</div>
<div class="col-xs-7">
{form_field field="position{$ID}"}
<input id="position{$ID}" class="form-control" type="number" min="1" name="{$name}" value={$POSITION}>
{/form_field}
</div>
</div>
<div style="padding-top: 10px">
<div class="row col-md-12">
{form_field field="disable{$ID}"}
<input id="disable{$ID}" type="checkbox" name="{$name}" {if $DISABLE}checked{/if}>
<label for="disable{$ID}">{$label}</label>
{/form_field}
</div>
<div class="row col-md-12">
{form_field field="limited{$ID}"}
<input id="limited{$ID}" type="checkbox" name="{$name}" {if $LIMITED}checked{/if}>
<label for="limited{$ID}">{$label}</label>
{/form_field}
</div>
<div class="row col-md-6">
{form_field field="start_date{$ID}"}
<label class="row col-md-12" for="{$name}">{$label}</label>
<div class="col-md-10" style="padding: 0">
<input name="{$name}"
placeholder="{intl l='YYYY-MM-DD' d='carousel.bo.default'}"
id="start_date{$ID}"
type="datetime-local"
class="form-control datetime-picker"
value="{$START_DATE}"/>
</div>
{/form_field}
</div>
<div class="row col-md-6">
{form_field field="end_date{$ID}"}
<label class="row col-md-12" for="{$name}">{$label}</label>
<div class="col-md-10" style="padding: 0">
<input name="{$name}"
placeholder="{intl l='YYYY-MM-DD' d='carousel.bo.default'}"
id="end_date{$ID}"
type="datetime-local"
class="form-control datetime-picker"
value="{$END_DATE}"/>
</div>
{/form_field}
</div>
</div>
</div>
<div class="col-md-6">
{* Not yet implemented
{render_form_field field="chapo{$ID} value=$CHAPO"}
*}
{render_form_field field="title{$ID}" value=$TITLE}
{render_form_field field="alt{$ID}" value=$ALT}
{render_form_field field="url{$ID}" value=$URL}
{render_form_field field="description{$ID}" extra_class="wysiwyg" value=$DESCRIPTION}
{render_form_field field="group{$ID}" value=$GROUP}
{* Not yet implemented
{render_form_field field="postscriptum{$ID}" value=$POSTSCRIPTUM}
*}
</div>
</div>
</div>
{/loop}
{include
file = "includes/inner-form-toolbar.html"
page_url = "{url path='/admin/module/Carousel'}"
close_url = "{url path='/admin/modules'}"
page_bottom = true
}
</form>
{/form}
{/ifloop}
{elseloop rel="carousel.image"}
<div class="alert alert-info">
{intl d='carousel.bo.default' l="Your carousel contains no image. Please add one using the form above."}
</div>
{/elseloop}
</div>
</div>
</div>
</div>
{capture "delete_dialog"}
<input type="hidden" name="image_id" id="image_delete_id" value="" />
{/capture}
{include
file = "includes/generic-confirm-dialog.html"
dialog_id = "delete_carousel_dialog"
dialog_title = {intl l="Delete a carousel image" d="carousel.bo.default"}
dialog_message = {intl l="Do you really want to remove this image from the carousel ?" d="carousel.bo.default"}
form_action = {url path='/admin/module/carousel/delete'}
form_content = {$smarty.capture.delete_dialog nofilter}
}

View File

@@ -0,0 +1,24 @@
{ifloop rel="carousel.front"}
<section class="carousel-container">
<div id="carousel" class="carousel slide" data-ride="carousel">
<div class="carousel-wrapper">
<div class="carousel-inner">
{loop type="carousel" name="carousel.front" width="1200" height="390" resize_mode="borders"}
<figure class="item {if $LOOP_COUNT == 1}active{/if}">
{if $URL}<a href="{$URL|default:'#'}">{/if}
<img src="{$IMAGE_URL}" alt="{$ALT}">
{if $URL}</a>{/if}
<div class="carousel-caption">
{if $TITLE}<h3>{$TITLE}</h3>{/if}
{if $DESCRIPTION}{$DESCRIPTION nofilter}{/if}
</div>
</figure>
{/loop}
</div>
</div>
<a class="left carousel-control" href="#carousel" data-slide="prev"><span class="icon-prev"></span></a>
<a class="right carousel-control" href="#carousel" data-slide="next"><span class="icon-next"></span></a>
</div>
</section><!-- #carousel -->
{/ifloop}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Cheque;
use Propel\Runtime\Connection\ConnectionInterface;
use Thelia\Install\Database;
use Thelia\Model\MessageQuery;
use Thelia\Model\Order;
use Thelia\Module\AbstractPaymentModule;
class Cheque extends AbstractPaymentModule
{
public const MESSAGE_DOMAIN = 'Cheque';
public function pay(Order $order): void
{
// Nothing special to to.
}
/**
* This method is call on Payment loop.
*
* If you return true, the payment method will de display
* If you return false, the payment method will not be display
*
* @return bool
*/
public function isValidPayment()
{
return $this->getCurrentOrderTotalAmount() > 0;
}
public function postActivation(ConnectionInterface $con = null): void
{
$database = new Database($con);
// Insert email message
$database->insertSql(null, [__DIR__.'/Config/setup.sql']);
}
public function destroy(ConnectionInterface $con = null, $deleteModuleData = false): void
{
// Delete our message
if (null !== $message = MessageQuery::create()->findOneByName('order_confirmation_cheque')) {
$message->delete($con);
}
parent::destroy($con, $deleteModuleData);
}
/**
* if you want, you can manage stock in your module instead of order process.
* Return false if you want to manage yourself the stock.
*
* @return bool
*/
public function manageStockOnCreation()
{
return false;
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns="http://thelia.net/schema/dic/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/config http://thelia.net/schema/dic/config/thelia-1.0.xsd">
<forms>
<form name="cheque.instructions.configure" class="Cheque\Form\ConfigurationForm" />
</forms>
<hooks>
<hook id="cheque.hook" class="Cheque\Hook\HookManager" scope="request">
<tag name="hook.event_listener" event="module.configuration" type="back" templates="render:module_configuration.html" />
<tag name="hook.event_listener" event="order-placed.additional-payment-info" type="front" method="onAdditionalPaymentInfo" />
</hook>
</hooks>
<services>
<service id="send.cheque.mail" class="Cheque\Listener\SendPaymentConfirmationEmail" scope="request">
<argument type="service" id="mailer"/>
<tag name="kernel.event_subscriber"/>
</service>
</services>
</config>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="http://thelia.net/schema/dic/module"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/module http://thelia.net/schema/dic/module/module-2_1.xsd">
<fullnamespace>Cheque\Cheque</fullnamespace>
<descriptive locale="en_US">
<title>Cheque</title>
</descriptive>
<descriptive locale="fr_FR">
<title>Cheque</title>
</descriptive>
<images-folder>images</images-folder>
<languages>
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>2.5.4</version>
<author>
<name>Manuel Raynaud</name>
<email>manu@raynaud.io</email>
</author>
<type>payment</type>
<thelia>2.5.4</thelia>
<stability>alpha</stability>
</module>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="cheque.configure" path="/admin/cheque/configure" methods="post">
<default key="_controller">Cheque\Controller\ConfigureController::configure</default>
</route>
</routes>

View File

@@ -0,0 +1,32 @@
-- ---------------------------------------------------------------------
-- Mail template for cheque
-- ---------------------------------------------------------------------
-- First, delete existing entries
SET @var := 0;
SELECT @var := `id` FROM `message` WHERE name="order_confirmation_cheque";
DELETE FROM `message` WHERE `id`=@var;
-- Then add new entries
SELECT @max := MAX(`id`) FROM `message`;
SET @max := @max+1;
-- insert message
INSERT INTO `message` (`id`, `name`, `secured`) VALUES
(@max,
'order_confirmation_cheque',
'0'
);
-- and mail templates
INSERT INTO `message_i18n` (`id`, `locale`, `title`, `subject`, `text_message`, `html_message`) VALUES
(@max,
'en_US',
'Confirmation of payment by cheque',
'Payment of order {$order_ref}', 'Dear customer,\r\nThis is a confirmation of the payment by cheque of your order {$order_ref} on our shop.\r\nYour invoice is now available in your customer account at {config key="url_site"}\r\nThank you again for your purchase.\r\nThe {config key="store_name"} team.', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r\n<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en">\r\n<head>\r\n <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\r\n <title>courriel de confirmation de commande de {config key="url_site"} </title>\r\n <style type="text/css">\r\n body {\r\n font-family: Arial, Helvetica, sans-serif;\r\n font-size: 100%;\r\n text-align: center;\r\n }\r\n #liencompte {\r\n margin: 15px 0;\r\n text-align: center;\r\n font-size: 10pt;\r\n }\r\n #wrapper {\r\n width: 480pt;\r\n margin: 0 auto;\r\n }\r\n #entete {\r\n padding-bottom: 20px;\r\n margin-bottom: 10px;\r\n border-bottom: 1px dotted #000;\r\n }\r\n #logotexte {\r\n float: left;\r\n width: 180pt;\r\n height: 75pt;\r\n border: 1pt solid #000;\r\n font-size: 18pt;\r\n text-align: center;\r\n }\r\n </style>\r\n</head>\r\n<body>\r\n<div id="wrapper">\r\n <div id="entete">\r\n <h1 id="logotexte">{config key="store_name"}</h1>\r\n <h2 id="info">The payment of your order is confirmed</h2>\r\n <h3 id="commande">Reference {$order_ref} </h3>\r\n </div>\r\n <p id="liencompte">\r\n Your invoice is now available in your customer account on\r\n <a href="{config key="url_site"}">{config key="store_name"}</a>.\r\n </p>\r\n <p>Thank you for your order !</p>\r\n <p>The {config key="store_name"} team.</p>\r\n</div>\r\n</body>\r\n</html>'
),
(@max,
'fr_FR',
'Confirmation de paiement par chèque',
'Paiement de la commande : {$order_ref}',
'Cher client,\r\nCe message confirme le paiement par chèque de votre commande numero {$order_ref} sur notre boutique.\r\nVotre facture est maintenant disponible dans votre compte client à l''adresse {config key="url_site"}\r\nMerci encore pour votre achat !\r\nL''équipe {config key="store_name"}', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r\n<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="fr">\r\n<head>\r\n <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\r\n <title>Confirmation du paiement de votre commande sur {config key="url_site"} </title>\r\n <style type="text/css">\r\n body {\r\n font-family: Arial, Helvetica, sans-serif;\r\n font-size: 100%;\r\n text-align: center;\r\n }\r\n #liencompte {\r\n margin: 15px 0;\r\n text-align: center;\r\n font-size: 10pt;\r\n }\r\n #wrapper {\r\n width: 480pt;\r\n margin: 0 auto;\r\n }\r\n #entete {\r\n padding-bottom: 20px;\r\n margin-bottom: 10px;\r\n border-bottom: 1px dotted #000;\r\n }\r\n #logotexte {\r\n float: left;\r\n width: 180pt;\r\n height: 75pt;\r\n border: 1pt solid #000;\r\n font-size: 18pt;\r\n text-align: center;\r\n }\r\n </style>\r\n</head>\r\n<body>\r\n<div id="wrapper">\r\n <div id="entete">\r\n <h1 id="logotexte">{config key="store_name"}</h1>\r\n <h2 id="info">Confirmation du paiement de votre commande</h2>\r\n <h3 id="commande">N&deg; {$order_ref}</h3>\r\n </div>\r\n <p id="liencompte">\r\n Le suivi de votre commande est disponible dans la rubrique mon compte sur\r\n <a href="{config key="url_site"}">{config key="url_site"}</a>\r\n </p>\r\n <p>Merci pour votre achat !</p>\r\n <p>L''équipe {config key="store_name"}</p>\r\n</div>\r\n</body>\r\n</html>'
);

View File

@@ -0,0 +1,84 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Cheque\Controller;
use Cheque\Cheque;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Thelia\Controller\Admin\BaseAdminController;
use Thelia\Core\Security\AccessManager;
use Thelia\Core\Security\Resource\AdminResources;
use Thelia\Form\Exception\FormValidationException;
use Thelia\Tools\URL;
/**
* Class SetTransferConfig.
*
* @author Thelia <info@thelia.net>
*/
class ConfigureController extends BaseAdminController
{
public function configure()
{
if (null !== $response = $this->checkAuth(AdminResources::MODULE, 'Cheque', AccessManager::UPDATE)) {
return $response;
}
// Initialize the potential exception
$ex = null;
// Create the Form from the request
$configurationForm = $this->createForm('cheque.instructions.configure');
try {
// Check the form against constraints violations
$form = $this->validateForm($configurationForm, 'POST');
// Get the form field values
$data = $form->getData();
Cheque::setConfigValue('instructions', $data['instructions'], $this->getCurrentEditionLocale());
Cheque::setConfigValue('payable_to', $data['payable_to']);
// Log configuration modification
$this->adminLogAppend(
'cheque.configuration.message',
AccessManager::UPDATE,
'Cheque instructions configuration updated'
);
// Everything is OK.
return new RedirectResponse(URL::getInstance()->absoluteUrl('/admin/module/Cheque'));
} catch (FormValidationException $ex) {
// Form cannot be validated. Create the error message using
// the BaseAdminController helper method.
$error_msg = $this->createStandardFormValidationErrorMessage($ex);
} catch (\Exception $ex) {
// Any other error
$error_msg = $ex->getMessage();
}
// At this point, the form has errors, and should be redisplayed. We don not redirect,
// just redisplay the same template.
// Setup the Form error context, to make error information available in the template.
$this->setupFormErrorContext(
$this->getTranslator()->trans('Cheque instructions configuration', [], Cheque::MESSAGE_DOMAIN),
$error_msg,
$configurationForm,
$ex
);
// Do not redirect at this point, or the error context will be lost.
// Just redisplay the current template.
return $this->render('module-configure', ['module_code' => 'Cheque']);
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Cheque\Form;
use Cheque\Cheque;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\NotBlank;
use Thelia\Core\Translation\Translator;
use Thelia\Form\BaseForm;
/**
* Class ConfigurationForm.
*
* @author Thelia <info@thelia.net>
*/
class ConfigurationForm extends BaseForm
{
protected function trans($str, $params = [])
{
return Translator::getInstance()->trans($str, $params, Cheque::MESSAGE_DOMAIN);
}
protected function buildForm(): void
{
$this->formBuilder
->add(
'payable_to',
TextType::class,
[
'constraints' => [new NotBlank()],
'label' => $this->trans('Cheque is payable to: '),
'label_attr' => [
'for' => 'payable_to',
'help' => $this->trans('The name to which the cheque shoud be payable to.'),
],
'attr' => [
'rows' => 10,
'placeholder' => $this->trans('Pay cheque to'),
],
]
)
->add(
'instructions',
TextareaType::class,
[
'constraints' => [],
'required' => false,
'label' => $this->trans('Cheque instructions'),
'label_attr' => [
'for' => 'namefield',
'help' => $this->trans('Please enter here the payment by cheque instructions'),
],
'attr' => [
'rows' => 10,
'placeholder' => $this->trans('Payment instruction'),
],
]
)
;
}
/**
* @return string the name of you form. This name must be unique
*/
public static function getName()
{
return 'cheque_configuration_instructions';
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Cheque\Hook;
use Thelia\Core\Event\Hook\HookRenderEvent;
use Thelia\Core\Hook\BaseHook;
/**
* Class HookManager.
*
* @author Franck Allimant <franck@cqfdev.fr>
*/
class HookManager extends BaseHook
{
public function onAdditionalPaymentInfo(HookRenderEvent $event): void
{
$content = $this->render('order-placed.additional-payment-info.html', [
'placed_order_id' => $event->getArgument('placed_order_id'),
]);
$event->add($content);
}
}

View File

@@ -0,0 +1,15 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Cheque instructions configuration' => 'Scheck-Anleitungen-Konfiguration',
];

View File

@@ -0,0 +1,15 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Cheque instructions configuration' => 'Cheque instructions configuration',
];

View File

@@ -0,0 +1,15 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Cheque instructions configuration' => 'Instructions de paiement par chèque',
];

View File

@@ -0,0 +1,15 @@
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
'Cheque instructions configuration' => 'Конфигурация инструкций для чека',
];

Some files were not shown because too many files have changed in this diff Show More