diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a01604a --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Composer +/composer.lock +/vendor/ + +# PhpUnit +/.phpunit.result.cache +/phpunit.xml + +# IDE +/.idea diff --git a/README.md b/README.md index 00b1d7e..28a834a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ --- -> Working with **Contao 4.9** and up to **Contao 4.13** (PHP ^7.4 and PHP 8) +> Working with **Contao 4.13** and **Contao 5.1** (PHP ^8.1) --- diff --git a/composer.json b/composer.json index 1944f7d..0788d4e 100644 --- a/composer.json +++ b/composer.json @@ -1,46 +1,74 @@ { - "name": "oveleon/contao-recommendation-bundle", - "type": "contao-bundle", - "description": "Recommendation integration for Contao 4 Open Source CMS", - "keywords": ["contao","recommendation-bundle"], - "homepage": "https://www.oveleon.de/", - "license": "MIT", - "authors": [ - { - "name": "Oveleon", - "homepage": "https://oveleon.de/", - "role": "Developer" - } - ], - "require": { - "php": "^7.4 || ^8.0", - "contao/core-bundle":"^4.9" + "name": "oveleon/contao-recommendation-bundle", + "type": "contao-bundle", + "description": "Recommendation integration for Contao Open Source CMS", + "keywords": [ + "contao", + "recommendation-bundle", + "recommendation", + "reviews" + ], + "homepage": "https://www.oveleon.de/", + "license": "MIT", + "authors": [ + { + "name": "Oveleon", + "homepage": "https://oveleon.de/", + "role": "Developer" }, - "require-dev": { - "contao/manager-plugin": "^2.0" + { + "name": "Sebastian Zoglowek", + "homepage": "https://github.com/zoglo", + "role": "Developer" }, - "conflict": { - "contao/core": "*", - "contao/manager-plugin": "<2.0 || >=3.0" + { + "name": "Fabian Ekert", + "homepage": "https://github.com/eki89", + "role": "Developer" + } + ], + "require": { + "php": "^8.1", + "contao/core-bundle": "^4.13 || ^5.1" + }, + "require-dev": { + "contao/manager-plugin": "^2.3.1", + "contao/test-case": "^5.1", + "phpunit/phpunit": "^9.5", + "symfony/http-client": "^5.4 || ^6.0", + "symfony/phpunit-bridge": "^5.4 || ^6.0" + }, + "conflict": { + "contao/core": "*", + "contao/manager-plugin": "<2.0 || >=3.0" + }, + "suggest": { + "oveleon/contao-google-recommendation-bundle": "This bundle imports Google reviews into the contao-recommendation-bundle" + }, + "autoload": { + "psr-4": { + "Oveleon\\ContaoRecommendationBundle\\": "src/" }, - "autoload": { - "psr-4": { - "Oveleon\\ContaoRecommendationBundle\\": "src/" - }, - "classmap": [ - "src/Resources/contao/" - ], - "exclude-from-classmap": [ - "src/Resources/contao/config/", - "src/Resources/contao/dca/", - "src/Resources/contao/languages/", - "src/Resources/contao/templates/" - ] + "classmap": [ + "contao/" + ], + "exclude-from-classmap": [ + "contao/config/", + "contao/dca/", + "contao/languages/", + "contao/templates/" + ] + }, + "extra": { + "branch-alias": { + "dev-main": "1.3.x-dev" }, - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - }, - "contao-manager-plugin": "Oveleon\\ContaoRecommendationBundle\\ContaoManager\\Plugin" + "contao-manager-plugin": "Oveleon\\ContaoRecommendationBundle\\ContaoManager\\Plugin" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "contao/manager-plugin": true } + } } diff --git a/config/services.yaml b/config/services.yaml new file mode 100644 index 0000000..e31ca06 --- /dev/null +++ b/config/services.yaml @@ -0,0 +1,9 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: true + + Oveleon\ContaoRecommendationBundle\: + resource: '../src/' + exclude: '../src/{Model,DependencyInjection,Resources}' diff --git a/contao/config/config.php b/contao/config/config.php new file mode 100644 index 0000000..36e49be --- /dev/null +++ b/contao/config/config.php @@ -0,0 +1,41 @@ + [ + 'tables' => ['tl_recommendation_archive', 'tl_recommendation'] + ] +]); + +ArrayUtil::arrayInsert($GLOBALS['BE_MOD']['system'], 3, [ + 'recommendation_settings' => [ + 'tables' => ['tl_recommendation_settings'], + 'hideInNavigation' => true + ] +]); + +// Front end modules +ArrayUtil::arrayInsert($GLOBALS['FE_MOD'], 2, [ + 'recommendation' => [ + 'recommendationform' => ModuleRecommendationForm::class, + 'recommendationlist' => ModuleRecommendationList::class, + 'recommendationreader' => ModuleRecommendationReader::class + ] +]); + +// Add permissions +$GLOBALS['TL_PERMISSIONS'][] = 'recommendations'; +$GLOBALS['TL_PERMISSIONS'][] = 'recommendationp'; + +// Models +$GLOBALS['TL_MODELS']['tl_recommendation'] = RecommendationModel::class; +$GLOBALS['TL_MODELS']['tl_recommendation_archive'] = RecommendationArchiveModel::class; diff --git a/src/Resources/contao/dca/tl_module.php b/contao/dca/tl_module.php similarity index 62% rename from src/Resources/contao/dca/tl_module.php rename to contao/dca/tl_module.php index a4571ee..131df1b 100644 --- a/src/Resources/contao/dca/tl_module.php +++ b/contao/dca/tl_module.php @@ -6,9 +6,15 @@ * (c) https://www.oveleon.de/ */ -Contao\System::loadLanguageFile('tl_recommendation'); -Contao\System::loadLanguageFile('tl_recommendation_notification'); -Contao\System::loadLanguageFile('tl_recommendation_list'); +use Contao\Backend; +use Contao\BackendUser; +use Contao\System; +use Contao\Controller; +use Oveleon\ContaoRecommendationBundle\Security\ContaoRecommendationPermissions; + +System::loadLanguageFile('tl_recommendation'); +System::loadLanguageFile('tl_recommendation_notification'); +System::loadLanguageFile('tl_recommendation_list'); // Add a palette selector $GLOBALS['TL_DCA']['tl_module']['palettes']['__selector__'][] = 'recommendation_activate'; @@ -21,222 +27,185 @@ $GLOBALS['TL_DCA']['tl_module']['subpalettes']['recommendation_activate'] = 'recommendation_activateJumpTo,recommendation_activateText'; // Add fields to tl_module -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_archives'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_archives'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_archives'] = [ 'exclude' => true, 'inputType' => 'checkbox', - 'options_callback' => array('tl_module_recommendation', 'getRecommendationArchives'), - 'eval' => array('multiple'=>true, 'mandatory'=>true), + 'options_callback' => ['tl_module_recommendation', 'getRecommendationArchives'], + 'eval' => ['multiple'=>true, 'mandatory'=>true], 'sql' => "blob NULL" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_archive'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_archive'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_archive'] = [ 'exclude' => true, 'inputType' => 'select', - 'options_callback' => array('tl_module_recommendation', 'getRecommendationArchives'), - 'eval' => array('mandatory'=>true, 'includeBlankOption'=>true, 'tl_class'=>'w50 clr'), + 'options_callback' => ['tl_module_recommendation', 'getRecommendationArchives'], + 'eval' => ['mandatory'=>true, 'includeBlankOption'=>true, 'tl_class'=>'w50 clr'], 'sql' => "int(10) unsigned NOT NULL default 0" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_featured'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_featured'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_featured'] = [ 'default' => 'all_items', 'exclude' => true, 'inputType' => 'select', - 'options' => array('all_items', 'featured', 'unfeatured', 'featured_first'), + 'options' => ['all_items', 'featured', 'unfeatured', 'featured_first'], 'reference' => &$GLOBALS['TL_LANG']['tl_recommendation_list'], - 'eval' => array('tl_class'=>'w50'), + 'eval' => ['tl_class'=>'w50'], 'sql' => "varchar(16) NOT NULL default ''" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_readerModule'] = array -( +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_readerModule'] = [ 'exclude' => true, 'inputType' => 'select', - 'options_callback' => array('tl_module_recommendation', 'getReaderModules'), + 'options_callback' => ['tl_module_recommendation', 'getReaderModules'], 'reference' => &$GLOBALS['TL_LANG']['tl_module'], - 'eval' => array('includeBlankOption'=>true, 'tl_class'=>'w50'), + 'eval' => ['includeBlankOption'=>true, 'tl_class'=>'w50'], 'sql' => "int(10) unsigned NOT NULL default 0" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_metaFields'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_metaFields'], - 'default' => array('date', 'author'), +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_metaFields'] = [ + 'default' => ['date', 'author'], 'exclude' => true, 'inputType' => 'checkbox', - 'options' => array('image', 'date', 'author', 'rating', 'location', 'customField'), + 'options' => ['image', 'date', 'author', 'rating', 'location', 'customField'], 'reference' => &$GLOBALS['TL_LANG']['tl_recommendation'], - 'eval' => array('multiple'=>true), + 'eval' => ['multiple'=>true], 'sql' => "varchar(255) NOT NULL default ''" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_order'] = array -( +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_order'] = [ 'exclude' => true, 'inputType' => 'select', - 'options' => array('order_date_asc', 'order_date_desc', 'order_random', 'order_rating_desc'), + 'options' => ['order_date_asc', 'order_date_desc', 'order_random', 'order_rating_desc'], 'reference' => &$GLOBALS['TL_LANG']['tl_recommendation_list'], - 'eval' => array('tl_class'=>'w50'), + 'eval' => ['tl_class'=>'w50'], 'sql' => "varchar(32) NOT NULL default 'order_date_desc'" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_minRating'] = array -( +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_minRating'] = [ 'exclude' => true, 'inputType' => 'select', - 'options' => array(1=> 'minOne', 2=>'minTwo', 3=>'minThree', 4=>'minFour', 5=>'minFive'), + 'options' => [1=> 'minOne', 2=>'minTwo', 3=>'minThree', 4=>'minFour', 5=>'minFive'], 'reference' => &$GLOBALS['TL_LANG']['tl_recommendation_list'], - 'eval' => array('tl_class'=>'w50'), + 'eval' => ['tl_class'=>'w50'], 'sql' => "char(1) NOT NULL default '1'" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_optionalFormFields'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_optionalFormFields'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_optionalFormFields'] = [ 'exclude' => true, 'inputType' => 'checkbox', - 'options' => array('title', 'location', 'email', 'customField'), + 'options' => ['title', 'location', 'email', 'customField'], 'reference' => &$GLOBALS['TL_LANG']['tl_recommendation'], - 'eval' => array('multiple'=>true, 'tl_class'=>'w50 clr'), + 'eval' => ['multiple'=>true, 'tl_class'=>'w50 clr'], 'sql' => "varchar(255) NOT NULL default ''" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_customFieldLabel'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_customFieldLabel'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_customFieldLabel'] = [ 'exclude' => true, 'inputType' => 'text', - 'eval' => array('maxlength'=>64, 'tl_class'=>'w50 clr'), + 'eval' => ['maxlength'=>64, 'tl_class'=>'w50 clr'], 'sql' => "varchar(64) NOT NULL default ''" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_notify'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_notify'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_notify'] = [ 'default' => true, 'exclude' => true, 'inputType' => 'checkbox', - 'eval' => array('tl_class'=>'w50 clr'), + 'eval' => ['tl_class'=>'w50 clr'], 'sql' => "char(1) NOT NULL default '1'" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_moderate'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_moderate'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_moderate'] = [ 'default' => true, 'exclude' => true, 'inputType' => 'checkbox', - 'eval' => array('tl_class'=>'w50'), + 'eval' => ['tl_class'=>'w50'], 'sql' => "char(1) NOT NULL default '1'" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_disableCaptcha'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_disableCaptcha'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_disableCaptcha'] = [ 'exclude' => true, 'inputType' => 'checkbox', - 'eval' => array('tl_class'=>'w50'), + 'eval' => ['tl_class'=>'w50'], 'sql' => "char(1) NOT NULL default ''" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_disableCaptcha'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_disableCaptcha'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_disableCaptcha'] = [ 'exclude' => true, 'inputType' => 'checkbox', - 'eval' => array('tl_class'=>'w50'), + 'eval' => ['tl_class'=>'w50'], 'sql' => "char(1) NOT NULL default ''" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_privacyText'] = array -( +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_privacyText'] = [ 'exclude' => true, 'inputType' => 'textarea', - 'eval' => array('style'=>'height:100px', 'allowHtml'=>true), + 'eval' => ['style'=>'height:100px', 'allowHtml'=>true], 'sql' => "text NULL" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_activate'] = array -( +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_activate'] = [ 'exclude' => true, 'inputType' => 'checkbox', - 'eval' => array('submitOnChange'=>true), + 'eval' => ['submitOnChange'=>true], 'sql' => "char(1) NOT NULL default ''" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_activateJumpTo'] = array -( +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_activateJumpTo'] = [ 'exclude' => true, 'inputType' => 'pageTree', 'foreignKey' => 'tl_page.title', - 'eval' => array('fieldType'=>'radio'), + 'eval' => ['fieldType'=>'radio'], 'sql' => "int(10) unsigned NOT NULL default 0", - 'relation' => array('type'=>'hasOne', 'load'=>'lazy') -); + 'relation' => ['type'=>'hasOne', 'load'=>'lazy'] +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_activateText'] = array -( +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_activateText'] = [ 'exclude' => true, 'inputType' => 'textarea', - 'eval' => array('style'=>'height:120px', 'decodeEntities'=>true, 'alwaysSave'=>true), - 'load_callback' => array - ( - array('tl_module_recommendation', 'getRecommendationActivationDefault') - ), + 'eval' => ['style'=>'height:120px', 'decodeEntities'=>true, 'alwaysSave'=>true], + 'load_callback' => + [ + ['tl_module_recommendation', 'getRecommendationActivationDefault'] + ], 'sql' => "text NULL" -); +]; -$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_template'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_template'], +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_template'] = [ 'exclude' => true, 'inputType' => 'select', 'options_callback' => static fn () => Controller::getTemplateGroup('recommendation_'), - 'eval' => array('includeBlankOption' => true, 'chosen' => true, 'tl_class'=>'w50'), + 'eval' => ['includeBlankOption' => true, 'chosen' => true, 'tl_class'=>'w50'], 'sql' => "varchar(64) NOT NULL default ''" -); - +]; -/** - * Provide miscellaneous methods that are used by the data configuration array. - * - * @author Fabian Ekert - */ -class tl_module_recommendation extends Contao\Backend +class tl_module_recommendation extends Backend { - /** * Import the back end user object */ public function __construct() { parent::__construct(); - $this->import('Contao\BackendUser', 'User'); + $this->import(BackendUser::class, 'User'); } /** * Get all recommendation archives and return them as array - * - * @return array */ - public function getRecommendationArchives() + public function getRecommendationArchives(): array { if (!$this->User->isAdmin && !is_array($this->User->recommendations)) { - return array(); + return []; } - $arrArchives = array(); + $arrArchives = []; $objArchives = $this->Database->execute("SELECT id, title FROM tl_recommendation_archive ORDER BY title"); + $security = System::getContainer()->get('security.helper'); while ($objArchives->next()) { - if ($this->User->hasAccess($objArchives->id, 'recommendations')) + if ($security->isGranted(ContaoRecommendationPermissions::USER_CAN_EDIT_ARCHIVE, $objArchives->id)) { $arrArchives[$objArchives->id] = $objArchives->title; } @@ -247,12 +216,10 @@ public function getRecommendationArchives() /** * Get all recommendation reader modules and return them as array - * - * @return array */ - public function getReaderModules() + public function getReaderModules(): array { - $arrModules = array(); + $arrModules = []; $objModules = $this->Database->execute("SELECT m.id, m.name, t.name AS theme FROM tl_module m LEFT JOIN tl_theme t ON m.pid=t.id WHERE m.type='recommendationreader' ORDER BY t.name, m.name"); while ($objModules->next()) @@ -265,12 +232,8 @@ public function getReaderModules() /** * Load the default recommendation activation text - * - * @param mixed $varValue - * - * @return mixed */ - public function getRecommendationActivationDefault($varValue) + public function getRecommendationActivationDefault(mixed $varValue): mixed { if (trim($varValue) === '') { diff --git a/contao/dca/tl_recommendation.php b/contao/dca/tl_recommendation.php new file mode 100644 index 0000000..28f655b --- /dev/null +++ b/contao/dca/tl_recommendation.php @@ -0,0 +1,261 @@ + [ + 'dataContainer' => DC_Table::class, + 'ptable' => 'tl_recommendation_archive', + 'switchToEdit' => true, + 'enableVersioning' => true, + 'onload_callback' => [ + [RecommendationListener::class, 'checkRecommendationPermission'], + [RecommendationListener::class, 'generateSitemap'] + ], + 'oncut_callback' => [ + [RecommendationListener::class, 'scheduleUpdate'] + ], + 'ondelete_callback' => [ + [RecommendationListener::class, 'scheduleUpdate'] + ], + 'onsubmit_callback' => [ + [RecommendationListener::class, 'adjustTime'], + [RecommendationListener::class, 'scheduleUpdate'] + ], + 'oninvalidate_cache_tags_callback' => [ + [RecommendationListener::class, 'addSitemapCacheInvalidationTag'], + ], + 'sql' => [ + 'keys' => [ + 'id' => 'primary', + 'alias' => 'index', + 'pid,start,stop,published' => 'index' + ] + ] + ], + + // List + 'list' => [ + 'sorting' => [ + 'mode' => DataContainer::MODE_PARENT, + 'fields' => ['date DESC'], + 'headerFields' => ['title', 'jumpTo', 'tstamp', 'protected'], + 'panelLayout' => 'filter;sort,search,limit', + 'child_record_callback' => [RecommendationListener::class, 'listRecommendations'], + 'child_record_class' => 'no_padding' + ], + 'global_operations' => [ + 'all' => [ + 'href' => 'act=select', + 'class' => 'header_edit_all', + 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"' + ] + ], + 'operations' => [ + 'edit' => [ + 'href' => 'act=edit', + 'icon' => 'edit.svg' + ], + 'copy' => [ + 'href' => 'act=paste&mode=copy', + 'icon' => 'copy.svg' + ], + 'cut' => [ + 'href' => 'act=paste&mode=cut', + 'icon' => 'cut.svg' + ], + 'delete' => [ + 'href' => 'act=delete', + 'icon' => 'delete.svg', + 'attributes' => 'onclick="if(!confirm(\'' . ($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? null) . '\'))return false;Backend.getScrollOffset()"' + ], + 'toggle' => [ + 'href' => 'act=toggle&field=published', + 'icon' => 'visible.svg', + 'showInHeader' => true + ], + 'feature' => [ + 'href' => 'act=toggle&field=featured', + 'icon' => 'featured.svg', + ], + 'show' => [ + 'href' => 'act=show', + 'icon' => 'show.svg' + ] + ] + ], + + // Palettes + 'palettes' => [ + 'default' => '{title_legend},author,title,alias,email,location;{date_legend},date,time;{recommendation_legend},text,imageUrl,rating,customField;{teaser_legend:hide},teaser;{expert_legend:hide},cssClass,featured;{publish_legend},published,start,stop' + ], + + // Fields + 'fields' => [ + 'id' => [ + 'sql' => "int(10) unsigned NOT NULL auto_increment" + ], + 'pid' => [ + 'foreignKey' => 'tl_recommendation_archive.title', + 'sql' => "int(10) unsigned NOT NULL default '0'", + 'relation' => ['type'=>'belongsTo', 'load'=>'lazy'] + ], + 'tstamp' => [ + 'sql' => "int(10) unsigned NOT NULL default '0'" + ], + 'title' => [ + 'exclude' => true, + 'search' => true, + 'sorting' => true, + 'flag' => DataContainer::SORT_INITIAL_LETTER_ASC, + 'inputType' => 'text', + 'eval' => ['maxlength'=>255, 'tl_class'=>'w50 clr'], + 'sql' => "varchar(255) NOT NULL default ''" + ], + 'alias' => [ + 'exclude' => true, + 'search' => true, + 'inputType' => 'text', + 'eval' => ['rgxp'=>'alias', 'doNotCopy'=>true, 'unique'=>true, 'maxlength'=>128, 'tl_class'=>'w50'], + 'save_callback' => [ + [RecommendationListener::class, 'generateRecommendationAlias'] + ], + 'sql' => "varchar(255) BINARY NOT NULL default ''" + ], + 'author' => [ + 'exclude' => true, + 'search' => true, + 'sorting' => true, + 'flag' => DataContainer::SORT_INITIAL_LETTER_ASC, + 'inputType' => 'text', + 'eval' => ['doNotCopy'=>true, 'mandatory'=>true, 'maxlength'=>128, 'tl_class'=>'w50'], + 'sql' => "varchar(128) NOT NULL default ''" + ], + 'email' => [ + 'exclude' => true, + 'search' => true, + 'inputType' => 'text', + 'eval' => ['doNotCopy'=>true, 'maxlength'=>255, 'rgxp'=>'email', 'decodeEntities'=>true, 'tl_class'=>'w50'], + 'sql' => "varchar(255) NOT NULL default ''" + ], + 'location' => [ + 'exclude' => true, + 'search' => true, + 'sorting' => true, + 'flag' => DataContainer::SORT_INITIAL_LETTER_ASC, + 'inputType' => 'text', + 'eval' => ['doNotCopy'=>true, 'maxlength'=>128, 'tl_class'=>'w50'], + 'sql' => "varchar(128) NOT NULL default ''" + ], + 'date' => [ + 'default' => time(), + 'exclude' => true, + 'filter' => true, + 'sorting' => true, + 'flag' => DataContainer::SORT_MONTH_DESC, + 'inputType' => 'text', + 'eval' => ['rgxp'=>'date', 'mandatory'=>true, 'doNotCopy'=>true, 'datepicker'=>true, 'tl_class'=>'w50 wizard'], + 'load_callback' => [ + [RecommendationListener::class, 'loadDate'] + ], + 'sql' => "int(10) unsigned NOT NULL default 0" + ], + 'time' => [ + 'default' => time(), + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['rgxp'=>'time', 'mandatory'=>true, 'doNotCopy'=>true, 'tl_class'=>'w50'], + 'load_callback' => [ + [RecommendationListener::class, 'loadTime'] + ], + 'sql' => "int(10) unsigned NOT NULL default 0" + ], + 'teaser' => [ + 'exclude' => true, + 'search' => true, + 'inputType' => 'textarea', + 'eval' => ['rte'=>'tinyMCE', 'tl_class'=>'clr'], + 'sql' => "mediumtext NULL" + ], + 'text' => [ + 'exclude' => true, + 'search' => true, + 'inputType' => 'textarea', + 'eval' => ['mandatory'=>true, 'rte'=>'tinyMCE', 'tl_class'=>'clr'], + 'sql' => "mediumtext NULL" + ], + 'imageUrl' => [ + 'exclude' => true, + 'search' => true, + 'inputType' => 'text', + 'eval' => ['rgxp'=>'url', 'decodeEntities'=>true, 'maxlength'=>255, 'dcaPicker'=>true, 'tl_class'=>'w50 wizard'], + 'sql' => "varchar(255) NOT NULL default ''" + ], + 'rating' => [ + 'default' => 5, + 'exclude' => true, + 'search' => true, + 'filter' => true, + 'sorting' => true, + 'inputType' => 'select', + 'options' => [1,2,3,4,5], + 'eval' => ['mandatory'=>true, 'tl_class'=>'w50'], + 'sql' => "char(1) NOT NULL default ''" + ], + 'customField' => [ + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['doNotCopy'=>true, 'maxlength'=>255, 'tl_class'=>'w100 clr'], + 'sql' => "varchar(255) NOT NULL default ''" + ], + 'cssClass' => [ + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['tl_class'=>'w50'], + 'sql' => "varchar(255) NOT NULL default ''" + ], + 'featured' => [ + 'exclude' => true, + 'toggle' => true, + 'filter' => true, + 'inputType' => 'checkbox', + 'eval' => ['tl_class'=>'w50 m12'], + 'sql' => "char(1) NOT NULL default ''" + ], + 'verified' => [ + 'filter' => true, + 'eval' => ['isBoolean'=>true], + 'sql' => "char(1) NOT NULL default '1'" + ], + 'published' => [ + 'exclude' => true, + 'toggle' => true, + 'filter' => true, + 'flag' => DataContainer::SORT_INITIAL_LETTER_ASC, + 'inputType' => 'checkbox', + 'eval' => ['doNotCopy'=>true], + 'sql' => "char(1) NOT NULL default ''" + ], + 'start' => [ + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['rgxp'=>'datim', 'datepicker'=>true, 'tl_class'=>'w50 wizard'], + 'sql' => "varchar(10) NOT NULL default ''" + ], + 'stop' => [ + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['rgxp'=>'datim', 'datepicker'=>true, 'tl_class'=>'w50 wizard'], + 'sql' => "varchar(10) NOT NULL default ''" + ] + ] +]; diff --git a/contao/dca/tl_recommendation_archive.php b/contao/dca/tl_recommendation_archive.php new file mode 100644 index 0000000..fe5a269 --- /dev/null +++ b/contao/dca/tl_recommendation_archive.php @@ -0,0 +1,139 @@ + [ + 'dataContainer' => DC_Table::class, + 'ctable' => ['tl_recommendation'], + 'switchToEdit' => true, + 'enableVersioning' => true, + 'oncreate_callback' => [ + [RecommendationArchiveListener::class, 'adjustPermissions'] + ], + 'oncopy_callback' => [ + [RecommendationArchiveListener::class, 'adjustPermissions'] + ], + 'oninvalidate_cache_tags_callback' => [ + [RecommendationArchiveListener::class, 'addSitemapCacheInvalidationTag'], + ], + 'sql' => [ + 'keys' => [ + 'id' => 'primary' + ] + ] + ], + + // List + 'list' => [ + 'sorting' => [ + 'mode' => DataContainer::MODE_SORTED, + 'fields' => ['title'], + 'flag' => DataContainer::SORT_INITIAL_LETTER_ASC, + 'panelLayout' => 'filter;search,limit' + ], + 'label' => [ + 'fields' => ['title'], + 'format' => '%s' + ], + 'global_operations' => [ + 'settings' => [ + 'href' => 'do=recommendation_settings', + 'class' => '', + 'icon' => 'edit.svg', + 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"' + ], + 'all' => [ + 'href' => 'act=select', + 'class' => 'header_edit_all', + 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"' + ] + ], + 'operations' => [ + 'editheader' => [ + 'href' => 'act=edit', + 'icon' => 'header.svg', + 'button_callback' => [RecommendationArchiveListener::class, 'editHeader'] + ], + 'edit' => [ + 'href' => 'table=tl_recommendation', + 'icon' => 'edit.svg' + ], + 'copy' => [ + 'href' => 'act=copy', + 'icon' => 'copy.svg', + 'button_callback' => [RecommendationArchiveListener::class, 'copyArchive'] + ], + 'delete' => [ + 'href' => 'act=delete', + 'icon' => 'delete.svg', + 'attributes' => 'onclick="if(!confirm(\'' . ($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? null) . '\'))return false;Backend.getScrollOffset()"', + 'button_callback' => [RecommendationArchiveListener::class, 'deleteArchive'] + ], + 'show' => [ + 'href' => 'act=show', + 'icon' => 'show.svg' + ] + ] + ], + + // Palettes + 'palettes' => [ + '__selector__' => ['protected'], + 'default' => '{title_legend},title,jumpTo;{protected_legend:hide},protected' + ], + + // Subpalettes + 'subpalettes' => [ + 'protected' => 'groups' + ], + + // Fields + 'fields' => [ + 'id' => [ + 'sql' => "int(10) unsigned NOT NULL auto_increment" + ], + 'tstamp' => [ + 'sql' => "int(10) unsigned NOT NULL default '0'" + ], + 'title' => [ + 'exclude' => true, + 'search' => true, + 'inputType' => 'text', + 'eval' => ['mandatory'=>true, 'maxlength'=>255, 'tl_class'=>'w50'], + 'sql' => "varchar(255) NOT NULL default ''" + ], + 'jumpTo' => [ + 'exclude' => true, + 'inputType' => 'pageTree', + 'foreignKey' => 'tl_page.title', + 'eval' => ['fieldType'=>'radio', 'tl_class'=>'clr'], + 'sql' => "int(10) unsigned NOT NULL default '0'", + 'relation' => ['type'=>'hasOne', 'load'=>'eager'] + ], + 'protected' => [ + 'exclude' => true, + 'filter' => true, + 'inputType' => 'checkbox', + 'eval' => ['submitOnChange'=>true], + 'sql' => "char(1) NOT NULL default ''" + ], + 'groups' => [ + 'exclude' => true, + 'inputType' => 'checkbox', + 'foreignKey' => 'tl_member_group.name', + 'eval' => ['mandatory'=>true, 'multiple'=>true], + 'sql' => "blob NULL", + 'relation' => ['type'=>'hasMany', 'load'=>'lazy'] + ] + ] +]; diff --git a/contao/dca/tl_recommendation_settings.php b/contao/dca/tl_recommendation_settings.php new file mode 100644 index 0000000..ebb78ff --- /dev/null +++ b/contao/dca/tl_recommendation_settings.php @@ -0,0 +1,49 @@ + [ + 'dataContainer' => DC_File::class, + 'closed' => true + ], + + // Palettes + 'palettes' => [ + 'default' => '{recommendation_legend},recommendationDefaultImage,recommendationActiveColor;' + ], + + // Fields + 'fields' => [ + 'recommendationDefaultImage' => [ + 'inputType' => 'fileTree', + 'eval' => ['fieldType'=>'radio', 'filesOnly'=>true, 'isGallery'=>true, 'extensions'=> Config::get('validImageTypes'), 'tl_class'=>'clr'] + ], + 'recommendationActiveColor' => [ + 'inputType' => 'text', + 'eval' => ['maxlength'=>6, 'multiple'=>true, 'size'=>1, 'colorpicker'=>true, 'isHexColor'=>true, 'decodeEntities'=>true, 'tl_class'=>'w50 wizard'], + 'save_callback' => [ + // See contao/issues (#6105) + static function ($value) + { + if (!\is_array($value)) + { + return StringUtil::restoreBasicEntities($value); + } + + return serialize(array_map('\Contao\StringUtil::restoreBasicEntities', $value)); + } + ] + ] + ] +]; diff --git a/contao/dca/tl_user.php b/contao/dca/tl_user.php new file mode 100644 index 0000000..a1cde50 --- /dev/null +++ b/contao/dca/tl_user.php @@ -0,0 +1,35 @@ +addLegend('recommendation_legend', 'amg_legend', PaletteManipulator::POSITION_BEFORE) + ->addField(['recommendations', 'recommendationp'], 'recommendation_legend', PaletteManipulator::POSITION_APPEND) + ->applyToPalette('extend', 'tl_user') + ->applyToPalette('custom', 'tl_user') +; + +// Add fields to tl_user_group +$GLOBALS['TL_DCA']['tl_user']['fields']['recommendations'] = [ + 'exclude' => true, + 'inputType' => 'checkbox', + 'foreignKey' => 'tl_recommendation_archive.title', + 'eval' => ['multiple'=>true], + 'sql' => "blob NULL" +]; + +$GLOBALS['TL_DCA']['tl_user']['fields']['recommendationp'] = [ + 'exclude' => true, + 'inputType' => 'checkbox', + 'options' => ['create', 'delete'], + 'reference' => &$GLOBALS['TL_LANG']['MSC'], + 'eval' => ['multiple'=>true], + 'sql' => "blob NULL" +]; diff --git a/src/Resources/contao/dca/tl_user_group.php b/contao/dca/tl_user_group.php similarity index 50% rename from src/Resources/contao/dca/tl_user_group.php rename to contao/dca/tl_user_group.php index 52071ad..0af419e 100644 --- a/src/Resources/contao/dca/tl_user_group.php +++ b/contao/dca/tl_user_group.php @@ -6,31 +6,29 @@ * (c) https://www.oveleon.de/ */ +use Contao\CoreBundle\DataContainer\PaletteManipulator; + // Extend the default palette -Contao\CoreBundle\DataContainer\PaletteManipulator::create() - ->addLegend('recommendation_legend', 'amg_legend', Contao\CoreBundle\DataContainer\PaletteManipulator::POSITION_BEFORE) - ->addField(array('recommendations', 'recommendationp'), 'recommendation_legend', Contao\CoreBundle\DataContainer\PaletteManipulator::POSITION_APPEND) +PaletteManipulator::create() + ->addLegend('recommendation_legend', 'amg_legend', PaletteManipulator::POSITION_BEFORE) + ->addField(['recommendations', 'recommendationp'], 'recommendation_legend', PaletteManipulator::POSITION_APPEND) ->applyToPalette('default', 'tl_user_group') ; // Add fields to tl_user_group -$GLOBALS['TL_DCA']['tl_user_group']['fields']['recommendations'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_user']['recommendations'], +$GLOBALS['TL_DCA']['tl_user_group']['fields']['recommendations'] = [ 'exclude' => true, 'inputType' => 'checkbox', 'foreignKey' => 'tl_recommendation_archive.title', - 'eval' => array('multiple'=>true), + 'eval' => ['multiple'=>true], 'sql' => "blob NULL" -); +]; -$GLOBALS['TL_DCA']['tl_user_group']['fields']['recommendationp'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_user']['recommendationp'], +$GLOBALS['TL_DCA']['tl_user_group']['fields']['recommendationp'] = [ 'exclude' => true, 'inputType' => 'checkbox', - 'options' => array('create', 'delete'), + 'options' => ['create', 'delete'], 'reference' => &$GLOBALS['TL_LANG']['MSC'], - 'eval' => array('multiple'=>true), + 'eval' => ['multiple'=>true], 'sql' => "blob NULL" -); +]; diff --git a/src/Resources/contao/languages/de/default.xlf b/contao/languages/de/default.xlf similarity index 70% rename from src/Resources/contao/languages/de/default.xlf rename to contao/languages/de/default.xlf index 1d7701f..97241b7 100644 --- a/src/Resources/contao/languages/de/default.xlf +++ b/contao/languages/de/default.xlf @@ -1,5 +1,5 @@ - + Currently there are no recommendations. diff --git a/src/Resources/contao/languages/de/modules.xlf b/contao/languages/de/modules.xlf similarity index 94% rename from src/Resources/contao/languages/de/modules.xlf rename to contao/languages/de/modules.xlf index a187565..9b6d02e 100644 --- a/src/Resources/contao/languages/de/modules.xlf +++ b/contao/languages/de/modules.xlf @@ -1,5 +1,5 @@ - + Recommendations diff --git a/src/Resources/contao/languages/de/tl_module.xlf b/contao/languages/de/tl_module.xlf similarity index 98% rename from src/Resources/contao/languages/de/tl_module.xlf rename to contao/languages/de/tl_module.xlf index 123da80..8fed375 100644 --- a/src/Resources/contao/languages/de/tl_module.xlf +++ b/contao/languages/de/tl_module.xlf @@ -1,5 +1,5 @@ - + Data security diff --git a/src/Resources/contao/languages/de/tl_recommendation.xlf b/contao/languages/de/tl_recommendation.xlf similarity index 99% rename from src/Resources/contao/languages/de/tl_recommendation.xlf rename to contao/languages/de/tl_recommendation.xlf index db80856..f5b4311 100644 --- a/src/Resources/contao/languages/de/tl_recommendation.xlf +++ b/contao/languages/de/tl_recommendation.xlf @@ -1,5 +1,5 @@ - + Not verified diff --git a/src/Resources/contao/languages/de/tl_recommendation_archive.xlf b/contao/languages/de/tl_recommendation_archive.xlf similarity index 96% rename from src/Resources/contao/languages/de/tl_recommendation_archive.xlf rename to contao/languages/de/tl_recommendation_archive.xlf index 6647544..dade3fa 100644 --- a/src/Resources/contao/languages/de/tl_recommendation_archive.xlf +++ b/contao/languages/de/tl_recommendation_archive.xlf @@ -1,12 +1,12 @@ - + Title Titel - Please enter a news archive title. + Please enter a recommendation archive title. Bitte geben Sie den Archiv-Titel ein. diff --git a/src/Resources/contao/languages/de/tl_recommendation_list.xlf b/contao/languages/de/tl_recommendation_list.xlf similarity index 94% rename from src/Resources/contao/languages/de/tl_recommendation_list.xlf rename to contao/languages/de/tl_recommendation_list.xlf index 8cb36e9..4dcc15c 100644 --- a/src/Resources/contao/languages/de/tl_recommendation_list.xlf +++ b/contao/languages/de/tl_recommendation_list.xlf @@ -1,5 +1,5 @@ - + Show all recommendations diff --git a/src/Resources/contao/languages/de/tl_recommendation_notification.xlf b/contao/languages/de/tl_recommendation_notification.xlf similarity index 95% rename from src/Resources/contao/languages/de/tl_recommendation_notification.xlf rename to contao/languages/de/tl_recommendation_notification.xlf index a1ba652..faafae8 100644 --- a/src/Resources/contao/languages/de/tl_recommendation_notification.xlf +++ b/contao/languages/de/tl_recommendation_notification.xlf @@ -1,5 +1,5 @@ - + Your recommendation has been added and is now pending for approval. diff --git a/src/Resources/contao/languages/de/tl_recommendation_settings.xlf b/contao/languages/de/tl_recommendation_settings.xlf similarity index 89% rename from src/Resources/contao/languages/de/tl_recommendation_settings.xlf rename to contao/languages/de/tl_recommendation_settings.xlf index 37347ab..cd147f0 100644 --- a/src/Resources/contao/languages/de/tl_recommendation_settings.xlf +++ b/contao/languages/de/tl_recommendation_settings.xlf @@ -1,5 +1,5 @@ - + Default image diff --git a/src/Resources/contao/languages/de/tl_user.xlf b/contao/languages/de/tl_user.xlf similarity index 89% rename from src/Resources/contao/languages/de/tl_user.xlf rename to contao/languages/de/tl_user.xlf index e0bbd9b..305763a 100644 --- a/src/Resources/contao/languages/de/tl_user.xlf +++ b/contao/languages/de/tl_user.xlf @@ -1,5 +1,5 @@ - + Allowed archives diff --git a/contao/languages/de/tl_user_group.xlf b/contao/languages/de/tl_user_group.xlf new file mode 100644 index 0000000..faf9fe6 --- /dev/null +++ b/contao/languages/de/tl_user_group.xlf @@ -0,0 +1,26 @@ + + + + + Recommendation permissions + Bewertungsrechte + + + Allowed archives + Erlaubte Archive + + + Here you can grant access to one or more recommendation archives. + Hier können Sie den Zugriff auf ein oder mehrere Bewertungs-Archive erlauben. + + + Recommendation permissions + Archivrechte + + + Here you can define the recommendation permissions. + Hier können Sie die Archivrechte festlegen. + + + + diff --git a/src/Resources/contao/languages/en/default.xlf b/contao/languages/en/default.xlf similarity index 68% rename from src/Resources/contao/languages/en/default.xlf rename to contao/languages/en/default.xlf index 48abcdb..7244fee 100644 --- a/src/Resources/contao/languages/en/default.xlf +++ b/contao/languages/en/default.xlf @@ -1,5 +1,5 @@ - + Currently there are no recommendations. diff --git a/src/Resources/contao/languages/en/modules.xlf b/contao/languages/en/modules.xlf similarity index 93% rename from src/Resources/contao/languages/en/modules.xlf rename to contao/languages/en/modules.xlf index 6002ff0..cb85905 100644 --- a/src/Resources/contao/languages/en/modules.xlf +++ b/contao/languages/en/modules.xlf @@ -1,5 +1,5 @@ - + Recommendations diff --git a/src/Resources/contao/languages/en/tl_module.xlf b/contao/languages/en/tl_module.xlf similarity index 98% rename from src/Resources/contao/languages/en/tl_module.xlf rename to contao/languages/en/tl_module.xlf index a7e0744..45df350 100644 --- a/src/Resources/contao/languages/en/tl_module.xlf +++ b/contao/languages/en/tl_module.xlf @@ -1,5 +1,5 @@ - + Data security diff --git a/src/Resources/contao/languages/en/tl_recommendation.xlf b/contao/languages/en/tl_recommendation.xlf similarity index 98% rename from src/Resources/contao/languages/en/tl_recommendation.xlf rename to contao/languages/en/tl_recommendation.xlf index 665104b..1073f30 100644 --- a/src/Resources/contao/languages/en/tl_recommendation.xlf +++ b/contao/languages/en/tl_recommendation.xlf @@ -1,5 +1,5 @@ - + Not verified diff --git a/src/Resources/contao/languages/en/tl_recommendation_archive.xlf b/contao/languages/en/tl_recommendation_archive.xlf similarity index 95% rename from src/Resources/contao/languages/en/tl_recommendation_archive.xlf rename to contao/languages/en/tl_recommendation_archive.xlf index 65cea85..966798d 100644 --- a/src/Resources/contao/languages/en/tl_recommendation_archive.xlf +++ b/contao/languages/en/tl_recommendation_archive.xlf @@ -1,11 +1,11 @@ - + Title - Please enter a news archive title. + Please enter a recommendation archive title. Redirect page diff --git a/src/Resources/contao/languages/en/tl_recommendation_list.xlf b/contao/languages/en/tl_recommendation_list.xlf similarity index 93% rename from src/Resources/contao/languages/en/tl_recommendation_list.xlf rename to contao/languages/en/tl_recommendation_list.xlf index c602cf1..7014289 100644 --- a/src/Resources/contao/languages/en/tl_recommendation_list.xlf +++ b/contao/languages/en/tl_recommendation_list.xlf @@ -1,5 +1,5 @@ - + Show all recommendations diff --git a/src/Resources/contao/languages/en/tl_recommendation_notification.xlf b/contao/languages/en/tl_recommendation_notification.xlf similarity index 93% rename from src/Resources/contao/languages/en/tl_recommendation_notification.xlf rename to contao/languages/en/tl_recommendation_notification.xlf index 32535f5..dda2248 100644 --- a/src/Resources/contao/languages/en/tl_recommendation_notification.xlf +++ b/contao/languages/en/tl_recommendation_notification.xlf @@ -1,5 +1,5 @@ - + Your recommendation has been added and is now pending for approval. diff --git a/src/Resources/contao/languages/en/tl_recommendation_settings.xlf b/contao/languages/en/tl_recommendation_settings.xlf similarity index 88% rename from src/Resources/contao/languages/en/tl_recommendation_settings.xlf rename to contao/languages/en/tl_recommendation_settings.xlf index e4e3396..15c2e9a 100644 --- a/src/Resources/contao/languages/en/tl_recommendation_settings.xlf +++ b/contao/languages/en/tl_recommendation_settings.xlf @@ -1,5 +1,5 @@ - + Default image diff --git a/src/Resources/contao/languages/en/tl_user.xlf b/contao/languages/en/tl_user.xlf similarity index 88% rename from src/Resources/contao/languages/en/tl_user.xlf rename to contao/languages/en/tl_user.xlf index 26da2b3..59e1c15 100644 --- a/src/Resources/contao/languages/en/tl_user.xlf +++ b/contao/languages/en/tl_user.xlf @@ -1,5 +1,5 @@ - + Allowed archives diff --git a/contao/languages/en/tl_user_group.xlf b/contao/languages/en/tl_user_group.xlf new file mode 100644 index 0000000..aa0555d --- /dev/null +++ b/contao/languages/en/tl_user_group.xlf @@ -0,0 +1,21 @@ + + + + + Recommendation permissions + + + Allowed archives + + + Here you can grant access to one or more recommendation archives. + + + Recommendation permissions + + + Here you can define the recommendation permissions. + + + + diff --git a/src/Resources/contao/modules/ModuleRecommendation.php b/contao/modules/ModuleRecommendation.php similarity index 66% rename from src/Resources/contao/modules/ModuleRecommendation.php rename to contao/modules/ModuleRecommendation.php index d55781a..e7a8b55 100644 --- a/src/Resources/contao/modules/ModuleRecommendation.php +++ b/contao/modules/ModuleRecommendation.php @@ -9,64 +9,50 @@ namespace Oveleon\ContaoRecommendationBundle; use Contao\Config; -use Contao\Controller; +use Contao\CoreBundle\Security\ContaoCorePermissions; use Contao\Date; use Contao\FilesModel; use Contao\FrontendTemplate; -use Contao\FrontendUser; use Contao\Model\Collection; use Contao\Module; use Contao\PageModel; use Contao\StringUtil; use Contao\System; +use Exception; +use Oveleon\ContaoRecommendationBundle\Model\RecommendationModel; +use Oveleon\ContaoRecommendationBundle\Model\RecommendationArchiveModel; +use Symfony\Component\Filesystem\Path; /** * Parent class for recommendation modules. * * @property string $recommendation_template * @property mixed $recommendation_metaFields - * - * @author Fabian Ekert - * @author Sebastian Zoglowek */ abstract class ModuleRecommendation extends Module { - /** * Sort out protected archives - * - * @param array $arrArchives - * - * @return array */ - protected function sortOutProtected($arrArchives) + protected function sortOutProtected(array $arrArchives): array { if (empty($arrArchives) || !\is_array($arrArchives)) { return $arrArchives; } - $this->import(FrontendUser::class, 'User'); $objArchive = RecommendationArchiveModel::findMultipleByIds($arrArchives); - $arrArchives = array(); + $arrArchives = []; if ($objArchive !== null) { + $security = System::getContainer()->get('security.helper'); + while ($objArchive->next()) { - if ($objArchive->protected) + if ($objArchive->protected && !$security->isGranted(ContaoCorePermissions::MEMBER_IN_GROUPS, StringUtil::deserialize($objArchive->groups, true))) { - if (!FE_USER_LOGGED_IN) - { - continue; - } - - $groups = StringUtil::deserialize($objArchive->groups); - - if (empty($groups) || !\is_array($groups) || !\count(array_intersect($groups, $this->User->groups))) - { - continue; - } + continue; } $arrArchives[] = $objArchive->id; @@ -78,21 +64,13 @@ protected function sortOutProtected($arrArchives) /** * Parse an item and return it as string - * - * @param RecommendationModel $objRecommendation - * @param RecommendationArchiveModel $objRecommendationArchive - * @param string $strClass - * @param integer $intCount - * - * @return string */ - protected function parseRecommendation($objRecommendation, $objRecommendationArchive, $strClass='', $intCount=0) + protected function parseRecommendation(RecommendationModel $objRecommendation, RecommendationArchiveModel $objRecommendationArchive, string $strClass='', int $intCount=0): string { - /** @var FrontendTemplate|object $objTemplate */ $objTemplate = new FrontendTemplate($this->recommendation_template ?: 'recommendation_default'); $objTemplate->setData($objRecommendation->row()); - if ($objRecommendation->cssClass != '') + if ($objRecommendation->cssClass) { $strClass = ' ' . $objRecommendation->cssClass . $strClass; } @@ -142,29 +120,34 @@ protected function parseRecommendation($objRecommendation, $objRecommendationArc // Parsing image meta field to template for backwards compatibility // Works for recommendation_default.html5 $objTemplate->addRecommendationImage = array_key_exists('image', $arrMeta); + $container = System::getContainer(); + // Add an image if ($objRecommendation->imageUrl != '') { - $objRecommendation->imageUrl = Controller::replaceInsertTags($objRecommendation->imageUrl); + $objRecommendation->imageUrl = $container->get('contao.insert_tag.parser')->replace($objRecommendation->imageUrl); + + // Insert tag parser on contao ^5 returns a leading slash whilst contao 4.13 does not + if (Path::isAbsolute($objRecommendation->imageUrl)) + { + $objRecommendation->imageUrl = substr($objRecommendation->imageUrl,1); + } if ($this->isExternal($objRecommendation->imageUrl)) { $objTemplate->addExternalImage = true; - $objTemplate->imageUrl = $objRecommendation->imageUrl; } else { $objModel = FilesModel::findByPath($objRecommendation->imageUrl); - - $this->addInternalImage($objModel, $objRecommendation, $objTemplate); + $this->addInternalImage($objModel, $objTemplate); } } elseif (Config::get('recommendationDefaultImage')) { $objModel = FilesModel::findByUuid(Config::get('recommendationDefaultImage')); - - $this->addInternalImage($objModel, $objRecommendation, $objTemplate); + $this->addInternalImage($objModel, $objTemplate); } // HOOK: add custom logic @@ -178,10 +161,10 @@ protected function parseRecommendation($objRecommendation, $objRecommendationArc } // Tag recommendations - if (System::getContainer()->has('fos_http_cache.http.symfony_response_tagger')) + if ($container->has('fos_http_cache.http.symfony_response_tagger')) { - $responseTagger = System::getContainer()->get('fos_http_cache.http.symfony_response_tagger'); - $responseTagger->addTags(array('contao.db.tl_recommendation.' . $objRecommendation->id)); + $responseTagger = $container->get('fos_http_cache.http.symfony_response_tagger'); + $responseTagger->addTags(['contao.db.tl_recommendation.' . $objRecommendation->id]); } return $objTemplate->parse(); @@ -189,22 +172,18 @@ protected function parseRecommendation($objRecommendation, $objRecommendationArc /** * Parse one or more items and return them as array - * - * @param Collection $objRecommendations - * - * @return array */ - protected function parseRecommendations($objRecommendations) + protected function parseRecommendations(Collection $objRecommendations): array { $limit = $objRecommendations->count(); if ($limit < 1) { - return array(); + return []; } $count = 0; - $arrRecommendations = array(); + $arrRecommendations = []; while ($objRecommendations->next()) { @@ -222,24 +201,20 @@ protected function parseRecommendations($objRecommendations) /** * Return the meta fields of a recommendation as array - * - * @param RecommendationModel $objRecommendation - * - * @return array */ - protected function getMetaFields($objRecommendation) + protected function getMetaFields(RecommendationModel $objRecommendation): array { $meta = StringUtil::deserialize($this->recommendation_metaFields); if (!\is_array($meta)) { - return array(); + return []; } /** @var PageModel $objPage */ global $objPage; - $return = array(); + $return = []; foreach ($meta as $field) { @@ -263,15 +238,8 @@ protected function getMetaFields($objRecommendation) /** * Generate a link and return it as string - * - * @param string $strLink - * @param RecommendationModel $objRecommendation - * @param string $strTitle - * @param boolean $blnIsReadMore - * - * @return string */ - protected function generateLink($strLink, $objRecommendation, $strTitle, $blnIsReadMore=false) + protected function generateLink(string $strLink, RecommendationModel $objRecommendation, string $strTitle, bool $blnIsReadMore=false): string { return sprintf('', $this->generateRecommendationUrl($objRecommendation), @@ -282,28 +250,20 @@ protected function generateLink($strLink, $objRecommendation, $strTitle, $blnIsR /** * Generate a URL and return it as string - * - * @param RecommendationModel $objRecommendation - * - * @return string */ - protected function generateRecommendationUrl($objRecommendation) + protected function generateRecommendationUrl(RecommendationModel $objRecommendation): string { $objPage = PageModel::findByPk($objRecommendation->getRelated('pid')->jumpTo); - return ampersand($objPage->getFrontendUrl((Config::get('useAutoItem') ? '/' : '/items/') . ($objRecommendation->alias ?: $objRecommendation->id))); + return StringUtil::ampersand($objPage->getFrontendUrl((Config::get('useAutoItem') ? '/' : '/items/') . ($objRecommendation->alias ?: $objRecommendation->id))); } /** * Check whether path is external - * - * @param string $strPath The file path - * - * @return boolean */ - protected function isExternal($strPath) + protected function isExternal(string $strPath): bool { - if (substr($strPath, 0, 7) == 'http://' || substr($strPath, 0, 8) == 'https://') + if (str_starts_with($strPath, 'http://') || str_starts_with($strPath, 'https://')) { return true; } @@ -313,48 +273,47 @@ protected function isExternal($strPath) /** * Add an internal image to template - * - * @param FilesModel $objModel The files model - * @param RecommendationModel $objRecommendation The recommendation model - * @param FrontendTemplate $objTemplate The frontend template */ - protected function addInternalImage($objModel, $objRecommendation, &$objTemplate) + protected function addInternalImage($objModel, &$objTemplate): void { - if ($objModel !== null && is_file(TL_ROOT . '/' . $objModel->path)) + if (null !== $objModel) { + $imgSize = $this->imgSize ?: null; $objTemplate->addInternalImage = true; - // Do not override the field now that we have a model registry (see #6303) - $arrRecommendation = $objRecommendation->row(); - // Override the default image size - if ($this->imgSize != '') + if ($this->imgSize) { $size = StringUtil::deserialize($this->imgSize); if ($size[0] > 0 || $size[1] > 0 || is_numeric($size[2]) || ($size[2][0] ?? null) === '_') { - $arrRecommendation['size'] = $this->imgSize; + $imgSize = $this->imgSize; } } - $arrRecommendation['singleSRC'] = $objModel->path; - $this->addImageToTemplate($objTemplate, $arrRecommendation, null, null, $objModel); + $figureBuilder = System::getContainer() + ->get('contao.image.studio') + ->createFigureBuilder() + ->from($objModel->path) + ->setSize($imgSize); + + if (null !== ($figure = $figureBuilder->buildIfResourceExists())) + { + $figure->applyLegacyTemplateData($objTemplate); + } } } /** - * Parses a timestamp into a human readable string - * - * @param string $strDateTime - * - * @return string + * Parses a timestamp into a human-readable string + * @throws Exception */ - protected function getElapsedTime($strDateTime) + protected function getElapsedTime(string $strDateTime): string { $objElapsedTime = (new \DateTime($strDateTime))->diff(new \DateTime()); - if(($years = $objElapsedTime->y) > 0) + if (($years = $objElapsedTime->y) > 0) { return $this->translateElapsedTime($years, 'year'); } @@ -382,13 +341,8 @@ protected function getElapsedTime($strDateTime) /** * Translates elapsed time - * - * @param integer $value - * @param string $strUnit - * - * @return string */ - protected function translateElapsedTime($value, $strUnit = 'justNow') + protected function translateElapsedTime(int $value, string $strUnit = 'justNow'): string { return sprintf($GLOBALS['TL_LANG']['tl_recommendation'][$strUnit][!($value>1)], $value); } diff --git a/src/Resources/contao/modules/ModuleRecommendationForm.php b/contao/modules/ModuleRecommendationForm.php similarity index 74% rename from src/Resources/contao/modules/ModuleRecommendationForm.php rename to contao/modules/ModuleRecommendationForm.php index 4957d83..a5d76a3 100644 --- a/src/Resources/contao/modules/ModuleRecommendationForm.php +++ b/contao/modules/ModuleRecommendationForm.php @@ -10,7 +10,6 @@ use Contao\BackendTemplate; use Contao\CoreBundle\Exception\InternalServerErrorException; -use Contao\CoreBundle\Monolog\ContaoContext; use Contao\CoreBundle\OptIn\OptIn; use Contao\Email; use Contao\Environment; @@ -21,7 +20,8 @@ use Contao\StringUtil; use Contao\System; use Contao\Widget; -use Psr\Log\LogLevel; +use Oveleon\ContaoRecommendationBundle\Model\RecommendationArchiveModel; +use Oveleon\ContaoRecommendationBundle\Model\RecommendationModel; /** * Front end module "recommendation form". @@ -39,8 +39,6 @@ * @property integer $jumpTo * @property boolean $recommendation_activate * @property string $recommendation_activateText - * - * @author Sebastian Zoglowek */ class ModuleRecommendationForm extends ModuleRecommendation { @@ -55,18 +53,18 @@ class ModuleRecommendationForm extends ModuleRecommendation * * @return string */ - public function generate() + public function generate(): string { $request = System::getContainer()->get('request_stack')->getCurrentRequest(); if ($request && System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request)) { $objTemplate = new BackendTemplate('be_wildcard'); - $objTemplate->wildcard = '### ' . mb_strtoupper($GLOBALS['TL_LANG']['FMD']['recommendationform'][0], 'UTF-8') . ' ###'; + $objTemplate->wildcard = '### ' . $GLOBALS['TL_LANG']['FMD']['recommendationform'][0] . ' ###'; $objTemplate->title = $this->headline; $objTemplate->id = $this->id; $objTemplate->link = $this->name; - $objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id; + $objTemplate->href = StringUtil::specialcharsUrl(System::getContainer()->get('router')->generate('contao_backend', array('do'=>'themes', 'table'=>'tl_module', 'act'=>'edit', 'id'=>$this->id))); return $objTemplate->parse(); } @@ -84,7 +82,7 @@ public function generate() /** * Generate the module */ - protected function compile() + protected function compile(): void { System::loadLanguageFile('tl_recommendation'); System::loadLanguageFile('tl_recommendation_notification'); @@ -98,70 +96,61 @@ protected function compile() } // Form fields - $arrFields = array - ( - 'author' => array - ( + $arrFields = [ + 'author' => [ 'name' => 'author', 'label' => $GLOBALS['TL_LANG']['tl_recommendation']['author'], 'inputType' => 'text', - 'eval' => array('mandatory'=>true, 'maxlength'=>128) - ), - 'rating' => array - ( + 'eval' => ['mandatory'=>true, 'maxlength'=>128] + ], + 'rating' => [ 'name' => 'rating', 'label' => $GLOBALS['TL_LANG']['tl_recommendation']['rating'], 'inputType' => 'select', - 'options' => array(5,4,3,2,1), - 'eval' => array('mandatory'=>true) - ), - 'title' => array - ( + 'options' => [5,4,3,2,1], + 'eval' => ['mandatory'=>true] + ], + 'title' => [ 'name' => 'title', 'label' => $GLOBALS['TL_LANG']['tl_recommendation']['title'], 'inputType' => 'text', - 'eval' => array('optional'=>true, 'maxlength'=>255), - ), - 'customField' => array - ( + 'eval' => ['optional'=>true, 'maxlength'=>255], + ], + 'customField' => [ 'name' => 'customField', 'label' => $this->recommendation_customFieldLabel ?: $GLOBALS['TL_LANG']['tl_recommendation']['customFieldLabel'], 'inputType' => 'text', - 'eval' => array('optional'=>true, 'maxlength'=>255), - ), - 'location' => array - ( + 'eval' => ['optional'=>true, 'maxlength'=>255], + ], + 'location' => [ 'name' => 'location', 'label' => $GLOBALS['TL_LANG']['tl_recommendation']['location'], 'inputType' => 'text', - 'eval' => array('optional'=>true, 'maxlength'=>128), - ), - 'text' => array - ( + 'eval' => ['optional'=>true, 'maxlength'=>128], + ], + 'text' => [ 'name' => 'text', 'label' => $GLOBALS['TL_LANG']['tl_recommendation']['text'], 'inputType' => 'textarea', - 'eval' => array('mandatory'=>true, 'rows'=>4, 'cols'=>40) - ), - 'email' => array - ( + 'eval' => ['mandatory'=>true, 'rows'=>4, 'cols'=>40] + ], + 'email' => [ 'name' => 'email', 'label' => $GLOBALS['TL_LANG']['tl_recommendation']['email'], 'inputType' => 'text', - 'eval' => array('optional'=>true, 'maxlength'=>255, 'rgxp'=>'email', 'decodeEntities'=>true), - ), - ); + 'eval' => ['optional'=>true, 'maxlength'=>255, 'rgxp'=>'email', 'decodeEntities'=>true], + ], + ]; // Captcha - if (!$this->recommendation_disableCaptcha == true) + if (!$this->recommendation_disableCaptcha) { - $arrFields['captcha'] = array - ( + $arrFields['captcha'] = [ 'name' => 'captcha', 'label' => $GLOBALS['TL_LANG']['MSC']['securityQuestion'], 'inputType' => 'captcha', - 'eval' => array('mandatory'=>true) - ); + 'eval' => ['mandatory'=>true] + ]; } // Set e-mail as mandatory and non-optional if comments should be validated via activation mail @@ -174,18 +163,17 @@ protected function compile() // Set an opt-in checkbox when privacy text is given if ($this->recommendation_privacyText) { - $arrFields['privacy'] = array - ( + $arrFields['privacy'] = [ 'name' => 'privacy', 'inputType' => 'checkbox', - 'options' => array(1=>$this->recommendation_privacyText), - 'eval' => array('mandatory'=>true) - ); + 'options' => [1=>$this->recommendation_privacyText], + 'eval' => ['mandatory'=>true] + ]; } $doNotSubmit = false; - $arrWidgets = array(); - $strFormId = 'recommendation_' . $this->id; + $arrWidgets = []; + $strFormId = 'recommendation_' . $this->id; // Optional recommendation form fields $arrOptionalFormFields = StringUtil::deserialize($this->recommendation_optionalFormFields, true); @@ -194,7 +182,7 @@ protected function compile() foreach ($arrFields as $fieldName => $arrField) { // Check for optional form fields - if(($arrField['eval']['optional'] ?? null) && !\in_array($fieldName, $arrOptionalFormFields)) + if (($arrField['eval']['optional'] ?? null) && !\in_array($fieldName, $arrOptionalFormFields)) { continue; } @@ -230,17 +218,18 @@ protected function compile() $arrWidgets[$arrField['name']] = $objWidget; } - $this->Template->fields = $arrWidgets; - $this->Template->submit = $GLOBALS['TL_LANG']['tl_recommendation']['recommendation_submit']; - $this->Template->formId = $strFormId; - $this->Template->hasError = $doNotSubmit; + $this->Template->fields = $arrWidgets; + $this->Template->submit = $GLOBALS['TL_LANG']['tl_recommendation']['recommendation_submit']; + $this->Template->formId = $strFormId; + $this->Template->hasError = $doNotSubmit; + $this->Template->requestToken = System::getContainer()->get('contao.csrf.token_manager')->getDefaultTokenValue(); - $session = System::getContainer()->get('session'); + $objSession = System::getContainer()->get('request_stack')->getSession(); // Do not index or cache the page with the confirmation message - if ($session->isStarted()) + if ($objSession->isStarted()) { - $flashBag = $session->getFlashBag(); + $flashBag = $objSession->getFlashBag(); if ($flashBag->has('recommendation_added')) { @@ -255,7 +244,7 @@ protected function compile() // Do not parse any tags in the recommendation $strText = StringUtil::specialchars(trim($arrWidgets['text']->value)); - $strText = str_replace(array('&', '<', '>'), array('[&]', '[lt]', '[gt]'), $strText); + $strText = str_replace(['&', '<', '>'], ['[&]', '[lt]', '[gt]'], $strText); // Remove multiple line feeds $strText = preg_replace('@\n\n+@', "\n\n", $strText); @@ -264,8 +253,7 @@ protected function compile() $strText = preg_replace('/(href|src|on[a-z]+)="[^"]*(contao\/main\.php|typolight\/main\.php|javascript|vbscri?pt|script|alert|document|cookie|window)[^"]*"+/i', '$1="#"', $strText); // Prepare the record - $arrData = array - ( + $arrData = [ 'tstamp' => $time, 'pid' => $this->recommendation_archive, 'title' => $arrWidgets['title']->value ?: '', @@ -279,7 +267,7 @@ protected function compile() 'text' => $this->convertLineFeeds($strText), 'rating' => $arrWidgets['rating']->value, 'published' => $this->recommendation_moderate ? '' : 1 - ); + ]; // Store the recommendation $objRecommendation = new RecommendationModel(); @@ -308,7 +296,7 @@ protected function compile() } else { - $session->getFlashBag()->set('recommendation_added', $this->getFlashBagMessage()); + $objSession->getFlashBag()->set('recommendation_added', $this->getFlashBagMessage()); } $this->reload(); @@ -317,12 +305,8 @@ protected function compile() /** * Convert line feeds to
tags - * - * @param string $strText - * - * @return string */ - public function convertLineFeeds($strText) + public function convertLineFeeds(string $strText): string { $strText = preg_replace('/\r?\n/', '
', $strText); @@ -332,23 +316,20 @@ public function convertLineFeeds($strText) $strText = '

' . $strText . '

'; } - $arrReplace = array - ( + $arrReplace = [ '@
\s?
\s?@' => "

\n

", // Convert two linebreaks into a new paragraph '@\s?

@' => '

', // Remove BR tags before closing P tags '@

'

@' => '' // Do not nest DIVs inside paragraphs - ); + ]; return preg_replace(array_keys($arrReplace), array_values($arrReplace), $strText); } /** * Get flashbag message - * - * @return string */ - protected function getFlashBagMessage() + protected function getFlashBagMessage(): string { // Confirmation e-mail if ($this->recommendation_activate) @@ -368,22 +349,20 @@ protected function getFlashBagMessage() /** * Sends a notification to the administrator - * - * @param RecommendationModel $objRecommendation */ - protected function sendNotificationMail($objRecommendation) + protected function sendNotificationMail(RecommendationModel $objRecommendation): void { $strText = $objRecommendation->text; $objEmail = new Email(); - $objEmail->from = $GLOBALS['TL_ADMIN_EMAIL']; - $objEmail->fromName = $GLOBALS['TL_ADMIN_NAME']; + $objEmail->from = $GLOBALS['TL_ADMIN_EMAIL'] ?? null; + $objEmail->fromName = $GLOBALS['TL_ADMIN_NAME'] ?? null; $objEmail->subject = sprintf($GLOBALS['TL_LANG']['tl_recommendation_notification']['email_subject'], Idna::decode(Environment::get('host'))); // Convert the recommendation to plain text $strText = strip_tags($strText); $strText = StringUtil::decodeEntities($strText); - $strText = str_replace(array('[&]', '[lt]', '[gt]'), array('&', '<', '>'), $strText); + $strText = str_replace(['[&]', '[lt]', '[gt]'], ['&', '<', '>'], $strText); // Add the recommendation details $objEmail->text = sprintf( @@ -407,39 +386,44 @@ protected function sendNotificationMail($objRecommendation) /** * Send the verification mail - * - * @param array $arrData - * @param integer $id */ - protected function sendVerificationMail($arrData, $id) + protected function sendVerificationMail(array $arrData, int $id): void { + $container = System::getContainer(); + /** @var OptIn $optIn */ - $optIn = System::getContainer()->get('contao.opt-in'); - $optInToken = $optIn->create('rec', $arrData['email'], array('tl_recommendation'=>array($id))); + $optIn = $container->get('contao.opt_in'); + $optInToken = $optIn->create('rec', $arrData['email'], ['tl_recommendation'=> [$id]]); // Prepare the simple token data $arrTokenData = $arrData; - $arrTokenData['token'] = $optInToken->getIdentifier(); - $arrTokenData['domain'] = Idna::decode(Environment::get('host')); - $arrTokenData['link'] = Idna::decode(Environment::get('base')) . Environment::get('request') . ((strpos(Environment::get('request'), '?') !== false) ? '&' : '?') . 'token=' . $optInToken->getIdentifier(); + $arrTokenData['token'] = $optInToken->getIdentifier(); + $arrTokenData['domain'] = Idna::decode(Environment::get('host')); + $arrTokenData['link'] = Idna::decode(Environment::get('base')) . Environment::get('request') . ((str_contains(Environment::get('request'), '?')) ? '&' : '?') . 'token=' . $optInToken->getIdentifier(); $arrTokenData['channel'] = ''; // Send the token - $optInToken->send(sprintf($GLOBALS['TL_LANG']['tl_recommendation_notification']['email_activation'][0], Idna::decode(Environment::get('host'))), StringUtil::parseSimpleTokens($this->recommendation_activateText, $arrTokenData)); + $optInToken->send(sprintf($GLOBALS['TL_LANG']['tl_recommendation_notification']['email_activation'][0], Idna::decode(Environment::get('host'))), $container->get('contao.string.simple_token_parser')->parse($this->recommendation_activateText, $arrTokenData)); } /** * Verifies the recommendation */ - protected function verifyRecommendation() + protected function verifyRecommendation(): void { $this->Template = new FrontendTemplate('mod_message'); /** @var OptIn $optin */ - $optIn = System::getContainer()->get('contao.opt-in'); + $optIn = System::getContainer()->get('contao.opt_in'); // Find an unconfirmed token - if ((!$optInToken = $optIn->find(Input::get('token'))) || !$optInToken->isValid() || \count($arrRelated = $optInToken->getRelatedRecords()) < 1 || key($arrRelated) != 'tl_recommendation' || \count($arrIds = current($arrRelated)) < 1) + if ( + (!$optInToken = $optIn->find(Input::get('token'))) || + !$optInToken->isValid() || + \count($arrRelated = $optInToken->getRelatedRecords()) < 1 || + key($arrRelated) != 'tl_recommendation' || + \count($arrIds = current($arrRelated)) < 1 + ) { $this->Template->type = 'error'; $this->Template->message = $GLOBALS['TL_LANG']['MSC']['invalidToken']; @@ -455,7 +439,7 @@ protected function verifyRecommendation() return; } - $arrRecommendations = array(); + $arrRecommendations = []; foreach ($arrIds as $intId) { @@ -481,7 +465,6 @@ protected function verifyRecommendation() $objRecommendation->verified = 1; $objRecommendation->save(); - // Notify system administrator via e-mail if ($this->recommendation_notify) { @@ -492,7 +475,7 @@ protected function verifyRecommendation() // Log activity $logger = System::getContainer()->get('monolog.logger.contao'); - $logger->log(LogLevel::INFO, 'Recommendation ID ' . $objRecommendation->id . ' (' . Idna::decodeEmail($objRecommendation->email) . ') has been verified', array('contao' => new ContaoContext(__METHOD__, TL_ACCESS))); + $logger?->info('Recommendation ID ' . $objRecommendation->id . ' (' . Idna::decodeEmail($objRecommendation->email) . ') has been verified'); // Redirect to the jumpTo page if (($objTarget = $this->objModel->getRelated('recommendation_activateJumpTo')) instanceof PageModel) @@ -512,3 +495,5 @@ protected function verifyRecommendation() } } } + +class_alias(ModuleRecommendationForm::class, 'ModuleRecommendationForm'); diff --git a/src/Resources/contao/modules/ModuleRecommendationList.php b/contao/modules/ModuleRecommendationList.php similarity index 91% rename from src/Resources/contao/modules/ModuleRecommendationList.php rename to contao/modules/ModuleRecommendationList.php index b414791..d74745a 100644 --- a/src/Resources/contao/modules/ModuleRecommendationList.php +++ b/contao/modules/ModuleRecommendationList.php @@ -17,6 +17,7 @@ use Contao\Pagination; use Contao\StringUtil; use Contao\System; +use Oveleon\ContaoRecommendationBundle\Model\RecommendationModel; /** * Front end module "recommendation list". @@ -24,12 +25,9 @@ * @property array $recommendation_archives * @property string $recommendation_featured * @property string $recommendation_order - * - * @author Fabian Ekert */ class ModuleRecommendationList extends ModuleRecommendation { - /** * Template * @var string @@ -41,18 +39,18 @@ class ModuleRecommendationList extends ModuleRecommendation * * @return string */ - public function generate() + public function generate(): string { $request = System::getContainer()->get('request_stack')->getCurrentRequest(); if ($request && System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request)) { $objTemplate = new BackendTemplate('be_wildcard'); - $objTemplate->wildcard = '### ' . mb_strtoupper($GLOBALS['TL_LANG']['FMD']['recommendationlist'][0], 'UTF-8') . ' ###'; + $objTemplate->wildcard = '### ' . $GLOBALS['TL_LANG']['FMD']['recommendationlist'][0] . ' ###'; $objTemplate->title = $this->headline; $objTemplate->id = $this->id; $objTemplate->link = $this->name; - $objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id; + $objTemplate->href = StringUtil::specialcharsUrl(System::getContainer()->get('router')->generate('contao_backend', array('do'=>'themes', 'table'=>'tl_module', 'act'=>'edit', 'id'=>$this->id))); return $objTemplate->parse(); } @@ -114,7 +112,7 @@ protected function compile() $blnFeatured = null; } - $this->Template->recommendations = array(); + $this->Template->recommendations = []; $this->Template->empty = $GLOBALS['TL_LANG']['MSC']['emptyRecommendationList']; // Get the total number of items @@ -137,7 +135,7 @@ protected function compile() } // Get the current page - $id = 'page_n' . $this->id; + $id = 'page_r' . $this->id; $page = Input::get($id) ?? 1; // Do not index or cache the page if the page number is outside the range @@ -203,14 +201,6 @@ protected function countItems($recommendationArchives, $blnFeatured, $minRating) /** * Fetch the matching items - * - * @param array $recommendationArchives - * @param boolean $blnFeatured - * @param integer $limit - * @param integer $offset - * @param integer $minRating - * - * @return Collection|RecommendationModel|null */ protected function fetchItems($recommendationArchives, $blnFeatured, $limit, $offset, $minRating) { @@ -257,6 +247,8 @@ protected function fetchItems($recommendationArchives, $blnFeatured, $limit, $of $order .= "$t.date DESC"; } - return RecommendationModel::findPublishedByPids($recommendationArchives, $blnFeatured, $limit, $offset, $minRating, array('order'=>$order)); + return RecommendationModel::findPublishedByPids($recommendationArchives, $blnFeatured, $limit, $offset, $minRating, ['order'=>$order]); } } + +class_alias(ModuleRecommendationList::class, 'ModuleRecommendationList'); diff --git a/src/Resources/contao/modules/ModuleRecommendationReader.php b/contao/modules/ModuleRecommendationReader.php similarity index 83% rename from src/Resources/contao/modules/ModuleRecommendationReader.php rename to contao/modules/ModuleRecommendationReader.php index 465d6db..afe756f 100644 --- a/src/Resources/contao/modules/ModuleRecommendationReader.php +++ b/contao/modules/ModuleRecommendationReader.php @@ -16,13 +16,13 @@ use Contao\Input; use Contao\StringUtil; use Contao\System; +use Oveleon\ContaoRecommendationBundle\Model\RecommendationArchiveModel; +use Oveleon\ContaoRecommendationBundle\Model\RecommendationModel; /** * Front end module "recommendation reader". * * @property array $recommendation_archives - * - * @author Fabian Ekert */ class ModuleRecommendationReader extends ModuleRecommendation { @@ -45,11 +45,11 @@ public function generate() if ($request && System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request)) { $objTemplate = new BackendTemplate('be_wildcard'); - $objTemplate->wildcard = '### ' . mb_strtoupper($GLOBALS['TL_LANG']['FMD']['recommendationreader'][0], 'UTF-8') . ' ###'; + $objTemplate->wildcard = '### ' . $GLOBALS['TL_LANG']['FMD']['recommendationreader'][0] . ' ###'; $objTemplate->title = $this->headline; $objTemplate->id = $this->id; $objTemplate->link = $this->name; - $objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id; + $objTemplate->href = StringUtil::specialcharsUrl(System::getContainer()->get('router')->generate('contao_backend', array('do'=>'themes', 'table'=>'tl_module', 'act'=>'edit', 'id'=>$this->id))); return $objTemplate->parse(); } @@ -85,7 +85,7 @@ protected function compile() $this->Template->referer = 'javascript:history.go(-1)'; $this->Template->back = $GLOBALS['TL_LANG']['MSC']['goBack']; - // Get the news item + // Get the recommendation item $objRecommendation = RecommendationModel::findPublishedByParentAndIdOrAlias(Input::get('items'), $this->recommendation_archives); if (null === $objRecommendation) @@ -100,3 +100,5 @@ protected function compile() $this->Template->recommendation = $arrRecommendation; } } + +class_alias(ModuleRecommendationReader::class, 'ModuleRecommendationReader'); diff --git a/src/Resources/contao/templates/modules/mod_recommendationform.html5 b/contao/templates/modules/mod_recommendationform.html5 similarity index 89% rename from src/Resources/contao/templates/modules/mod_recommendationform.html5 rename to contao/templates/modules/mod_recommendationform.html5 index 0adb651..3eb51a8 100644 --- a/src/Resources/contao/templates/modules/mod_recommendationform.html5 +++ b/contao/templates/modules/mod_recommendationform.html5 @@ -10,7 +10,7 @@
- + fields as $field): ?> parse() ?> diff --git a/src/Resources/contao/templates/modules/mod_recommendationlist.html5 b/contao/templates/modules/mod_recommendationlist.html5 similarity index 100% rename from src/Resources/contao/templates/modules/mod_recommendationlist.html5 rename to contao/templates/modules/mod_recommendationlist.html5 diff --git a/src/Resources/contao/templates/modules/mod_recommendationreader.html5 b/contao/templates/modules/mod_recommendationreader.html5 similarity index 100% rename from src/Resources/contao/templates/modules/mod_recommendationreader.html5 rename to contao/templates/modules/mod_recommendationreader.html5 diff --git a/src/Resources/contao/templates/recommendation/recommendation_default.html5 b/contao/templates/recommendation/recommendation_default.html5 similarity index 100% rename from src/Resources/contao/templates/recommendation/recommendation_default.html5 rename to contao/templates/recommendation/recommendation_default.html5 diff --git a/src/Resources/contao/templates/recommendation/recommendation_full.html5 b/contao/templates/recommendation/recommendation_full.html5 similarity index 100% rename from src/Resources/contao/templates/recommendation/recommendation_full.html5 rename to contao/templates/recommendation/recommendation_full.html5 diff --git a/src/Resources/contao/templates/recommendation/recommendation_latest.html5 b/contao/templates/recommendation/recommendation_latest.html5 similarity index 100% rename from src/Resources/contao/templates/recommendation/recommendation_latest.html5 rename to contao/templates/recommendation/recommendation_latest.html5 diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..b64057e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + ./src + + + + + + + + + + ./tests + + + + + + diff --git a/src/ContaoManager/Plugin.php b/src/ContaoManager/Plugin.php index ba93dbd..d32d03c 100644 --- a/src/ContaoManager/Plugin.php +++ b/src/ContaoManager/Plugin.php @@ -16,22 +16,17 @@ use Contao\ManagerPlugin\Bundle\Parser\ParserInterface; use Oveleon\ContaoRecommendationBundle\ContaoRecommendationBundle; -/** - * Plugin for the Contao Manager. - * - * @author Fabian Ekert - */ class Plugin implements BundlePluginInterface { /** * {@inheritdoc} */ - public function getBundles(ParserInterface $parser) + public function getBundles(ParserInterface $parser): array { return [ BundleConfig::create(ContaoRecommendationBundle::class) + ->setReplace(['recommendation']) ->setLoadAfter([ContaoCoreBundle::class]) - ->setReplace(['recommendation']), ]; } } diff --git a/src/ContaoRecommendationBundle.php b/src/ContaoRecommendationBundle.php index 462578f..fbc6ae7 100644 --- a/src/ContaoRecommendationBundle.php +++ b/src/ContaoRecommendationBundle.php @@ -10,11 +10,10 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; -/** - * Configures the Contao recommendation bundle. - * - * @author Fabian Ekert - */ class ContaoRecommendationBundle extends Bundle { + public function getPath(): string + { + return \dirname(__DIR__); + } } diff --git a/src/Cron/PurgeRecommendationsCron.php b/src/Cron/PurgeRecommendationsCron.php new file mode 100644 index 0000000..14e3665 --- /dev/null +++ b/src/Cron/PurgeRecommendationsCron.php @@ -0,0 +1,44 @@ +framework->initialize(); + + $recommendations = $this->framework->getAdapter(RecommendationModel::class)->findExpiredRecommendations(); + + if (null === $recommendations) + { + return; + } + + /** @var RecommendationModel $recommendation */ + foreach ($recommendations as $recommendation) + { + $recommendation->delete(); + } + + $this->logger?->info('Purged the unactivated recommendations'); + } +} diff --git a/src/DependencyInjection/ContaoRecommendationExtension.php b/src/DependencyInjection/ContaoRecommendationExtension.php new file mode 100644 index 0000000..16e0e07 --- /dev/null +++ b/src/DependencyInjection/ContaoRecommendationExtension.php @@ -0,0 +1,26 @@ +load('services.yaml'); + } +} diff --git a/src/EventListener/DataContainer/RecommendationArchiveListener.php b/src/EventListener/DataContainer/RecommendationArchiveListener.php new file mode 100644 index 0000000..87f8bd2 --- /dev/null +++ b/src/EventListener/DataContainer/RecommendationArchiveListener.php @@ -0,0 +1,157 @@ +get('security.helper')->isGranted(ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE, 'tl_recommendation_archive') ? '' . Image::getHtml($icon, $label) . ' ' : Image::getHtml(preg_replace('/\.svg$/i', '_.svg', $icon)) . ' '; + } + + /** + * Return the copy archive button + */ + public function copyArchive(array $row, string $href, string $label, string $title, string $icon, string $attributes): string + { + return System::getContainer()->get('security.helper')->isGranted(ContaoRecommendationPermissions::USER_CAN_CREATE_ARCHIVES) ? '' . Image::getHtml($icon, $label) . ' ' : Image::getHtml(preg_replace('/\.svg$/i', '_.svg', $icon)) . ' '; + } + + /** + * Return the delete archive button + */ + public function deleteArchive(array $row, string $href, string $label, string $title, string $icon, string $attributes): string + { + return System::getContainer()->get('security.helper')->isGranted(ContaoRecommendationPermissions::USER_CAN_DELETE_ARCHIVES) ? '' . Image::getHtml($icon, $label) . ' ' : Image::getHtml(preg_replace('/\.svg$/i', '_.svg', $icon)) . ' '; + } + + /** + * Add the new archive to the permissions + */ + public function adjustPermissions(int|string $insertId): void + { + // The oncreate_callback passes $insertId as second argument + if (func_num_args() == 4) + { + $insertId = func_get_arg(1); + } + + $objUser = Controller::getContainer()->get('security.helper')->getUser(); + + if ($objUser->isAdmin) + { + return; + } + + // Set root IDs + if (empty($objUser->recommendations) || !is_array($objUser->recommendations)) + { + $root = [0]; + } + else + { + $root = $objUser->recommendations; + } + + // The archive is enabled already + if (in_array($insertId, $root)) + { + return; + } + + /** @var AttributeBagInterface $objSessionBag */ + $objSessionBag = System::getContainer()->get('request_stack')->getSession()->getBag('contao_backend'); + + $arrNew = $objSessionBag->get('new_records'); + + if (is_array($arrNew['tl_recommendation_archive']) && in_array($insertId, $arrNew['tl_recommendation_archive'])) + { + $db = Database::getInstance(); + + // Add the permissions on group level + if ($objUser->inherit != 'custom') + { + $objGroup = $db->execute("SELECT id, recommendations, recommendationp FROM tl_user_group WHERE id IN(" . implode(',', array_map('\intval', $objUser->groups)) . ")"); + + while ($objGroup->next()) + { + $arrRecommendationp = StringUtil::deserialize($objGroup->recommendationp); + + if (is_array($arrRecommendationp) && in_array('create', $arrRecommendationp)) + { + $arrRecommendations = StringUtil::deserialize($objGroup->recommendations, true); + $arrRecommendations[] = $insertId; + + $db->prepare("UPDATE tl_user_group SET recommendations=? WHERE id=?") + ->execute(serialize($arrRecommendations), $objGroup->id); + } + } + } + + // Add the permissions on user level + if ($objUser->inherit != 'group') + { + $objUser = $db->prepare("SELECT recommendations, recommendationp FROM tl_user WHERE id=?") + ->limit(1) + ->execute($objUser->id); + + $arrRecommendationp = StringUtil::deserialize($objUser->recommendationp); + + if (is_array($arrRecommendationp) && in_array('create', $arrRecommendationp)) + { + $arrRecommendations = StringUtil::deserialize($objUser->recommendations, true); + $arrRecommendations[] = $insertId; + + $db->prepare("UPDATE tl_user SET recommendations=? WHERE id=?") + ->execute(serialize($arrRecommendations), $objUser->id); + } + } + + // Add the new element to the user object + $root[] = $insertId; + $objUser->recommendations = $root; + } + } + + public function addSitemapCacheInvalidationTag(DataContainer $dc, array $tags): array + { + $pageModel = PageModel::findWithDetails($dc->activeRecord->jumpTo); + + if ($pageModel === null) + { + return $tags; + } + + return array_merge($tags, ['contao.sitemap.' . $pageModel->rootId]); + } +} diff --git a/src/EventListener/DataContainer/RecommendationListener.php b/src/EventListener/DataContainer/RecommendationListener.php new file mode 100644 index 0000000..7204df9 --- /dev/null +++ b/src/EventListener/DataContainer/RecommendationListener.php @@ -0,0 +1,290 @@ +activeRecord) + { + return; + } + + $arrSet['date'] = strtotime(date('Y-m-d', $dc->activeRecord->date) . ' ' . date('H:i:s', $dc->activeRecord->time)); + $arrSet['time'] = $arrSet['date']; + + $db = Database::getInstance(); + $db->prepare("UPDATE tl_recommendation %s WHERE id=?")->set($arrSet)->execute($dc->id); + } + + /** + * @throws Exception + */ + public function generateRecommendationAlias($varValue, DataContainer $dc) + { + $db = Database::getInstance(); + + $aliasExists = function (string $alias) use ($dc, $db): bool + { + return $db->prepare("SELECT id FROM tl_recommendation WHERE alias=? AND id!=?")->execute($alias, $dc->id)->numRows > 0; + }; + + // Generate alias if there is none + if (!$varValue) + { + $varValue = System::getContainer()->get('contao.slug')->generate($dc->activeRecord->title, RecommendationArchiveModel::findByPk($dc->activeRecord->pid)->jumpTo, $aliasExists); + } + elseif (preg_match('/^[1-9]\d*$/', $varValue)) + { + throw new Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasNumeric'], $varValue)); + } + elseif ($aliasExists($varValue)) + { + throw new Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $varValue)); + } + + return $varValue; + } + + public function checkRecommendationPermission(DataContainer $dc) + { + $objUser = Controller::getContainer()->get('security.helper')->getUser(); + + if ($objUser->isAdmin) + { + return; + } + + // Set the root IDs + if (empty($objUser->recommendations) || !is_array($objUser->recommendations)) + { + $root = [0]; + } + else + { + $root = $objUser->recommendations; + } + + $id = strlen(Input::get('id')) ? Input::get('id') : $dc->currentPid; + $db = Database::getInstance(); + + // Check current action + switch (Input::get('act')) + { + case 'paste': + case 'select': + // Check currentPid + if (!in_array($dc->currentPid, $root)) + { + throw new AccessDeniedException('Not enough permissions to access recommendation archive ID ' . $id . '.'); + } + break; + + case 'create': + if (!Input::get('pid') || !in_array(Input::get('pid'), $root)) + { + throw new AccessDeniedException('Not enough permissions to create recommendation items in recommendation archive ID ' . Input::get('pid') . '.'); + } + break; + + case 'cut': + case 'copy': + if (Input::get('act') == 'cut' && Input::get('mode') == 1) + { + $objArchive = $db->prepare("SELECT pid FROM tl_recommendation WHERE id=?") + ->limit(1) + ->execute(Input::get('pid')); + + if ($objArchive->numRows < 1) + { + throw new AccessDeniedException('Invalid recommendation item ID ' . Input::get('pid') . '.'); + } + + $pid = $objArchive->pid; + } + else + { + $pid = Input::get('pid'); + } + + if (!in_array($pid, $root)) + { + throw new AccessDeniedException('Not enough permissions to ' . Input::get('act') . ' recommendation item ID ' . $id . ' to recommendation archive ID ' . $pid . '.'); + } + // no break + + case 'edit': + case 'show': + case 'delete': + case 'toggle': + $objArchive = $db->prepare("SELECT pid FROM tl_recommendation WHERE id=?") + ->limit(1) + ->execute($id); + + if ($objArchive->numRows < 1) + { + throw new AccessDeniedException('Invalid recommendation item ID ' . $id . '.'); + } + + if (!in_array($objArchive->pid, $root)) + { + throw new AccessDeniedException('Not enough permissions to ' . Input::get('act') . ' recommendation item ID ' . $id . ' of recommendation archive ID ' . $objArchive->pid . '.'); + } + break; + + case 'editAll': + case 'deleteAll': + case 'overrideAll': + case 'cutAll': + case 'copyAll': + if (!in_array($id, $root)) + { + throw new AccessDeniedException('Not enough permissions to access recommendation archive ID ' . $id . '.'); + } + + $objArchive = $db->prepare("SELECT id FROM tl_recommendation WHERE pid=?") + ->execute($id); + + $objSession = System::getContainer()->get('request_stack')->getSession(); + + $session = $objSession->all(); + $session['CURRENT']['IDS'] = array_intersect((array) $session['CURRENT']['IDS'], $objArchive->fetchEach('id')); + $objSession->replace($session); + break; + + default: + if (Input::get('act')) + { + throw new AccessDeniedException('Invalid command "' . Input::get('act') . '".'); + } + + if (!in_array($id, $root)) + { + throw new AccessDeniedException('Not enough permissions to access recommendation archive ID ' . $id . '.'); + } + break; + } + } + + /** + * List a recommendation record + */ + public function listRecommendations(array $arrRow): string + { + if(!$arrRow['verified']) + { + return '
' . $arrRow['author'] . ' [' . ($GLOBALS['TL_LANG']['tl_recommendation']['notVerified'] ?? null) . ']
'; + } + + return '
' . $arrRow['author'] . ' [' . Date::parse(Config::get('datimFormat'), $arrRow['date']) . ']
'; + } + + /** + * Check for modified recommendation and update the XML files if necessary + */ + public function generateSitemap(): void + { + /** @var SessionInterface $objSession */ + $objSession = System::getContainer()->get('request_stack')->getSession(); + + $session = $objSession->get('recommendation_updater'); + + if (empty($session) || !is_array($session)) + { + return; + } + + $automator = new Automator(); + $automator->generateSitemap(); + + $objSession->set('recommendation_updater', null); + } + + /** + * Schedule a recommendation update + * + * This method is triggered when a single recommendation or multiple recommendations + * are modified (edit/editAll), moved (cut/cutAll) or deleted (delete/deleteAll). + * Since duplicated items are unpublished by default, it is not necessary to + * schedule updates on copyAll as well. + */ + public function scheduleUpdate(DataContainer $dc): void + { + // Return if there is no ID + if (!$dc->activeRecord || !$dc->activeRecord->pid || Input::get('act') == 'copy') + { + return; + } + + /** @var SessionInterface $objSession */ + $objSession = System::getContainer()->get('request_stack')->getSession(); + + // Store the ID in the session + $session = $objSession->get('recommendation_updater'); + $session[] = $dc->activeRecord->pid; + $objSession->set('recommendation_updater', array_unique($session)); + } + + public function addSitemapCacheInvalidationTag(DataContainer $dc, array $tags): array + { + $archiveModel = RecommendationArchiveModel::findByPk($dc->activeRecord->pid); + $pageModel = PageModel::findWithDetails($archiveModel->jumpTo); + + if ($pageModel === null) + { + return $tags; + } + + return array_merge($tags, ['contao.sitemap.' . $pageModel->rootId]); + } +} diff --git a/src/EventListener/SitemapListener.php b/src/EventListener/SitemapListener.php new file mode 100644 index 0000000..a8c2575 --- /dev/null +++ b/src/EventListener/SitemapListener.php @@ -0,0 +1,132 @@ +framework->createInstance(Database::class)->getChildRecords($event->getRootPageIds(), 'tl_page'); + + // Early return here in the unlikely case that there are no pages + if (empty($arrRoot)) + { + return; + } + + $arrPages = []; + $time = time(); + + // Get all recommendation archives + $objArchives = $this->framework->getAdapter(RecommendationArchiveModel::class)->findByProtected(''); + + if (null === $objArchives) + { + return; + } + + // Walk through each recommendation archive + foreach ($objArchives as $objArchive) + { + // Skip recommendation archives without target page + if (!$objArchive->jumpTo) + { + continue; + } + + // Skip recommendation archives outside the root nodes + if (!\in_array($objArchive->jumpTo, $arrRoot, true)) + { + continue; + } + + $objParent = $this->framework->getAdapter(PageModel::class)->findWithDetails($objArchive->jumpTo); + + // The target page does not exist + if (null === $objParent) + { + continue; + } + + // The target page has not been published + if (!$objParent->published || ($objParent->start && $objParent->start > $time) || ($objParent->stop && $objParent->stop <= $time)) + { + continue; + } + + // The target page is protected + if ($objParent->protected) + { + continue; + } + + // The target page is exempt from the sitemap + if ('noindex,nofollow' === $objParent->robots) + { + continue; + } + + // Get the items + $objRecommendations = $this->framework->getAdapter(RecommendationModel::class)->findPublishedByPid($objArchive->id); + + if (null === $objRecommendations) + { + continue; + } + + foreach ($objRecommendations as $objRecommendation) + { + $arrPages[] = $objParent->getAbsoluteUrl('/' . ($objRecommendation->alias ?: $objRecommendation->id)); + } + } + + foreach ($arrPages as $strUrl) + { + $this->addUrlToDefaultUrlSet($strUrl, $event); + } + } + + private function addUrlToDefaultUrlSet(string $url, $event): self + { + $sitemap = $event->getDocument(); + $urlSet = $sitemap->getElementsByTagNameNS('https://www.sitemaps.org/schemas/sitemap/0.9', 'urlset')->item(0); + + if (null === $urlSet) + { + return $this; + } + + $loc = $sitemap->createElement('loc', $url); + $urlEl = $sitemap->createElement('url'); + $urlEl->appendChild($loc); + $urlSet->appendChild($urlEl); + + $sitemap->appendChild($urlSet); + + return $this; + } +} diff --git a/src/Resources/contao/models/RecommendationArchiveModel.php b/src/Model/RecommendationArchiveModel.php similarity index 95% rename from src/Resources/contao/models/RecommendationArchiveModel.php rename to src/Model/RecommendationArchiveModel.php index b9fe6ad..a283ac5 100644 --- a/src/Resources/contao/models/RecommendationArchiveModel.php +++ b/src/Model/RecommendationArchiveModel.php @@ -6,7 +6,7 @@ * (c) https://www.oveleon.de/ */ -namespace Oveleon\ContaoRecommendationBundle; +namespace Oveleon\ContaoRecommendationBundle\Model; use Contao\Model; use Contao\Model\Collection; @@ -46,8 +46,6 @@ * @method static integer countByJumpTo($val, array $opt=array()) * @method static integer countByProtected($val, array $opt=array()) * @method static integer countByGroups($val, array $opt=array()) - * - * @author Fabian Ekert */ class RecommendationArchiveModel extends Model { @@ -58,3 +56,5 @@ class RecommendationArchiveModel extends Model */ protected static $strTable = 'tl_recommendation_archive'; } + +class_alias(RecommendationArchiveModel::class, 'RecommendationArchiveModel'); diff --git a/src/Resources/contao/models/RecommendationModel.php b/src/Model/RecommendationModel.php similarity index 80% rename from src/Resources/contao/models/RecommendationModel.php rename to src/Model/RecommendationModel.php index 45f6ee0..e4dd912 100644 --- a/src/Resources/contao/models/RecommendationModel.php +++ b/src/Model/RecommendationModel.php @@ -6,7 +6,7 @@ * (c) https://www.oveleon.de/ */ -namespace Oveleon\ContaoRecommendationBundle; +namespace Oveleon\ContaoRecommendationBundle\Model; use Contao\Database; use Contao\Date; @@ -104,12 +104,9 @@ * @method static integer countByPublished($val, array $opt=array()) * @method static integer countByStart($val, array $opt=array()) * @method static integer countByStop($val, array $opt=array()) - * - * @author Fabian Ekert */ class RecommendationModel extends Model { - /** * Table name * @var string @@ -118,14 +115,8 @@ class RecommendationModel extends Model /** * Find a published recommendation from one or more recommendation archives by its ID or alias - * - * @param mixed $varId The numeric ID or alias name - * @param array $arrPids An array of parent IDs - * @param array $arrOptions An optional options array - * - * @return RecommendationModel|null The model or null if there are no recommendations */ - public static function findPublishedByParentAndIdOrAlias($varId, $arrPids, array $arrOptions=array()) + public static function findPublishedByParentAndIdOrAlias(mixed $varId, $arrPids, array $arrOptions=array()): ?RecommendationModel { if (empty($arrPids) || !\is_array($arrPids)) { @@ -133,7 +124,7 @@ public static function findPublishedByParentAndIdOrAlias($varId, $arrPids, array } $t = static::$strTable; - $arrColumns = !is_numeric($varId) ? array("$t.alias=?") : array("$t.id=?"); + $arrColumns = !is_numeric($varId) ? ["$t.alias=?"] : ["$t.id=?"]; $arrColumns[] = "$t.pid IN(" . implode(',', array_map('\intval', $arrPids)) . ") AND $t.verified='1'"; if (!static::isPreviewMode($arrOptions)) @@ -147,16 +138,11 @@ public static function findPublishedByParentAndIdOrAlias($varId, $arrPids, array /** * Find published recommendations with the default redirect target by their parent ID - * - * @param integer $intPid The recommendation archive ID - * @param array $arrOptions An optional options array - * - * @return Collection|RecommendationModel[]|RecommendationModel|null A collection of models or null if there are no recommendations */ - public static function findPublishedByPid($intPid, array $arrOptions=array()) + public static function findPublishedByPid(int $intPid, array $arrOptions=array()): Collection|RecommendationModel|array|null { $t = static::$strTable; - $arrColumns = array("$t.pid=? AND $t.verified='1'"); + $arrColumns = ["$t.pid=? AND $t.verified='1'"]; if (!static::isPreviewMode($arrOptions)) { @@ -174,16 +160,8 @@ public static function findPublishedByPid($intPid, array $arrOptions=array()) /** * Find published recommendations by their parent ID - * - * @param array $arrPids An array of recommendation archive IDs - * @param boolean $blnFeatured If true, return only featured recommendations, if false, return only unfeatured recommendations - * @param integer $intLimit An optional limit - * @param integer $intOffset An optional offset - * @param array $arrOptions An optional options array - * - * @return Collection|RecommendationModel[]|RecommendationModel|null A collection of models or null if there are no recommendations */ - public static function findPublishedByPids($arrPids, $blnFeatured=null, $intLimit=0, $intOffset=0, $minRating=null, array $arrOptions=array()) + public static function findPublishedByPids($arrPids, bool $blnFeatured=null, int $intLimit=0, int $intOffset=0, $minRating=null, array $arrOptions=array()): Collection|RecommendationModel|array|null { if (empty($arrPids) || !\is_array($arrPids)) { @@ -191,7 +169,7 @@ public static function findPublishedByPids($arrPids, $blnFeatured=null, $intLimi } $t = static::$strTable; - $arrColumns = array("$t.pid IN(" . implode(',', array_map('\intval', $arrPids)) . ") AND $t.verified='1'"); + $arrColumns = ["$t.pid IN(" . implode(',', array_map('\intval', $arrPids)) . ") AND $t.verified='1'"]; if ($blnFeatured === true) { @@ -226,14 +204,8 @@ public static function findPublishedByPids($arrPids, $blnFeatured=null, $intLimi /** * Count published recommendations by their parent ID - * - * @param array $arrPids An array of recommendation archive IDs - * @param boolean $blnFeatured If true, return only featured recommendations, if false, return only unfeatured recommendations - * @param array $arrOptions An optional options array - * - * @return integer The number of recommendations */ - public static function countPublishedByPids($arrPids, $blnFeatured=null, $minRating=null, array $arrOptions=array()) + public static function countPublishedByPids($arrPids, bool $blnFeatured=null, $minRating=null, array $arrOptions=array()): int { if (empty($arrPids) || !\is_array($arrPids)) { @@ -241,7 +213,7 @@ public static function countPublishedByPids($arrPids, $blnFeatured=null, $minRat } $t = static::$strTable; - $arrColumns = array("$t.pid IN(" . implode(',', array_map('\intval', $arrPids)) . ") AND $t.verified='1'"); + $arrColumns = ["$t.pid IN(" . implode(',', array_map('\intval', $arrPids)) . ") AND $t.verified='1'"]; if ($blnFeatured === true) { @@ -268,12 +240,8 @@ public static function countPublishedByPids($arrPids, $blnFeatured=null, $minRat /** * Find registrations that have not been activated for more than 24 hours - * - * @param array $arrOptions An optional options array - * - * @return Collection|RecommendationModel[]|RecommendationModel|null A collection of models or null if there are no expired recommendations */ - public static function findExpiredRecommendations(array $arrOptions=array()) + public static function findExpiredRecommendations(array $arrOptions=array()): Collection|RecommendationModel|array|null { $t = static::$strTable; $objDatabase = Database::getInstance(); @@ -289,3 +257,5 @@ public static function findExpiredRecommendations(array $arrOptions=array()) return static::createCollectionFromDbResult($objResult, $t); } } + +class_alias(RecommendationModel::class, 'RecommendationModel'); diff --git a/src/Resources/contao/classes/Recommendation.php b/src/Resources/contao/classes/Recommendation.php deleted file mode 100644 index 815ef37..0000000 --- a/src/Resources/contao/classes/Recommendation.php +++ /dev/null @@ -1,141 +0,0 @@ - - */ -class Recommendation extends Backend -{ - /** - * Purge recommendations that have not been activated within 24 hours - */ - public function purgeRecommendations() - { - $objRecommendation = RecommendationModel::findExpiredRecommendations(); - - if ($objRecommendation === null) - { - return; - } - - while ($objRecommendation->next()) - { - $objRecommendation->delete(); - } - - // Add a log entry - $logger = System::getContainer()->get('monolog.logger.contao'); - $logger->log(LogLevel::INFO, 'Purged the unactivated recommendations', array('contao' => new ContaoContext(__METHOD__, TL_CRON))); - } - - /** - * Add Recommendations to the indexer - * - * @param array $arrPages - * @param integer $intRoot - * @param boolean $blnIsSitemap - * - * @return array - */ - public function getSearchablePages($arrPages, $intRoot=0, $blnIsSitemap=false) - { - $arrRoot = array(); - - if ($intRoot > 0) - { - $arrRoot = $this->Database->getChildRecords($intRoot, 'tl_page'); - } - - $arrProcessed = array(); - $time = time(); - - // Get all categories - $objRecommendationArchive = RecommendationArchiveModel::findByProtected(''); - - // Walk through each archive - if ($objRecommendationArchive !== null) - { - while($objRecommendationArchive->next()) - { - // Skip archives without a target page - if (!$objRecommendationArchive->jumpTo) - { - continue; - } - - // Skip archives outside the root nodes - if (!empty($arrRoot) && !\in_array($objRecommendationArchive->jumpTo, $arrRoot)) - { - continue; - } - - // Get the URL of the jumpTo page - if (!isset($arrProcessed[$objRecommendationArchive->jumpTo])) - { - $objParent = PageModel::findWithDetails($objRecommendationArchive->jumpTo); - - // The target page does not exist - if ($objParent === null) - { - continue; - } - - // The target page has not been published - if (!$objParent->published || ($objParent->start && $objParent->start > $time) || ($objParent->stop && $objParent->stop <= $time)) - { - continue; - } - - if($blnIsSitemap) - { - // The target page is protected - if ($objParent->protected) - { - continue; - } - - // the target page is exempt from the sitemap - if ($objParent->robots == 'noindex,nofollow') - { - continue; - } - } - - // Generate the URL - $arrProcessed[$objRecommendationArchive->jumpTo] = $objParent->getAbsoluteUrl(Config::get('useAutoItem') ? '/%s' : '/items/%s'); - } - - $strUrl = $arrProcessed[$objRecommendationArchive->jumpTo]; - - // Get the items - $objItems = RecommendationModel::findPublishedByPid($objRecommendationArchive->id); - - if($objItems !== null) - { - while ($objItems->next()) - { - $arrPages[] = sprintf(preg_replace('/%(?!s)/', '%%', $strUrl), ($objItems->alias ?: $objItems->id)); - } - } - } - } - - return $arrPages; - } -} diff --git a/src/Resources/contao/config/config.php b/src/Resources/contao/config/config.php deleted file mode 100644 index 5aa2f67..0000000 --- a/src/Resources/contao/config/config.php +++ /dev/null @@ -1,51 +0,0 @@ - array - ( - 'tables' => array('tl_recommendation_archive', 'tl_recommendation') - ) -)); -array_insert($GLOBALS['BE_MOD']['system'], 3, array -( - 'recommendation_settings' => array - ( - 'tables' => array('tl_recommendation_settings'), - 'hideInNavigation' => true - ) -)); - -// Front end modules -array_insert($GLOBALS['FE_MOD'], 2, array -( - 'recommendation' => array - ( - 'recommendationlist' => 'Oveleon\ContaoRecommendationBundle\ModuleRecommendationList', - 'recommendationreader' => 'Oveleon\ContaoRecommendationBundle\ModuleRecommendationReader', - 'recommendationform' => 'Oveleon\ContaoRecommendationBundle\ModuleRecommendationForm', - ) -)); - -// Models -$GLOBALS['TL_MODELS']['tl_recommendation'] = 'Oveleon\ContaoRecommendationBundle\RecommendationModel'; -$GLOBALS['TL_MODELS']['tl_recommendation_archive'] = 'Oveleon\ContaoRecommendationBundle\RecommendationArchiveModel'; - -// Add permissions -$GLOBALS['TL_PERMISSIONS'][] = 'recommendations'; -$GLOBALS['TL_PERMISSIONS'][] = 'recommendationp'; - -// Cron jobs -$GLOBALS['TL_CRON']['daily']['purgeRecommendations'] = array('Oveleon\ContaoRecommendationBundle\Recommendation', 'purgeRecommendations'); - -// Register hooks -$GLOBALS['TL_HOOKS']['getSearchablePages'][] = array('Oveleon\ContaoRecommendationBundle\Recommendation', 'getSearchablePages'); - -// Style sheet -if (TL_MODE == 'BE') -{ - $GLOBALS['TL_CSS'][] = 'bundles/contaorecommendation/style.css|static'; -} diff --git a/src/Resources/contao/dca/tl_recommendation.php b/src/Resources/contao/dca/tl_recommendation.php deleted file mode 100644 index 03290bb..0000000 --- a/src/Resources/contao/dca/tl_recommendation.php +++ /dev/null @@ -1,873 +0,0 @@ - array - ( - 'dataContainer' => 'Table', - 'ptable' => 'tl_recommendation_archive', - 'switchToEdit' => true, - 'enableVersioning' => true, - 'onload_callback' => array - ( - array('tl_recommendation', 'checkPermission'), - array('tl_recommendation', 'generateSitemap') - ), - 'oncut_callback' => array - ( - array('tl_recommendation', 'scheduleUpdate') - ), - 'ondelete_callback' => array - ( - array('tl_recommendation', 'scheduleUpdate') - ), - 'onsubmit_callback' => array - ( - array('tl_recommendation', 'adjustTime'), - array('tl_recommendation', 'scheduleUpdate') - ), - 'oninvalidate_cache_tags_callback' => array - ( - array('tl_recommendation', 'addSitemapCacheInvalidationTag'), - ), - 'sql' => array - ( - 'keys' => array - ( - 'id' => 'primary', - 'alias' => 'index', - 'pid,start,stop,published' => 'index' - ) - ) - ), - - // List - 'list' => array - ( - 'sorting' => array - ( - 'mode' => 4, - 'fields' => array('date DESC'), - 'headerFields' => array('title', 'jumpTo', 'tstamp', 'protected'), - 'panelLayout' => 'filter;sort,search,limit', - 'child_record_callback' => array('tl_recommendation', 'listRecommendations'), - 'child_record_class' => 'no_padding' - ), - 'global_operations' => array - ( - 'all' => array - ( - 'label' => &$GLOBALS['TL_LANG']['MSC']['all'], - 'href' => 'act=select', - 'class' => 'header_edit_all', - 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"' - ) - ), - 'operations' => array - ( - 'edit' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['edit'], - 'href' => 'act=edit', - 'icon' => 'edit.svg' - ), - 'copy' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['copy'], - 'href' => 'act=paste&mode=copy', - 'icon' => 'copy.svg' - ), - 'cut' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['cut'], - 'href' => 'act=paste&mode=cut', - 'icon' => 'cut.svg' - ), - 'delete' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['delete'], - 'href' => 'act=delete', - 'icon' => 'delete.svg', - 'attributes' => 'onclick="if(!confirm(\'' . ($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? null) . '\'))return false;Backend.getScrollOffset()"' - ), - 'toggle' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['toggle'], - 'icon' => 'visible.svg', - 'attributes' => 'onclick="Backend.getScrollOffset();return AjaxRequest.toggleVisibility(this,%s)"', - 'button_callback' => array('tl_recommendation', 'toggleIcon') - ), - 'feature' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['feature'], - 'icon' => 'featured.svg', - 'attributes' => 'onclick="Backend.getScrollOffset();return AjaxRequest.toggleFeatured(this,%s)"', - 'button_callback' => array('tl_recommendation', 'iconFeatured') - ), - 'show' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['show'], - 'href' => 'act=show', - 'icon' => 'show.svg' - ) - ) - ), - - // Palettes - 'palettes' => array - ( - 'default' => '{title_legend},author,title,alias,email,location;{date_legend},date,time;{recommendation_legend},text,imageUrl,rating,customField;{teaser_legend:hide},teaser;{expert_legend:hide},cssClass,featured;{publish_legend},published,start,stop' - ), - - // Fields - 'fields' => array - ( - 'id' => array - ( - 'sql' => "int(10) unsigned NOT NULL auto_increment" - ), - 'pid' => array - ( - 'foreignKey' => 'tl_recommendation_archive.title', - 'sql' => "int(10) unsigned NOT NULL default '0'", - 'relation' => array('type'=>'belongsTo', 'load'=>'lazy') - ), - 'tstamp' => array - ( - 'sql' => "int(10) unsigned NOT NULL default '0'" - ), - 'title' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['title'], - 'exclude' => true, - 'search' => true, - 'sorting' => true, - 'flag' => 1, - 'inputType' => 'text', - 'eval' => array('maxlength'=>255, 'tl_class'=>'w50 clr'), - 'sql' => "varchar(255) NOT NULL default ''" - ), - 'alias' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['alias'], - 'exclude' => true, - 'search' => true, - 'inputType' => 'text', - 'eval' => array('rgxp'=>'alias', 'doNotCopy'=>true, 'unique'=>true, 'maxlength'=>128, 'tl_class'=>'w50'), - 'save_callback' => array - ( - array('tl_recommendation', 'generateAlias') - ), - 'sql' => "varchar(255) BINARY NOT NULL default ''" - ), - 'author' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['author'], - 'exclude' => true, - 'search' => true, - 'sorting' => true, - 'flag' => 1, - 'inputType' => 'text', - 'eval' => array('doNotCopy'=>true, 'mandatory'=>true, 'maxlength'=>128, 'tl_class'=>'w50'), - 'sql' => "varchar(128) NOT NULL default ''" - ), - 'email' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['email'], - 'exclude' => true, - 'search' => true, - 'inputType' => 'text', - 'eval' => array('doNotCopy'=>true, 'maxlength'=>255, 'rgxp'=>'email', 'decodeEntities'=>true, 'tl_class'=>'w50'), - 'sql' => "varchar(255) NOT NULL default ''" - ), - 'location' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['location'], - 'exclude' => true, - 'search' => true, - 'sorting' => true, - 'flag' => 1, - 'inputType' => 'text', - 'eval' => array('doNotCopy'=>true, 'maxlength'=>128, 'tl_class'=>'w50'), - 'sql' => "varchar(128) NOT NULL default ''" - ), - 'date' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['date'], - 'default' => time(), - 'exclude' => true, - 'filter' => true, - 'sorting' => true, - 'flag' => 8, - 'inputType' => 'text', - 'eval' => array('rgxp'=>'date', 'mandatory'=>true, 'doNotCopy'=>true, 'datepicker'=>true, 'tl_class'=>'w50 wizard'), - 'load_callback' => array - ( - array('tl_recommendation', 'loadDate') - ), - 'sql' => "int(10) unsigned NOT NULL default '0'" - ), - 'time' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['time'], - 'default' => time(), - 'exclude' => true, - 'inputType' => 'text', - 'eval' => array('rgxp'=>'time', 'mandatory'=>true, 'doNotCopy'=>true, 'tl_class'=>'w50'), - 'load_callback' => array - ( - array('tl_recommendation', 'loadTime') - ), - 'sql' => "int(10) unsigned NOT NULL default '0'" - ), - 'teaser' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['teaser'], - 'exclude' => true, - 'search' => true, - 'inputType' => 'textarea', - 'eval' => array('rte'=>'tinyMCE', 'tl_class'=>'clr'), - 'sql' => "mediumtext NULL" - ), - 'text' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['text'], - 'exclude' => true, - 'search' => true, - 'inputType' => 'textarea', - 'eval' => array('mandatory'=>true, 'rte'=>'tinyMCE', 'tl_class'=>'clr'), - 'sql' => "mediumtext NULL" - ), - 'imageUrl' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['imageUrl'], - 'exclude' => true, - 'search' => true, - 'inputType' => 'text', - 'eval' => array('rgxp'=>'url', 'decodeEntities'=>true, 'maxlength'=>255, 'dcaPicker'=>true, 'tl_class'=>'w50 wizard'), - 'sql' => "varchar(255) NOT NULL default ''" - ), - 'rating' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['rating'], - 'default' => 5, - 'exclude' => true, - 'search' => true, - 'filter' => true, - 'sorting' => true, - 'inputType' => 'select', - 'options' => array(1,2,3,4,5), - 'eval' => array('mandatory'=>true, 'tl_class'=>'w50'), - 'sql' => "char(1) NOT NULL default ''" - ), - 'customField' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['customField'], - 'exclude' => true, - 'inputType' => 'text', - 'eval' => array('doNotCopy'=>true, 'maxlength'=>255, 'tl_class'=>'w100 clr'), - 'sql' => "varchar(255) NOT NULL default ''" - ), - 'cssClass' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['cssClass'], - 'exclude' => true, - 'inputType' => 'text', - 'eval' => array('tl_class'=>'w50'), - 'sql' => "varchar(255) NOT NULL default ''" - ), - 'featured' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['featured'], - 'exclude' => true, - 'filter' => true, - 'inputType' => 'checkbox', - 'eval' => array('tl_class'=>'w50 m12'), - 'sql' => "char(1) NOT NULL default ''" - ), - 'verified' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['verified'], - 'filter' => true, - 'eval' => array('isBoolean'=>true), - 'sql' => "char(1) NOT NULL default '1'" - ), - 'published' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['published'], - 'exclude' => true, - 'filter' => true, - 'flag' => 1, - 'inputType' => 'checkbox', - 'eval' => array('doNotCopy'=>true), - 'sql' => "char(1) NOT NULL default ''" - ), - 'start' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['start'], - 'exclude' => true, - 'inputType' => 'text', - 'eval' => array('rgxp'=>'datim', 'datepicker'=>true, 'tl_class'=>'w50 wizard'), - 'sql' => "varchar(10) NOT NULL default ''" - ), - 'stop' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation']['stop'], - 'exclude' => true, - 'inputType' => 'text', - 'eval' => array('rgxp'=>'datim', 'datepicker'=>true, 'tl_class'=>'w50 wizard'), - 'sql' => "varchar(10) NOT NULL default ''" - ) - ) -); - -/** - * Provide miscellaneous methods that are used by the data configuration array. - * - * @author Fabian Ekert - */ -class tl_recommendation extends Contao\Backend -{ - - /** - * Import the back end user object - */ - public function __construct() - { - parent::__construct(); - $this->import('Contao\BackendUser', 'User'); - } - - /** - * Check permissions to edit table tl_recommendation - * - * @throws Contao\CoreBundle\Exception\AccessDeniedException - */ - public function checkPermission() - { - if ($this->User->isAdmin) - { - return; - } - - // Set the root IDs - if (empty($this->User->recommendations) || !is_array($this->User->recommendations)) - { - $root = array(0); - } - else - { - $root = $this->User->recommendations; - } - - $id = strlen(Contao\Input::get('id')) ? Contao\Input::get('id') : CURRENT_ID; - - // Check current action - switch (Contao\Input::get('act')) - { - case 'paste': - case 'select': - if (!in_array(CURRENT_ID, $root)) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to access recommendation archive ID ' . $id . '.'); - } - break; - - case 'create': - if (!Contao\Input::get('pid') || !in_array(Contao\Input::get('pid'), $root)) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to create recommendation item in recommendation archive ID ' . Input::get('pid') . '.'); - } - break; - - case 'cut': - case 'copy': - if (Contao\Input::get('act') == 'cut' && Contao\Input::get('mode') == 1) - { - $objArchive = $this->Database->prepare("SELECT pid FROM tl_recommendation WHERE id=?") - ->limit(1) - ->execute(Contao\Input::get('pid')); - - if ($objArchive->numRows < 1) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Invalid recommendation item ID ' . Contao\Input::get('pid') . '.'); - } - - $pid = $objArchive->pid; - } - else - { - $pid = Input::get('pid'); - } - - if (!in_array($pid, $root)) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to ' . Contao\Input::get('act') . ' recommendation item ID ' . $id . ' to recommendation archive ID ' . $pid . '.'); - } - // no break - - case 'edit': - case 'show': - case 'delete': - case 'toggle': - case 'feature': - $objArchive = $this->Database->prepare("SELECT pid FROM tl_recommendation WHERE id=?") - ->limit(1) - ->execute($id); - - if ($objArchive->numRows < 1) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Invalid recommendation item ID ' . $id . '.'); - } - - if (!in_array($objArchive->pid, $root)) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to ' . Contao\Input::get('act') . ' recommendation item ID ' . $id . ' of recommendation archive ID ' . $objArchive->pid . '.'); - } - break; - - case 'editAll': - case 'deleteAll': - case 'overrideAll': - case 'cutAll': - case 'copyAll': - if (!in_array($id, $root)) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to access recommendation archive ID ' . $id . '.'); - } - - $objArchive = $this->Database->prepare("SELECT id FROM tl_recommendation WHERE pid=?") - ->execute($id); - - /** @var Symfony\Component\HttpFoundation\Session\SessionInterface $objSession */ - $objSession = Contao\System::getContainer()->get('session'); - - $session = $objSession->all(); - $session['CURRENT']['IDS'] = array_intersect((array) $session['CURRENT']['IDS'], $objArchive->fetchEach('id')); - $objSession->replace($session); - break; - - default: - if (Contao\Input::get('act')) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Invalid command "' . Contao\Input::get('act') . '".'); - } - - if (!in_array($id, $root)) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to access recommendation archive ID ' . $id . '.'); - } - break; - } - } - - /** - * Auto-generate the recommendation alias if it has not been set yet - * - * @param mixed $varValue - * @param Contao\DataContainer $dc - * - * @return string - * - * @throws Exception - */ - public function generateAlias($varValue, Contao\DataContainer $dc) - { - $autoAlias = false; - - // Generate alias if title is set - if ($varValue == '' && !empty($dc->activeRecord->title)) - { - $autoAlias = true; - $varValue = Contao\StringUtil::generateAlias($dc->activeRecord->title); - } - - $objAlias = $this->Database->prepare("SELECT id FROM tl_recommendation WHERE alias=? AND alias!='' AND id!=?") - ->execute($varValue, $dc->id); - - // Check whether the recommendation alias exists - if ($objAlias->numRows) - { - if (!$autoAlias) - { - throw new Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $varValue)); - } - - $varValue .= '-' . $dc->id; - } - - return $varValue; - } - - /** - * Set the timestamp to 00:00:00 - * - * @param integer $value - * - * @return integer - */ - public function loadDate($value) - { - return strtotime(date('Y-m-d', $value) . ' 00:00:00'); - } - - /** - * Set the timestamp to 1970-01-01 - * - * @param integer $value - * - * @return integer - */ - public function loadTime($value) - { - return strtotime('1970-01-01 ' . date('H:i:s', $value)); - } - - /** - * List a recommendation record - * - * @param array $arrRow - * - * @return string - */ - public function listRecommendations($arrRow) - { - if(!$arrRow['verified']) - { - return '
' . $arrRow['author'] . ' [' . $GLOBALS['TL_LANG']['tl_recommendation']['notVerified'] . ']
'; - } - - return '
' . $arrRow['author'] . ' [' . Date::parse(Config::get('datimFormat'), $arrRow['date']) . ']
'; - } - - /** - * Adjust start end end time of the event based on date, span, startTime and endTime - * - * @param Contao\DataContainer $dc - */ - public function adjustTime(Contao\DataContainer $dc) - { - // Return if there is no active record (override all) - if (!$dc->activeRecord) - { - return; - } - - $arrSet['date'] = strtotime(date('Y-m-d', $dc->activeRecord->date) . ' ' . date('H:i:s', $dc->activeRecord->time)); - $arrSet['time'] = $arrSet['date']; - - $this->Database->prepare("UPDATE tl_recommendation %s WHERE id=?")->set($arrSet)->execute($dc->id); - } - - /** - * Check for modified recommendation and update the XML files if necessary - */ - public function generateSitemap() - { - /** @var Symfony\Component\HttpFoundation\Session\SessionInterface $objSession */ - $objSession = Contao\System::getContainer()->get('session'); - - $session = $objSession->get('recommendation_updater'); - - if (empty($session) || !is_array($session)) - { - return; - } - - $this->import('Contao\Automator', 'Automator'); - $this->Automator->generateSitemap(); - - $objSession->set('recommendation_updater', null); - } - - /** - * Schedule a recommendation update - * - * This method is triggered when a single recommendation or multiple recommendations - * are modified (edit/editAll), moved (cut/cutAll) or deleted (delete/deleteAll). - * Since duplicated items are unpublished by default, it is not necessary to - * schedule updates on copyAll as well. - * - * @param Contao\DataContainer $dc - */ - public function scheduleUpdate(Contao\DataContainer $dc) - { - // Return if there is no ID - if (!$dc->activeRecord || !$dc->activeRecord->pid || Contao\Input::get('act') == 'copy') - { - return; - } - - /** @var Symfony\Component\HttpFoundation\Session\SessionInterface $objSession */ - $objSession = Contao\System::getContainer()->get('session'); - - // Store the ID in the session - $session = $objSession->get('recommendation_updater'); - $session[] = $dc->activeRecord->pid; - $objSession->set('recommendation_updater', array_unique($session)); - } - - /** - * Return the "feature/unfeature element" button - * - * @param array $row - * @param string $href - * @param string $label - * @param string $title - * @param string $icon - * @param string $attributes - * - * @return string - */ - public function iconFeatured($row, $href, $label, $title, $icon, $attributes) - { - if (strlen(Contao\Input::get('fid'))) - { - $this->toggleFeatured(Contao\Input::get('fid'), (Input::get('state') == 1), (@func_get_arg(12) ?: null)); - $this->redirect($this->getReferer()); - } - - // Check permissions AFTER checking the fid, so hacking attempts are logged - if (!$this->User->hasAccess('tl_recommendation::featured', 'alexf')) - { - return ''; - } - - $href .= '&fid='.$row['id'].'&state='.($row['featured'] ? '' : 1); - - if (!$row['featured']) - { - $icon = 'featured_.svg'; - } - - return ''.Contao\Image::getHtml($icon, $label, 'data-state="' . ($row['featured'] ? 1 : 0) . '"').' '; - } - - /** - * Feature/unfeature a recommendation - * - * @param integer $intId - * @param boolean $blnVisible - * @param Contao\DataContainer $dc - * - * @throws Contao\CoreBundle\Exception\AccessDeniedException - */ - public function toggleFeatured($intId, $blnVisible, Contao\DataContainer $dc=null) - { - // Check permissions to edit - Contao\Input::setGet('id', $intId); - Contao\Input::setGet('act', 'feature'); - $this->checkPermission(); - - // Check permissions to feature - if (!$this->User->hasAccess('tl_recommendation::featured', 'alexf')) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to feature/unfeature recommendation ID ' . $intId . '.'); - } - - $objVersions = new Contao\Versions('tl_recommendation', $intId); - $objVersions->initialize(); - - // Trigger the save_callback - if (is_array($GLOBALS['TL_DCA']['tl_recommendation']['fields']['featured']['save_callback'] ?? null)) - { - foreach ($GLOBALS['TL_DCA']['tl_recommendation']['fields']['featured']['save_callback'] as $callback) - { - if (is_array($callback)) - { - $this->import($callback[0]); - $blnVisible = $this->{$callback[0]}->{$callback[1]}($blnVisible, $dc); - } - elseif (is_callable($callback)) - { - $blnVisible = $callback($blnVisible, $this); - } - } - } - - // Update the database - $this->Database->prepare("UPDATE tl_recommendation SET tstamp=". time() .", featured='" . ($blnVisible ? 1 : '') . "' WHERE id=?") - ->execute($intId); - - $objVersions->create(); - - if ($dc) - { - $dc->invalidateCacheTags(); - } - } - - /** - * Return the "toggle visibility" button - * - * @param array $row - * @param string $href - * @param string $label - * @param string $title - * @param string $icon - * @param string $attributes - * - * @return string - */ - public function toggleIcon($row, $href, $label, $title, $icon, $attributes) - { - if (strlen(Input::get('tid'))) - { - $this->toggleVisibility(Contao\Input::get('tid'), (Contao\Input::get('state') == 1), (func_num_args() <= 12 ? null : func_get_arg(12))); - $this->redirect($this->getReferer()); - } - - // Check permissions AFTER checking the tid, so hacking attempts are logged - if (!$this->User->hasAccess('tl_recommendation::published', 'alexf')) - { - return ''; - } - - $href .= '&tid=' . $row['id'] . '&state=' . ($row['published'] ? '' : 1); - - if (!$row['published']) - { - $icon = 'invisible.svg'; - } - - return '' . Image::getHtml($icon, $label, 'data-state="' . ($row['published'] ? 1 : 0) . '"') . ' '; - } - - /** - * Disable/enable a recommendation - * - * @param integer $intId - * @param boolean $blnVisible - * @param Contao\DataContainer $dc - */ - public function toggleVisibility($intId, $blnVisible, Contao\DataContainer $dc=null) - { - // Set the ID and action - Contao\Input::setGet('id', $intId); - Contao\Input::setGet('act', 'toggle'); - - if ($dc) - { - $dc->id = $intId; - } - - // Trigger the onload_callback - if (is_array($GLOBALS['TL_DCA']['tl_recommendation']['config']['onload_callback'] ?? null)) - { - foreach ($GLOBALS['TL_DCA']['tl_recommendation']['config']['onload_callback'] as $callback) - { - if (is_array($callback)) - { - $this->import($callback[0]); - $this->{$callback[0]}->{$callback[1]}($dc); - } - elseif (is_callable($callback)) - { - $callback($dc); - } - } - } - - // Check the field access - if (!$this->User->hasAccess('tl_recommendation::published', 'alexf')) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to publish/unpublish recommendation ID ' . $intId . '.'); - } - - // Set the current record - if ($dc) - { - $objRow = $this->Database->prepare("SELECT * FROM tl_recommendation WHERE id=?") - ->limit(1) - ->execute($intId); - - if ($objRow->numRows) - { - $dc->activeRecord = $objRow; - } - } - - $objVersions = new Contao\Versions('tl_recommendation', $intId); - $objVersions->initialize(); - - // Trigger the save_callback - if (is_array($GLOBALS['TL_DCA']['tl_recommendation']['fields']['published']['save_callback'] ?? null)) - { - foreach ($GLOBALS['TL_DCA']['tl_recommendation']['fields']['published']['save_callback'] as $callback) - { - if (is_array($callback)) - { - $this->import($callback[0]); - $blnVisible = $this->{$callback[0]}->{$callback[1]}($blnVisible, $dc); - } - elseif (is_callable($callback)) - { - $blnVisible = $callback($blnVisible, $dc); - } - } - } - - $time = time(); - - // Update the database - $this->Database->prepare("UPDATE tl_recommendation SET tstamp=$time, published='" . ($blnVisible ? '1' : '') . "' WHERE id=?") - ->execute($intId); - - if ($dc) - { - $dc->activeRecord->tstamp = $time; - $dc->activeRecord->published = ($blnVisible ? '1' : ''); - } - - // Trigger the onsubmit_callback - if (is_array($GLOBALS['TL_DCA']['tl_recommendation']['config']['onsubmit_callback'] ?? null)) - { - foreach ($GLOBALS['TL_DCA']['tl_recommendation']['config']['onsubmit_callback'] as $callback) - { - if (is_array($callback)) - { - $this->import($callback[0]); - $this->{$callback[0]}->{$callback[1]}($dc); - } - elseif (is_callable($callback)) - { - $callback($dc); - } - } - } - - $objVersions->create(); - - if ($dc) - { - $dc->invalidateCacheTags(); - } - } - - /** - * @param Contao\DataContainer $dc - * - * @return array - */ - public function addSitemapCacheInvalidationTag($dc, array $tags) - { - $archiveModel = RecommendationArchiveModel::findByPk($dc->activeRecord->pid); - $pageModel = Contao\PageModel::findWithDetails($archiveModel->jumpTo); - - if ($pageModel === null) - { - return $tags; - } - - return array_merge($tags, array('contao.sitemap.' . $pageModel->rootId)); - } -} diff --git a/src/Resources/contao/dca/tl_recommendation_archive.php b/src/Resources/contao/dca/tl_recommendation_archive.php deleted file mode 100644 index 75de17e..0000000 --- a/src/Resources/contao/dca/tl_recommendation_archive.php +++ /dev/null @@ -1,441 +0,0 @@ - array - ( - 'dataContainer' => 'Table', - 'ctable' => array('tl_recommendation'), - 'switchToEdit' => true, - 'enableVersioning' => true, - 'onload_callback' => array - ( - array('tl_recommendation_archive', 'checkPermission') - ), - 'oncreate_callback' => array - ( - array('tl_recommendation_archive', 'adjustPermissions') - ), - 'oncopy_callback' => array - ( - array('tl_recommendation_archive', 'adjustPermissions') - ), - 'oninvalidate_cache_tags_callback' => array - ( - array('tl_recommendation_archive', 'addSitemapCacheInvalidationTag'), - ), - 'sql' => array - ( - 'keys' => array - ( - 'id' => 'primary' - ) - ) - ), - - // List - 'list' => array - ( - 'sorting' => array - ( - 'mode' => 1, - 'fields' => array('title'), - 'flag' => 1, - 'panelLayout' => 'filter;search,limit' - ), - 'label' => array - ( - 'fields' => array('title'), - 'format' => '%s' - ), - 'global_operations' => array - ( - 'settings' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['settings'], - 'href' => 'do=recommendation_settings', - 'class' => '', - 'icon' => 'edit.svg', - 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"' - ), - 'all' => array - ( - 'label' => &$GLOBALS['TL_LANG']['MSC']['all'], - 'href' => 'act=select', - 'class' => 'header_edit_all', - 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"' - ) - ), - 'operations' => array - ( - 'edit' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['edit'], - 'href' => 'table=tl_recommendation', - 'icon' => 'edit.svg' - ), - 'editheader' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['editheader'], - 'href' => 'act=edit', - 'icon' => 'header.svg', - 'button_callback' => array('tl_recommendation_archive', 'editHeader') - ), - 'copy' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['copy'], - 'href' => 'act=copy', - 'icon' => 'copy.svg', - 'button_callback' => array('tl_recommendation_archive', 'copyArchive') - ), - 'delete' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['delete'], - 'href' => 'act=delete', - 'icon' => 'delete.svg', - 'attributes' => 'onclick="if(!confirm(\'' . ($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? null) . '\'))return false;Backend.getScrollOffset()"', - 'button_callback' => array('tl_recommendation_archive', 'deleteArchive') - ), - 'show' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['show'], - 'href' => 'act=show', - 'icon' => 'show.svg' - ) - ) - ), - - // Palettes - 'palettes' => array - ( - '__selector__' => array('protected'), - 'default' => '{title_legend},title,jumpTo;{protected_legend:hide},protected' - ), - - // Subpalettes - 'subpalettes' => array - ( - 'protected' => 'groups' - ), - - // Fields - 'fields' => array - ( - 'id' => array - ( - 'sql' => "int(10) unsigned NOT NULL auto_increment" - ), - 'tstamp' => array - ( - 'sql' => "int(10) unsigned NOT NULL default '0'" - ), - 'title' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['title'], - 'exclude' => true, - 'search' => true, - 'inputType' => 'text', - 'eval' => array('mandatory'=>true, 'maxlength'=>255, 'tl_class'=>'w50'), - 'sql' => "varchar(255) NOT NULL default ''" - ), - 'jumpTo' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['jumpTo'], - 'exclude' => true, - 'inputType' => 'pageTree', - 'foreignKey' => 'tl_page.title', - 'eval' => array('fieldType'=>'radio', 'tl_class'=>'clr'), - 'sql' => "int(10) unsigned NOT NULL default '0'", - 'relation' => array('type'=>'hasOne', 'load'=>'eager') - ), - 'protected' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['protected'], - 'exclude' => true, - 'filter' => true, - 'inputType' => 'checkbox', - 'eval' => array('submitOnChange'=>true), - 'sql' => "char(1) NOT NULL default ''" - ), - 'groups' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_archive']['groups'], - 'exclude' => true, - 'inputType' => 'checkbox', - 'foreignKey' => 'tl_member_group.name', - 'eval' => array('mandatory'=>true, 'multiple'=>true), - 'sql' => "blob NULL", - 'relation' => array('type'=>'hasMany', 'load'=>'lazy') - ) - ) -); - -/** - * Provide miscellaneous methods that are used by the data configuration array. - * - * @author Fabian Ekert - */ -class tl_recommendation_archive extends Contao\Backend -{ - - /** - * Import the back end user object - */ - public function __construct() - { - parent::__construct(); - $this->import('Contao\BackendUser', 'User'); - } - - /** - * Check permissions to edit table tl_recommendation_archive - * - * @throws Contao\CoreBundle\Exception\AccessDeniedException - */ - public function checkPermission() - { - if ($this->User->isAdmin) - { - return; - } - - // Set root IDs - if (empty($this->User->recommendations) || !is_array($this->User->recommendations)) - { - $root = array(0); - } - else - { - $root = $this->User->recommendations; - } - - $GLOBALS['TL_DCA']['tl_recommendation_archive']['list']['sorting']['root'] = $root; - - // Check permissions to add archives - if (!$this->User->hasAccess('create', 'recommendationp')) - { - $GLOBALS['TL_DCA']['tl_recommendation_archive']['config']['closed'] = true; - $GLOBALS['TL_DCA']['tl_recommendation_archive']['config']['notCreatable'] = true; - $GLOBALS['TL_DCA']['tl_recommendation_archive']['config']['notCopyable'] = true; - } - - // Check permissions to delete calendars - if (!$this->User->hasAccess('delete', 'recommendationp')) - { - $GLOBALS['TL_DCA']['tl_recommendation_archive']['config']['notDeletable'] = true; - } - - /** @var Symfony\Component\HttpFoundation\Session\SessionInterface $objSession */ - $objSession = Contao\System::getContainer()->get('session'); - - // Check current action - switch (Contao\Input::get('act')) - { - case 'select': - // Allow - break; - - case 'create': - if (!$this->User->hasAccess('create', 'recommendationp')) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to create recommendation archives.'); - } - break; - - case 'edit': - case 'copy': - case 'delete': - case 'show': - if (!in_array(Contao\Input::get('id'), $root) || (Contao\Input::get('act') == 'delete' && !$this->User->hasAccess('delete', 'recommendationp'))) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to ' . Contao\Input::get('act') . ' recommendation archive ID ' . Contao\Input::get('id') . '.'); - } - break; - - case 'editAll': - case 'deleteAll': - case 'overrideAll': - case 'copyAll': - $session = $objSession->all(); - - if (Contao\Input::get('act') == 'deleteAll' && !$this->User->hasAccess('delete', 'recommendationp')) - { - $session['CURRENT']['IDS'] = array(); - } - else - { - $session['CURRENT']['IDS'] = array_intersect((array) $session['CURRENT']['IDS'], $root); - } - $objSession->replace($session); - break; - - default: - if (strlen(Contao\Input::get('act'))) - { - throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to ' . Input::get('act') . ' recommendation archives.'); - } - break; - } - } - - /** - * Add the new archive to the permissions - * - * @param $insertId - */ - public function adjustPermissions($insertId) - { - // The oncreate_callback passes $insertId as second argument - if (func_num_args() == 4) - { - $insertId = func_get_arg(1); - } - - if ($this->User->isAdmin) - { - return; - } - - // Set root IDs - if (empty($this->User->recommendations) || !is_array($this->User->recommendations)) - { - $root = array(0); - } - else - { - $root = $this->User->recommendations; - } - - // The archive is enabled already - if (in_array($insertId, $root)) - { - return; - } - - /** @var Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface $objSessionBag */ - $objSessionBag = Contao\System::getContainer()->get('session')->getBag('contao_backend'); - - $arrNew = $objSessionBag->get('new_records'); - - if (is_array($arrNew['tl_recommendation_archive']) && in_array($insertId, $arrNew['tl_recommendation_archive'])) - { - // Add the permissions on group level - if ($this->User->inherit != 'custom') - { - $objGroup = $this->Database->execute("SELECT id, recommendations, recommendationp FROM tl_user_group WHERE id IN(" . implode(',', array_map('\intval', $this->User->groups)) . ")"); - - while ($objGroup->next()) - { - $arrRecommendationp = Contao\StringUtil::deserialize($objGroup->recommendationp); - - if (is_array($arrRecommendationp) && in_array('create', $arrRecommendationp)) - { - $arrRecommendations = Contao\StringUtil::deserialize($objGroup->recommendations, true); - $arrRecommendations[] = $insertId; - - $this->Database->prepare("UPDATE tl_user_group SET recommendations=? WHERE id=?") - ->execute(serialize($arrRecommendations), $objGroup->id); - } - } - } - - // Add the permissions on user level - if ($this->User->inherit != 'group') - { - $objUser = $this->Database->prepare("SELECT recommendations, recommendationp FROM tl_user WHERE id=?") - ->limit(1) - ->execute($this->User->id); - - $arrRecommendationp = Contao\StringUtil::deserialize($objUser->recommendationp); - - if (is_array($arrRecommendationp) && in_array('create', $arrRecommendationp)) - { - $arrRecommendations = Contao\StringUtil::deserialize($objUser->recommendations, true); - $arrRecommendations[] = $insertId; - - $this->Database->prepare("UPDATE tl_user SET recommendations=? WHERE id=?") - ->execute(serialize($arrRecommendations), $this->User->id); - } - } - - // Add the new element to the user object - $root[] = $insertId; - $this->User->recommendations = $root; - } - } - - /** - * Return the edit header button - * - * @param array $row - * @param string $href - * @param string $label - * @param string $title - * @param string $icon - * @param string $attributes - * - * @return string - */ - public function editHeader($row, $href, $label, $title, $icon, $attributes) - { - return $this->User->canEditFieldsOf('tl_recommendation_archive') ? ''.Contao\Image::getHtml($icon, $label).' ' : Contao\Image::getHtml(preg_replace('/\.svg$/i', '_.svg', $icon)).' '; - } - - /** - * Return the copy archive button - * - * @param array $row - * @param string $href - * @param string $label - * @param string $title - * @param string $icon - * @param string $attributes - * - * @return string - */ - public function copyArchive($row, $href, $label, $title, $icon, $attributes) - { - return $this->User->hasAccess('create', 'recommendationp') ? ''.Contao\Image::getHtml($icon, $label).' ' : Contao\Image::getHtml(preg_replace('/\.svg$/i', '_.svg', $icon)).' '; - } - - /** - * Return the delete archive button - * - * @param array $row - * @param string $href - * @param string $label - * @param string $title - * @param string $icon - * @param string $attributes - * - * @return string - */ - public function deleteArchive($row, $href, $label, $title, $icon, $attributes) - { - return $this->User->hasAccess('delete', 'recommendationp') ? ''.Contao\Image::getHtml($icon, $label).' ' : Contao\Image::getHtml(preg_replace('/\.svg$/i', '_.svg', $icon)).' '; - } - - /** - * @param Contao\DataContainer $dc - * - * @return array - */ - public function addSitemapCacheInvalidationTag($dc, array $tags) - { - $pageModel = PageModel::findWithDetails($dc->activeRecord->jumpTo); - - if ($pageModel === null) - { - return $tags; - } - - return array_merge($tags, array('contao.sitemap.' . $pageModel->rootId)); - } -} diff --git a/src/Resources/contao/dca/tl_recommendation_settings.php b/src/Resources/contao/dca/tl_recommendation_settings.php deleted file mode 100644 index 6c81a7c..0000000 --- a/src/Resources/contao/dca/tl_recommendation_settings.php +++ /dev/null @@ -1,41 +0,0 @@ - array - ( - 'dataContainer' => 'File', - 'closed' => true - ), - - // Palettes - 'palettes' => array - ( - 'default' => '{recommendation_legend},recommendationDefaultImage,recommendationActiveColor;' - ), - - // Fields - 'fields' => array - ( - 'recommendationDefaultImage' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_settings']['recommendationDefaultImage'], - 'inputType' => 'fileTree', - 'eval' => array('fieldType'=>'radio', 'filesOnly'=>true, 'isGallery'=>true, 'extensions'=>Contao\Config::get('validImageTypes'), 'tl_class'=>'clr') - ), - 'recommendationActiveColor' => array - ( - 'label' => &$GLOBALS['TL_LANG']['tl_recommendation_settings']['recommendationActiveColor'], - 'inputType' => 'text', - 'eval' => array('maxlength'=>6, 'multiple'=>true, 'size'=>1, 'colorpicker'=>true, 'isHexColor'=>true, 'decodeEntities'=>true, 'tl_class'=>'w50 wizard'), - ) - ) -); diff --git a/src/Resources/contao/dca/tl_user.php b/src/Resources/contao/dca/tl_user.php deleted file mode 100644 index 88907c4..0000000 --- a/src/Resources/contao/dca/tl_user.php +++ /dev/null @@ -1,37 +0,0 @@ -addLegend('recommendation_legend', 'amg_legend', Contao\CoreBundle\DataContainer\PaletteManipulator::POSITION_BEFORE) - ->addField(array('recommendations', 'recommendationp'), 'recommendation_legend', Contao\CoreBundle\DataContainer\PaletteManipulator::POSITION_APPEND) - ->applyToPalette('extend', 'tl_user') - ->applyToPalette('custom', 'tl_user') -; - -// Add fields to tl_user_group -$GLOBALS['TL_DCA']['tl_user']['fields']['recommendations'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_user']['recommendations'], - 'exclude' => true, - 'inputType' => 'checkbox', - 'foreignKey' => 'tl_recommendation_archive.title', - 'eval' => array('multiple'=>true), - 'sql' => "blob NULL" -); - -$GLOBALS['TL_DCA']['tl_user']['fields']['recommendationp'] = array -( - 'label' => &$GLOBALS['TL_LANG']['tl_user']['recommendationp'], - 'exclude' => true, - 'inputType' => 'checkbox', - 'options' => array('create', 'delete'), - 'reference' => &$GLOBALS['TL_LANG']['MSC'], - 'eval' => array('multiple'=>true), - 'sql' => "blob NULL" -); diff --git a/src/Resources/contao/languages/de/tl_user_group.xlf b/src/Resources/contao/languages/de/tl_user_group.xlf deleted file mode 100644 index a2d719b..0000000 --- a/src/Resources/contao/languages/de/tl_user_group.xlf +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Recommendation permissions - Bewertungsrechte - - - - diff --git a/src/Resources/contao/languages/en/tl_user_group.xlf b/src/Resources/contao/languages/en/tl_user_group.xlf deleted file mode 100644 index e79a5e2..0000000 --- a/src/Resources/contao/languages/en/tl_user_group.xlf +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Recommendation permissions - - - - diff --git a/src/Resources/public/style.css b/src/Resources/public/style.css deleted file mode 100644 index ec6318c..0000000 --- a/src/Resources/public/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.navigation.recommendation_settings { - display: none !important; -} diff --git a/src/Security/ContaoRecommendationPermissions.php b/src/Security/ContaoRecommendationPermissions.php new file mode 100644 index 0000000..00b39ba --- /dev/null +++ b/src/Security/ContaoRecommendationPermissions.php @@ -0,0 +1,18 @@ +createMock(ParserInterface::class); + + /** @var BundleConfig $config */ + $config = (new Plugin())->getBundles($parser)[0]; + + $this->assertInstanceOf(BundleConfig::class, $config); + $this->assertSame(ContaoRecommendationBundle::class, $config->getName()); + $this->assertSame([ContaoCoreBundle::class], $config->getLoadAfter()); + $this->assertSame(['recommendation'], $config->getReplace()); + } +} diff --git a/tests/Cron/PurgeRecommendationsCronTest.php b/tests/Cron/PurgeRecommendationsCronTest.php new file mode 100644 index 0000000..2a49a6d --- /dev/null +++ b/tests/Cron/PurgeRecommendationsCronTest.php @@ -0,0 +1,33 @@ +createMock(RecommendationModel::class); + $recommendationModel + ->expects($this->once()) + ->method('delete') + ; + + $recommendationModelAdapter = $this->mockAdapter(['findExpiredRecommendations']); + $recommendationModelAdapter + ->expects($this->once()) + ->method('findExpiredRecommendations') + ->willReturn(new Collection([$recommendationModel], RecommendationModel::getTable())) + ; + + $framework = $this->mockContaoFramework([RecommendationModel::class => $recommendationModelAdapter]); + + (new PurgeRecommendationsCron($framework, null))(); + } +} diff --git a/tests/EventListener/SitemapListenerTest.php b/tests/EventListener/SitemapListenerTest.php new file mode 100644 index 0000000..2b09e04 --- /dev/null +++ b/tests/EventListener/SitemapListenerTest.php @@ -0,0 +1,102 @@ + $this->mockConfiguredAdapter(['findByProtected' => null]), + ]; + + $sitemapEvent = $this->createSitemapEvent([]); + $listener = $this->createListener([], $adapters); + $listener($sitemapEvent); + + $this->assertStringNotContainsString('', (string) $sitemapEvent->getDocument()->saveXML()); + } + + public function testRecommendationIsAdded(): void + { + $jumpToPage = $this->mockClassWithProperties(PageModel::class, [ + 'published' => 1, + 'protected' => 0, + ]); + + $jumpToPage + ->method('getAbsoluteUrl') + ->willReturn('https://www.oveleon.de') + ; + + $adapters = [ + RecommendationArchiveModel::class => $this->mockConfiguredAdapter([ + 'findByProtected' => [ + $this->mockClassWithProperties(RecommendationArchiveModel::class, [ + 'jumpTo' => 21, + ]), + ], + ]), + PageModel::class => $this->mockConfiguredAdapter([ + 'findWithDetails' => $jumpToPage, + ]), + RecommendationModel::class => $this->mockConfiguredAdapter([ + 'findPublishedByPid' => [ + $this->mockClassWithProperties(RecommendationModel::class, [ + 'jumpTo' => 21, + ]), + ], + ]), + ]; + + $sitemapEvent = $this->createSitemapEvent([1]); + $listener = $this->createListener([1, 21], $adapters); + $listener($sitemapEvent); + + $this->assertStringContainsString('https://www.oveleon.de', (string) $sitemapEvent->getDocument()->saveXML()); + } + + private function createListener(array $allPages, array $adapters): SitemapListener + { + $database = $this->createMock(Database::class); + $database + ->method('getChildRecords') + ->willReturn($allPages) + ; + + $instances = [ + Database::class => $database, + ]; + + $framework = $this->mockContaoFramework($adapters, $instances); + + return new SitemapListener($framework); + } + + private function createSitemapEvent(array $rootPages): SitemapEvent + { + $sitemap = new \DOMDocument('1.0', 'UTF-8'); + $urlSet = $sitemap->createElementNS('https://www.sitemaps.org/schemas/sitemap/0.9', 'urlset'); + $sitemap->appendChild($urlSet); + + return new SitemapEvent($sitemap, new Request(), $rootPages); + } +}