diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..023a4fb --- /dev/null +++ b/composer.json @@ -0,0 +1,43 @@ +{ + "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":"^5.6 || ^7.0", + "contao/core-bundle":"^4.4" + }, + "require-dev": { + "contao/manager-plugin": "^2.0" + }, + "conflict": { + "contao/core": "*", + "contao/manager-plugin": "<2.0 || >=3.0" + }, + "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/" + ] + }, + "extra": { + "contao-manager-plugin": "Oveleon\\ContaoRecommendationBundle\\ContaoManager\\Plugin" + } +} diff --git a/src/ContaoManager/Plugin.php b/src/ContaoManager/Plugin.php new file mode 100644 index 0000000..ba93dbd --- /dev/null +++ b/src/ContaoManager/Plugin.php @@ -0,0 +1,37 @@ + + */ +class Plugin implements BundlePluginInterface +{ + /** + * {@inheritdoc} + */ + public function getBundles(ParserInterface $parser) + { + return [ + BundleConfig::create(ContaoRecommendationBundle::class) + ->setLoadAfter([ContaoCoreBundle::class]) + ->setReplace(['recommendation']), + ]; + } +} diff --git a/src/ContaoRecommendationBundle.php b/src/ContaoRecommendationBundle.php new file mode 100644 index 0000000..462578f --- /dev/null +++ b/src/ContaoRecommendationBundle.php @@ -0,0 +1,20 @@ + + */ +class ContaoRecommendationBundle extends Bundle +{ +} diff --git a/src/Resources/contao/config/config.php b/src/Resources/contao/config/config.php new file mode 100644 index 0000000..abcafdd --- /dev/null +++ b/src/Resources/contao/config/config.php @@ -0,0 +1,30 @@ + array + ( + 'tables' => array('tl_recommendation_archive', 'tl_recommendation') + ) +)); + +// Front end modules +array_insert($GLOBALS['FE_MOD'], 2, array +( + 'recommendation' => array + ( + 'recommendationlist' => '\\Oveleon\\ContaoRecommendationBundle\\ModuleRecommendationList', + 'recommendationreader' => '\\Oveleon\\ContaoRecommendationBundle\\ModuleRecommendationReader', + ) +)); + +// 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'; diff --git a/src/Resources/contao/dca/tl_module.php b/src/Resources/contao/dca/tl_module.php new file mode 100644 index 0000000..f5b84db --- /dev/null +++ b/src/Resources/contao/dca/tl_module.php @@ -0,0 +1,112 @@ + &$GLOBALS['TL_LANG']['tl_module']['recommendation_archives'], + 'exclude' => true, + 'inputType' => 'checkbox', + 'options_callback' => array('tl_module_recommendation', 'getRecommendationArchives'), + 'eval' => array('multiple'=>true, 'mandatory'=>true), + 'sql' => "blob NULL" +); + +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_featured'] = array +( + 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_featured'], + 'default' => 'all_items', + 'exclude' => true, + 'inputType' => 'select', + 'options' => array('all_items', 'featured', 'unfeatured'), + 'reference' => &$GLOBALS['TL_LANG']['tl_module'], + 'eval' => array('tl_class'=>'w50'), + 'sql' => "varchar(16) NOT NULL default ''" +); + +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_metaFields'] = array +( + 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_metaFields'], + 'default' => array('date', 'author'), + 'exclude' => true, + 'inputType' => 'checkbox', + 'options' => array('date', 'author', 'rating'), + 'reference' => &$GLOBALS['TL_LANG']['MSC'], + 'eval' => array('multiple'=>true), + 'sql' => "varchar(255) NOT NULL default ''" +); + +$GLOBALS['TL_DCA']['tl_module']['fields']['recommendation_template'] = array +( + 'label' => &$GLOBALS['TL_LANG']['tl_module']['recommendation_template'], + 'default' => 'recommendation_latest', + 'exclude' => true, + 'inputType' => 'select', + 'options_callback' => array('tl_module_recommendation', 'getRecommendationTemplates'), + 'eval' => array('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 Backend +{ + + /** + * Import the back end user object + */ + public function __construct() + { + parent::__construct(); + $this->import('BackendUser', 'User'); + } + + /** + * Get all recommendation archives and return them as array + * + * @return array + */ + public function getRecommendationArchives() + { + if (!$this->User->isAdmin && !\is_array($this->User->recommendations)) + { + return array(); + } + + $arrArchives = array(); + $objArchives = $this->Database->execute("SELECT id, title FROM tl_recommendation_archive ORDER BY title"); + + while ($objArchives->next()) + { + if ($this->User->hasAccess($objArchives->id, 'recommendations')) + { + $arrArchives[$objArchives->id] = $objArchives->title; + } + } + + return $arrArchives; + } + + /** + * Return all recommendation templates as array + * + * @return array + */ + public function getRecommendationTemplates() + { + return $this->getTemplateGroup('recommendation_'); + } +} diff --git a/src/Resources/contao/dca/tl_recommendation.php b/src/Resources/contao/dca/tl_recommendation.php new file mode 100644 index 0000000..a0dd8eb --- /dev/null +++ b/src/Resources/contao/dca/tl_recommendation.php @@ -0,0 +1,632 @@ + array + ( + 'dataContainer' => 'Table', + 'ptable' => 'tl_recommendation_archive', + 'switchToEdit' => true, + 'enableVersioning' => true, + 'onload_callback' => array + ( + array('tl_recommendation', 'checkPermission') + ), + 'onsubmit_callback' => array + ( + array('tl_recommendation', 'adjustTime') + ), + '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'] . '\'))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},title,alias,author;{date_legend},date,time;{recommendation_legend},rating,text,imageUrl;{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'), + '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 clr'), + 'save_callback' => array + ( + array('tl_recommendation', 'generateAlias') + ), + 'sql' => "varchar(128) COLLATE utf8_bin 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 ''" + ), + '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 ''" + ), + '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 ''" + ), + '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 Backend +{ + + /** + * Import the back end user object + */ + public function __construct() + { + parent::__construct(); + $this->import('BackendUser', 'User'); + } + + /** + * Check permissions to edit table tl_recommendation + * + * @throws Contao\CoreBundle\Exception\AccessDeniedException + */ + public function checkPermission() + { + if ($this->User->isAdmin) + { + return; + } + } + + /** + * Auto-generate the recommendation alias if it has not been set yet + * + * @param mixed $varValue + * @param DataContainer $dc + * + * @return string + * + * @throws Exception + */ + public function generateAlias($varValue, DataContainer $dc) + { + $autoAlias = false; + + // Generate alias if title is set + if ($varValue == '' && !empty($dc->activeRecord->title)) + { + $autoAlias = true; + $varValue = 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) + { + 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 DataContainer $dc + */ + public function adjustTime(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); + } + + /** + * 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(Input::get('fid'))) + { + $this->toggleFeatured(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 ''.Image::getHtml($icon, $label, 'data-state="' . ($row['featured'] ? 1 : 0) . '"').' '; + } + + /** + * Feature/unfeature a recommendation + * + * @param integer $intId + * @param boolean $blnVisible + * @param DataContainer $dc + * + * @throws Contao\CoreBundle\Exception\AccessDeniedException + */ + public function toggleFeatured($intId, $blnVisible, DataContainer $dc=null) + { + // Check permissions to edit + Input::setGet('id', $intId); + 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 Versions('tl_recommendation', $intId); + $objVersions->initialize(); + + // Trigger the save_callback + if (\is_array($GLOBALS['TL_DCA']['tl_recommendation']['fields']['featured']['save_callback'])) + { + 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(); + } + + /** + * 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(Input::get('tid'), (Input::get('state') == 1), (@func_get_arg(12) ?: null)); + $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 DataContainer $dc + */ + public function toggleVisibility($intId, $blnVisible, DataContainer $dc=null) + { + // Set the ID and action + Input::setGet('id', $intId); + Input::setGet('act', 'toggle'); + + if ($dc) + { + $dc->id = $intId; + } + + // Trigger the onload_callback + if (\is_array($GLOBALS['TL_DCA']['tl_recommendation']['config']['onload_callback'])) + { + 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 Versions('tl_recommendation', $intId); + $objVersions->initialize(); + + // Trigger the save_callback + if (\is_array($GLOBALS['TL_DCA']['tl_recommendation']['fields']['published']['save_callback'])) + { + 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'])) + { + 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(); + } +} diff --git a/src/Resources/contao/dca/tl_recommendation_archive.php b/src/Resources/contao/dca/tl_recommendation_archive.php new file mode 100644 index 0000000..43fec32 --- /dev/null +++ b/src/Resources/contao/dca/tl_recommendation_archive.php @@ -0,0 +1,359 @@ + array + ( + 'dataContainer' => 'Table', + 'ctable' => array('tl_recommendation'), + 'switchToEdit' => true, + 'enableVersioning' => true, + 'onload_callback' => array + ( + array('tl_recommendation_archive', 'checkPermission') + ), + '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 + ( + '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'] . '\'))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('mandatory'=>true, '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 Backend +{ + + /** + * Import the back end user object + */ + public function __construct() + { + parent::__construct(); + $this->import('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; + } + + /** @var Symfony\Component\HttpFoundation\Session\SessionInterface $objSession */ + $objSession = System::getContainer()->get('session'); + + // Check current action + switch (Input::get('act')) + { + case 'create': + case 'select': + // Allow + break; + + case 'edit': + // Dynamically add the record to the user profile + if (!\in_array(Input::get('id'), $root)) + { + /** @var Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface $objSessionBag */ + $objSessionBag = $objSession->getBag('contao_backend'); + + $arrNew = $objSessionBag->get('new_records'); + + if (\is_array($arrNew['tl_recommendation_archive']) && \in_array(Input::get('id'), $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 = StringUtil::deserialize($objGroup->recommendationp); + + if (\is_array($arrRecommendationp) && \in_array('create', $arrRecommendationp)) + { + $arrRecommendation = StringUtil::deserialize($objGroup->recommendations, true); + $arrRecommendation[] = Input::get('id'); + + $this->Database->prepare("UPDATE tl_user_group SET recommendations=? WHERE id=?") + ->execute(serialize($arrRecommendation), $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 = StringUtil::deserialize($objUser->recommendationp); + + if (\is_array($arrRecommendationp) && \in_array('create', $arrRecommendationp)) + { + $arrRecommendation = StringUtil::deserialize($objUser->recommendations, true); + $arrRecommendation[] = Input::get('id'); + + $this->Database->prepare("UPDATE tl_user SET recommendations=? WHERE id=?") + ->execute(serialize($arrRecommendation), $this->User->id); + } + } + + // Add the new element to the user object + $root[] = Input::get('id'); + $this->User->recommendations = $root; + } + } + // No break; + + case 'copy': + case 'delete': + case 'show': + if (!\in_array(Input::get('id'), $root) || (Input::get('act') == 'delete' && !$this->User->hasAccess('delete', 'recommendationp'))) + { + throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to ' . Input::get('act') . ' recommendation archive ID ' . Input::get('id') . '.'); + } + break; + + case 'editAll': + case 'deleteAll': + case 'overrideAll': + $session = $objSession->all(); + if (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(Input::get('act'))) + { + throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to ' . Input::get('act') . ' recommendation archives.'); + } + break; + } + } + + /** + * 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') ? ''.Image::getHtml($icon, $label).' ' : 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') ? ''.Image::getHtml($icon, $label).' ' : 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') ? ''.Image::getHtml($icon, $label).' ' : Image::getHtml(preg_replace('/\.svg$/i', '_.svg', $icon)).' '; + } +} diff --git a/src/Resources/contao/dca/tl_user.php b/src/Resources/contao/dca/tl_user.php new file mode 100644 index 0000000..5a09c16 --- /dev/null +++ b/src/Resources/contao/dca/tl_user.php @@ -0,0 +1,37 @@ +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/dca/tl_user_group.php b/src/Resources/contao/dca/tl_user_group.php new file mode 100644 index 0000000..1d900fb --- /dev/null +++ b/src/Resources/contao/dca/tl_user_group.php @@ -0,0 +1,36 @@ +addLegend('recommendation_legend', 'amg_legend', Contao\CoreBundle\DataContainer\PaletteManipulator::POSITION_BEFORE) + ->addField(array('recommendations', 'recommendationp'), 'recommendation_legend', Contao\CoreBundle\DataContainer\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'], + 'exclude' => true, + 'inputType' => 'checkbox', + 'foreignKey' => 'tl_recommendation_archive.title', + 'eval' => array('multiple'=>true), + 'sql' => "blob NULL" +); + +$GLOBALS['TL_DCA']['tl_user_group']['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/default.xlf b/src/Resources/contao/languages/de/default.xlf new file mode 100644 index 0000000..dc1c519 --- /dev/null +++ b/src/Resources/contao/languages/de/default.xlf @@ -0,0 +1,10 @@ + + + + + Rating + Bewertung + + + + diff --git a/src/Resources/contao/languages/de/modules.xlf b/src/Resources/contao/languages/de/modules.xlf new file mode 100644 index 0000000..fa884ba --- /dev/null +++ b/src/Resources/contao/languages/de/modules.xlf @@ -0,0 +1,34 @@ + + + + + Recommendations + Bewertungen + + + Manage recommendations + Bewertungen verwalten + + + Recommendations + Bewertungen + + + recommendationlist + Bewertungsliste + + + Adds a list of recommendations to the page + Fügt der Seite eine Bewertungsliste hinzu + + + Recommendationreader + Bewertungsleser + + + Shows the details of a recommendation + Stellt eine einzelne Bewertung dar + + + + diff --git a/src/Resources/contao/languages/de/tl_module.xlf b/src/Resources/contao/languages/de/tl_module.xlf new file mode 100644 index 0000000..b94253e --- /dev/null +++ b/src/Resources/contao/languages/de/tl_module.xlf @@ -0,0 +1,50 @@ + + + + + Recommendation archives + Bewertungsarchive + + + Please select one or more recommendation archives. + Bitte wählen Sie ein oder mehrere Bewertungsarchive. + + + Featured items + Hervorgehobene Beiträge + + + Here you can choose how featured items are handled. + Hier legen Sie fest, wie hervorgehobene Beiträge gehandhabt werden. + + + Meta fields + Meta-Felder + + + Here you can select the meta fields. + Hier können Sie die Meta-Felder auswählen. + + + recommendation template + Bewertungstemplate + + + Here you can select the recommendation template. + Hier können Sie das Bewertungstemplate auswählen. + + + Show all recommendations + Alle Bewertungen anzeigen + + + Show featured recommendation only + Nur hervorgehobene Bewertungen anzeigen + + + Skip featured recommendations + Hervorgehobene Bewertungen überspringen + + + + diff --git a/src/Resources/contao/languages/de/tl_recommendation.xlf b/src/Resources/contao/languages/de/tl_recommendation.xlf new file mode 100644 index 0000000..eb0e6dd --- /dev/null +++ b/src/Resources/contao/languages/de/tl_recommendation.xlf @@ -0,0 +1,230 @@ + + + + + Title + Titel + + + Please enter the recommendation title. + Bitte geben Sie den Titel der Bewertung ein. + + + Recommendation alias + Alias der Bewertung + + + The recommendation alias is a unique reference to the recommendation which can be called instead of its numeric ID. + Der Alias der Bewertung ist eine eindeutige Referenz, die anstelle der numerischen Bewertungs-ID aufgerufen werden kann. + + + Author + Autor + + + Here you can change the author of the recommendation. + Hier können Sie den Autor der Bewertung ändern. + + + Date + Datum + + + Please enter the date according to the global date format. + Bitte geben Sie das Datum gemäß des globalen Datumsformats ein. + + + Time + Uhrzeit + + + Please enter the time according to the global time format. + Bitte geben Sie die Uhrzeit gemäß des globalen Zeitformats ein. + + + Rating + Bewertung + + + Here you can enter a recommendation. + Hier können Sie die Bewertung eingeben. + + + Recommendation teaser + Teasertext + + + The recommendation teaser can be shown in a recommendation list instead of the full text. A "read more …" link will be added automatically. + Der Teasertext kann in einer Bewertungsliste anstatt des ganzen Beitrags angezeigt werden. Ein "Weiterlesen …"-Link wird automatisch hinzugefügt. + + + recommendation text + Bewertungstext + + + Here you can enter the recommendation text. + Hier können Sie den Bewertungstext eingeben. + + + Image + Bild + + + Here you can enter a recommendation image. + Hier können Sie der Bewertung ein Bild zuordnen. + + + CSS class + CSS-Klasse + + + Here you can enter one or more classes. + Hier können Sie eine oder mehrere Klassen eingeben. + + + Feature recommendation + Bewertung hervorheben + + + Show the recommendation in a featured recommendation list. + Die Bewertung in einer Liste hervorgehobener Bewertungen anzeigen. + + + Publish recommendation + Bewertung veröffentlichen + + + Make the recommendation publicly visible on the website. + Die Bewertung auf der Webseite anzeigen. + + + Show from + Anzeigen ab + + + Do not show the recommendation on the website before this day. + Die Bewertung erst ab diesem Tag auf der Webseite anzeigen. + + + Show until + Anzeigen bis + + + Do not show the recommendation on the website on and after this day. + Die Bewertung nur bis zu diesem Tag auf der Webseite anzeigen. + + + Revision date + Änderungsdatum + + + Date and time of the latest revision + Datum und Uhrzeit der letzten Änderung + + + Title and author + Titel und Autor + + + Date and time + Datum und Zeit + + + Recommendation + Bewertung + + + Teaser + Teaser + + + Expert settings + Experten-Einstellungen + + + Publish settings + Veröffentlichung + + + New recommendation + Neue Bewertung + + + Create a new recommendation + Eine neue Bewertung erstellen + + + Recommendation details + Bewertungsdetails + + + Show the details of recommendation ID %s + Die Details der Bewertung ID %s anzeigen + + + Edit recommendation + Bewertung bearbeiten + + + Edit recommendation ID %s + Bewertung ID %s bearbeiten + + + Duplicate recommendation + Bewertung duplizieren + + + Duplicate recommendation ID %s + Bewertung ID %s duplizieren + + + Move recommendation + Bewertung verschieben + + + Move recommendation ID %s + Bewertung ID %s verschieben + + + Delete recommendation + Bewertung löschen + + + Delete recommendation ID %s + Bewertung ID %s löschen + + + Publish/unpublish recommendation + Bewertung veröffentlichen/unveröffentlichen + + + Publish/unpublish recommendation ID %s + Bewertung ID %s veröffentlichen/unveröffentlichen + + + Feature/unfeature recommendation + Bewertung hervorheben/zurücksetzen + + + Feature/unfeature recommendation ID %s + Bewertung ID %s hervorheben/zurücksetzen + + + Edit recommendation settings + Bewertungseinstellungen bearbeiten + + + Edit the recommendation settings + Die Bewertungseinstellungen bearbeiten + + + Paste into this recommendation archive + In dieses Bewertungsarchiv einfügen + + + Paste after recommendation ID %s + Nach der Bewertung ID %s einfügen + + + + diff --git a/src/Resources/contao/languages/de/tl_recommendation_archive.xlf b/src/Resources/contao/languages/de/tl_recommendation_archive.xlf new file mode 100644 index 0000000..e09be72 --- /dev/null +++ b/src/Resources/contao/languages/de/tl_recommendation_archive.xlf @@ -0,0 +1,102 @@ + + + + + Title + Titel + + + Please enter a news archive title. + Bitte geben Sie den Archiv-Titel ein. + + + Redirect page + Weiterleitungsseite + + + Please choose the recommendation reader page to which visitors will be redirected when clicking a recommendation. + Bitte wählen Sie die Bewertungsleser-Seite aus, zu der Besucher weitergeleitet werden, wenn Sie eine Bewertung anklicken. + + + Protect archive + Archiv schützen + + + Show recommendations to certain member groups only. + Bewertungen nur bestimmten Frontend-Gruppen anzeigen. + + + Allowed member groups + Erlaubte Mitgliedergruppen + + + These groups will be able to see the recommendations in this archive. + Diese Mitgliedergruppen können die Bewertungen des Archivs sehen. + + + Revision date + Änderungsdatum + + + Date and time of the latest revision + Datum und Uhrzeit der letzten Änderung + + + Title and redirect page + Titel und Weiterleitung + + + Access protection + Zugriffsschutz + + + New archive + Neues Archiv + + + Create a new archive + Ein neues Archiv erstellen + + + Archive details + Archivdetails + + + Show the details of archive ID %s + Die Details des Archivs ID %s anzeigen + + + Edit archive + Archiv bearbeiten + + + Edit archive ID %s + Archiv ID %s bearbeiten + + + Edit archive + Archiv bearbeiten + + + Edit the archive settings + Die Archiv-Einstellungen bearbeiten + + + Duplicate archive + Archiv duplizieren + + + Duplicate archive ID %s + Archiv ID %s duplizieren + + + Delete archive + Archiv löschen + + + Delete archive ID %s + Archiv ID %s löschen + + + + diff --git a/src/Resources/contao/languages/de/tl_user.xlf b/src/Resources/contao/languages/de/tl_user.xlf new file mode 100644 index 0000000..e0bbd9b --- /dev/null +++ b/src/Resources/contao/languages/de/tl_user.xlf @@ -0,0 +1,26 @@ + + + + + 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. + + + Recommendation permissions + Bewertungsrechte + + + + diff --git a/src/Resources/contao/languages/de/tl_user_group.xlf b/src/Resources/contao/languages/de/tl_user_group.xlf new file mode 100644 index 0000000..a2d719b --- /dev/null +++ b/src/Resources/contao/languages/de/tl_user_group.xlf @@ -0,0 +1,10 @@ + + + + + Recommendation permissions + Bewertungsrechte + + + + diff --git a/src/Resources/contao/models/RecommendationArchiveModel.php b/src/Resources/contao/models/RecommendationArchiveModel.php new file mode 100644 index 0000000..7c9bdc6 --- /dev/null +++ b/src/Resources/contao/models/RecommendationArchiveModel.php @@ -0,0 +1,58 @@ + + */ +class RecommendationArchiveModel extends \Model +{ + + /** + * Table name + * @var string + */ + protected static $strTable = 'tl_recommendation_archive'; + +} diff --git a/src/Resources/contao/models/RecommendationModel.php b/src/Resources/contao/models/RecommendationModel.php new file mode 100644 index 0000000..786fc9d --- /dev/null +++ b/src/Resources/contao/models/RecommendationModel.php @@ -0,0 +1,206 @@ + + */ +class RecommendationModel extends \Model +{ + + /** + * Table name + * @var string + */ + protected static $strTable = 'tl_recommendation'; + + /** + * 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()) + { + if (empty($arrPids) || !\is_array($arrPids)) + { + return null; + } + + $t = static::$strTable; + $arrColumns = !is_numeric($varId) ? array("$t.alias=?") : array("$t.id=?"); + $arrColumns[] = "$t.pid IN(" . implode(',', array_map('\intval', $arrPids)) . ")"; + + if (!static::isPreviewMode($arrOptions)) + { + $time = \Date::floorToMinute(); + $arrColumns[] = "($t.start='' OR $t.start<='$time') AND ($t.stop='' OR $t.stop>'" . ($time + 60) . "') AND $t.published='1'"; + } + + return static::findOneBy($arrColumns, $varId, $arrOptions); + } + + /** + * 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 }Model\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, array $arrOptions=array()) + { + if (empty($arrPids) || !\is_array($arrPids)) + { + return null; + } + + $t = static::$strTable; + $arrColumns = array("$t.pid IN(" . implode(',', array_map('\intval', $arrPids)) . ")"); + + if ($blnFeatured === true) + { + $arrColumns[] = "$t.featured='1'"; + } + elseif ($blnFeatured === false) + { + $arrColumns[] = "$t.featured=''"; + } + + if (!static::isPreviewMode($arrOptions)) + { + $time = \Date::floorToMinute(); + $arrColumns[] = "($t.start='' OR $t.start<='$time') AND ($t.stop='' OR $t.stop>'" . ($time + 60) . "') AND $t.published='1'"; + } + + if (!isset($arrOptions['order'])) + { + $arrOptions['order'] = "$t.date DESC"; + } + + $arrOptions['limit'] = $intLimit; + $arrOptions['offset'] = $intOffset; + + return static::findBy($arrColumns, null, $arrOptions); + } + + /** + * 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, array $arrOptions=array()) + { + if (empty($arrPids) || !\is_array($arrPids)) + { + return 0; + } + + $t = static::$strTable; + $arrColumns = array("$t.pid IN(" . implode(',', array_map('\intval', $arrPids)) . ")"); + + if ($blnFeatured === true) + { + $arrColumns[] = "$t.featured='1'"; + } + elseif ($blnFeatured === false) + { + $arrColumns[] = "$t.featured=''"; + } + + if (!static::isPreviewMode($arrOptions)) + { + $time = \Date::floorToMinute(); + $arrColumns[] = "($t.start='' OR $t.start<='$time') AND ($t.stop='' OR $t.stop>'" . ($time + 60) . "') AND $t.published='1'"; + } + + return static::countBy($arrColumns, null, $arrOptions); + } +} diff --git a/src/Resources/contao/modules/ModuleRecommendation.php b/src/Resources/contao/modules/ModuleRecommendation.php new file mode 100644 index 0000000..750a3aa --- /dev/null +++ b/src/Resources/contao/modules/ModuleRecommendation.php @@ -0,0 +1,292 @@ + + */ +abstract class ModuleRecommendation extends \Module +{ + + /** + * Sort out protected archives + * + * @param array $arrArchives + * + * @return array + */ + protected function sortOutProtected($arrArchives) + { + if (empty($arrArchives) || !\is_array($arrArchives)) + { + return $arrArchives; + } + + $this->import('FrontendUser', 'User'); + $objArchive = RecommendationArchiveModel::findMultipleByIds($arrArchives); + $arrArchives = array(); + + if ($objArchive !== null) + { + while ($objArchive->next()) + { + if ($objArchive->protected) + { + 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; + } + } + + $arrArchives[] = $objArchive->id; + } + } + + return $arrArchives; + } + + /** + * Parse an item and return it as string + * + * @param RecommendationModel $objRecommendation + * @param string $strClass + * @param integer $intCount + * + * @return string + */ + protected function parseRecommendation($objRecommendation, $strClass='', $intCount=0) + { + /** @var \FrontendTemplate|object $objTemplate */ + $objTemplate = new \FrontendTemplate($this->recommendation_template); + $objTemplate->setData($objRecommendation->row()); + + if ($objRecommendation->cssClass != '') + { + $strClass = ' ' . $objRecommendation->cssClass . $strClass; + } + + if ($objRecommendation->featured) + { + $strClass = ' featured' . $strClass; + } + + $objTemplate->class = $strClass; + + if ($objRecommendation->title) + { + $objTemplate->hasTitle = true; + $objTemplate->linkHeadline = $this->generateLink($objRecommendation->title, $objRecommendation, $objRecommendation->title); + $objTemplate->headline = $objRecommendation->title; + } + + $objTemplate->more = $this->generateLink($GLOBALS['TL_LANG']['MSC']['more'], $objRecommendation, $strTitle, true); + $objTemplate->archiveId = $objRecommendation->pid; + + $arrMeta = $this->getMetaFields($objRecommendation); + + // Add the meta information + $objTemplate->addRating = array_key_exists('rating', $arrMeta); + $objTemplate->addDate = array_key_exists('date', $arrMeta); + $objTemplate->date = $arrMeta['date']; + $objTemplate->addAuthor = array_key_exists('author', $arrMeta); + $objTemplate->author = $arrMeta['author']; + $objTemplate->datetime = date('Y-m-d\TH:i:sP', $objRecommendation->date); + + $objTemplate->addExternalImage = false; + $objTemplate->addInternalImage = false; + + // Add an image + if ($objRecommendation->imageUrl != '') + { + $objRecommendation->imageUrl = \Controller::replaceInsertTags($objRecommendation->imageUrl); + + if ($this->isExternal($objRecommendation->imageUrl)) + { + $objTemplate->addExternalImage = true; + + $objTemplate->imageUrl = $objRecommendation->imageUrl; + } + else + { + $objModel = \FilesModel::findByPath($objRecommendation->imageUrl); + + if ($objModel !== null && is_file(TL_ROOT . '/' . $objModel->path)) + { + $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 != '') + { + $size = \StringUtil::deserialize($this->imgSize); + + if ($size[0] > 0 || $size[1] > 0 || is_numeric($size[2])) + { + $arrRecommendation['size'] = $this->imgSize; + } + } + + $arrRecommendation['singleSRC'] = $objModel->path; + $this->addImageToTemplate($objTemplate, $arrRecommendation, null, null, $objModel); + } + } + } + + // HOOK: add custom logic + if (isset($GLOBALS['TL_HOOKS']['parseRecommendation']) && \is_array($GLOBALS['TL_HOOKS']['parseRecommendation'])) + { + foreach ($GLOBALS['TL_HOOKS']['parseRecommendation'] as $callback) + { + $this->import($callback[0]); + $this->{$callback[0]}->{$callback[1]}($objTemplate, $objRecommendation->row(), $this); + } + } + + return $objTemplate->parse(); + } + + /** + * Parse one or more items and return them as array + * + * @param \Model\Collection $objRecommendations + * + * @return array + */ + protected function parseRecommendations($objRecommendations) + { + $limit = $objRecommendations->count(); + + if ($limit < 1) + { + return array(); + } + + $count = 0; + $arrRecommendations = array(); + + while ($objRecommendations->next()) + { + /** @var RecommendationModel $objRecommendation */ + $objRecommendation = $objRecommendations->current(); + + $arrRecommendations[] = $this->parseRecommendation($objRecommendation, ((++$count == 1) ? ' first' : '') . (($count == $limit) ? ' last' : '') . ((($count % 2) == 0) ? ' odd' : ' even'), $count); + } + + return $arrRecommendations; + } + + /** + * Return the meta fields of a recommendation as array + * + * @param RecommendationModel $objRecommendation + * + * @return array + */ + protected function getMetaFields($objRecommendation) + { + $meta = \StringUtil::deserialize($this->recommendation_metaFields); + + if (!\is_array($meta)) + { + return array(); + } + + /** @var \PageModel $objPage */ + global $objPage; + + $return = array(); + + foreach ($meta as $field) + { + switch ($field) + { + case 'date': + $return['date'] = \Date::parse($objPage->datimFormat, $objRecommendation->date); + break; + + case 'author': + $return['author'] = $objRecommendation->author; + break; + + case 'rating': + $return['rating'] = $objRecommendation->rating; + break; + } + } + + return $return; + } + + /** + * 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) + { + return sprintf('', + $this->generateRecommendationUrl($objRecommendation), + \StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['readMore'], $strTitle), true), + $strLink, + ($blnIsReadMore ? '' : '')); + } + + /** + * Generate a URL and return it as string + * + * @param RecommendationModel $objRecommendation + * + * @return string + */ + protected function generateRecommendationUrl($objRecommendation) + { + $objPage = \PageModel::findByPk($objRecommendation->getRelated('pid')->jumpTo); + + return 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) + { + if (substr($strPath, 0, 7) == 'http://' || substr($strPath, 0, 8) == 'https://') + { + return true; + } + + return false; + } +} diff --git a/src/Resources/contao/modules/ModuleRecommendationList.php b/src/Resources/contao/modules/ModuleRecommendationList.php new file mode 100644 index 0000000..a5131c3 --- /dev/null +++ b/src/Resources/contao/modules/ModuleRecommendationList.php @@ -0,0 +1,209 @@ + + */ +class ModuleRecommendationList extends ModuleRecommendation +{ + + /** + * Template + * @var string + */ + protected $strTemplate = 'mod_recommendationlist'; + + /** + * Display a wildcard in the back end + * + * @return string + */ + public function generate() + { + if (TL_MODE == 'BE') + { + /** @var \BackendTemplate|object $objTemplate */ + $objTemplate = new \BackendTemplate('be_wildcard'); + + $objTemplate->wildcard = '### ' . Utf8::strtoupper($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; + + return $objTemplate->parse(); + } + + $this->recommendation_archives = $this->sortOutProtected(\StringUtil::deserialize($this->recommendation_archives)); + + // Return if there are no archives + if (empty($this->recommendation_archives) || !\is_array($this->recommendation_archives)) + { + return ''; + } + + return parent::generate(); + } + + /** + * Generate the module + */ + protected function compile() + { + $limit = null; + $offset = (int) $this->skipFirst; + + // Maximum number of items + if ($this->numberOfItems > 0) + { + $limit = $this->numberOfItems; + } + + // Handle featured recommendations + if ($this->recommendation_featured == 'featured') + { + $blnFeatured = true; + } + elseif ($this->recommendation_featured == 'unfeatured') + { + $blnFeatured = false; + } + else + { + $blnFeatured = null; + } + + $this->Template->recommendations = array(); + $this->Template->empty = $GLOBALS['TL_LANG']['MSC']['emptyList']; + + // Get the total number of items + $intTotal = $this->countItems($this->recommendation_archives, $blnFeatured); + + if ($intTotal < 1) + { + return; + } + + $total = $intTotal - $offset; + + // Split the results + if ($this->perPage > 0 && (!isset($limit) || $this->numberOfItems > $this->perPage)) + { + // Adjust the overall limit + if (isset($limit)) + { + $total = min($limit, $total); + } + + // Get the current page + $id = 'page_n' . $this->id; + $page = (\Input::get($id) !== null) ? \Input::get($id) : 1; + + // Do not index or cache the page if the page number is outside the range + if ($page < 1 || $page > max(ceil($total/$this->perPage), 1)) + { + throw new PageNotFoundException('Page not found: ' . \Environment::get('uri')); + } + + // Set limit and offset + $limit = $this->perPage; + $offset += (max($page, 1) - 1) * $this->perPage; + $skip = (int) $this->skipFirst; + + // Overall limit + if ($offset + $limit > $total + $skip) + { + $limit = $total + $skip - $offset; + } + + // Add the pagination menu + $objPagination = new \Pagination($total, $this->perPage, \Config::get('maxPaginationLinks'), $id); + $this->Template->pagination = $objPagination->generate("\n "); + } + + $objRecommendations = $this->fetchItems($this->recommendation_archives, $blnFeatured, ($limit ?: 0), $offset); + + // Add recommendations + if ($objRecommendations !== null) + { + $this->Template->recommendations = $this->parseRecommendations($objRecommendations); + } + } + + /** + * Count the total matching items + * + * @param array $recommendationArchives + * @param boolean $blnFeatured + * + * @return integer + */ + protected function countItems($recommendationArchives, $blnFeatured) + { + // HOOK: add custom logic + if (isset($GLOBALS['TL_HOOKS']['recommendationListCountItems']) && \is_array($GLOBALS['TL_HOOKS']['recommendationListCountItems'])) + { + foreach ($GLOBALS['TL_HOOKS']['recommendationListCountItems'] as $callback) + { + if (($intResult = \System::importStatic($callback[0])->{$callback[1]}($recommendationArchives, $blnFeatured, $this)) === false) + { + continue; + } + + if (\is_int($intResult)) + { + return $intResult; + } + } + } + + return RecommendationModel::countPublishedByPids($recommendationArchives, $blnFeatured); + } + + /** + * Fetch the matching items + * + * @param array $recommendationArchives + * @param boolean $blnFeatured + * @param integer $limit + * @param integer $offset + * + * @return \Model\Collection|RecommendationModel|null + */ + protected function fetchItems($recommendationArchives, $blnFeatured, $limit, $offset) + { + // HOOK: add custom logic + if (isset($GLOBALS['TL_HOOKS']['recommendationListFetchItems']) && \is_array($GLOBALS['TL_HOOKS']['recommendationListFetchItems'])) + { + foreach ($GLOBALS['TL_HOOKS']['recommendationListFetchItems'] as $callback) + { + if (($objCollection = \System::importStatic($callback[0])->{$callback[1]}($recommendationArchives, $blnFeatured, $limit, $offset, $this)) === false) + { + continue; + } + + if ($objCollection === null || $objCollection instanceof \Model\Collection) + { + return $objCollection; + } + } + } + + return RecommendationModel::findPublishedByPids($recommendationArchives, $blnFeatured, $limit, $offset); + } +} diff --git a/src/Resources/contao/modules/ModuleRecommendationReader.php b/src/Resources/contao/modules/ModuleRecommendationReader.php new file mode 100644 index 0000000..9417481 --- /dev/null +++ b/src/Resources/contao/modules/ModuleRecommendationReader.php @@ -0,0 +1,94 @@ + + */ +class ModuleRecommendationReader extends ModuleRecommendation +{ + + /** + * Template + * @var string + */ + protected $strTemplate = 'mod_recommendationreader'; + + /** + * Display a wildcard in the back end + * + * @return string + */ + public function generate() + { + if (TL_MODE == 'BE') + { + /** @var \BackendTemplate|object $objTemplate */ + $objTemplate = new \BackendTemplate('be_wildcard'); + + $objTemplate->wildcard = '### ' . Utf8::strtoupper($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; + + return $objTemplate->parse(); + } + + // Set the item from the auto_item parameter + if (!isset($_GET['items']) && \Config::get('useAutoItem') && isset($_GET['auto_item'])) + { + \Input::setGet('items', \Input::get('auto_item')); + } + + $this->recommendation_archives = $this->sortOutProtected(\StringUtil::deserialize($this->recommendation_archives)); + + // Do not index or cache the page if no recommendation item has been specified + if (!\Input::get('items') || empty($this->recommendation_archives) || !\is_array($this->recommendation_archives)) + { + /** @var \PageModel $objPage */ + global $objPage; + + $objPage->noSearch = 1; + $objPage->cache = 0; + + return ''; + } + + return parent::generate(); + } + + /** + * Generate the module + */ + protected function compile() + { + $this->Template->recommendation = ''; + $this->Template->referer = 'javascript:history.go(-1)'; + $this->Template->back = $GLOBALS['TL_LANG']['MSC']['goBack']; + + // Get the news item + $objRecommendation = RecommendationModel::findPublishedByParentAndIdOrAlias(\Input::get('items'), $this->recommendation_archives); + + if (null === $objRecommendation) + { + throw new PageNotFoundException('Page not found: ' . \Environment::get('uri')); + } + + $arrRecommendation = $this->parseRecommendation($objRecommendation); + $this->Template->recommendation = $arrRecommendation; + } +} diff --git a/src/Resources/contao/templates/modules/mod_recommendationlist.html5 b/src/Resources/contao/templates/modules/mod_recommendationlist.html5 new file mode 100644 index 0000000..aea7d65 --- /dev/null +++ b/src/Resources/contao/templates/modules/mod_recommendationlist.html5 @@ -0,0 +1,12 @@ +extend('block_unsearchable'); ?> + +block('content'); ?> + + recommendations)): ?> +

empty ?>

+ + recommendations) ?> + pagination ?> + + +endblock(); ?> diff --git a/src/Resources/contao/templates/modules/mod_recommendationreader.html5 b/src/Resources/contao/templates/modules/mod_recommendationreader.html5 new file mode 100644 index 0000000..2627cf2 --- /dev/null +++ b/src/Resources/contao/templates/modules/mod_recommendationreader.html5 @@ -0,0 +1,11 @@ +extend('block_searchable'); ?> + +block('content'); ?> + + recommendation ?> + + +

back ?>

+ + +endblock(); ?> diff --git a/src/Resources/contao/templates/recommendation/recommendation_full.html5 b/src/Resources/contao/templates/recommendation/recommendation_full.html5 new file mode 100644 index 0000000..330ce68 --- /dev/null +++ b/src/Resources/contao/templates/recommendation/recommendation_full.html5 @@ -0,0 +1,40 @@ + +
+ + hasTitle): ?> +

headline ?>

+ + + addInternalImage): ?> + insert('image', $this->arrData); ?> + addExternalImage): ?> +
+ +
+ + + addAuthor || $this->addDate || $this->addRating): ?> +
+ addAuthor): ?> +

author ?>

+ + + addDate): ?> +

+ + + addRating): ?> +
+ + + +
+ +
+ + +
+ text ?> +
+ +
diff --git a/src/Resources/contao/templates/recommendation/recommendation_latest.html5 b/src/Resources/contao/templates/recommendation/recommendation_latest.html5 new file mode 100644 index 0000000..9c8be38 --- /dev/null +++ b/src/Resources/contao/templates/recommendation/recommendation_latest.html5 @@ -0,0 +1,48 @@ + +
+ + addInternalImage): ?> + insert('image', $this->arrData); ?> + addExternalImage): ?> +
+ +
+ + +
+ hasTitle): ?> +

linkHeadline ?>

+ + + addAuthor || $this->addDate || $this->addRating): ?> +
+ addAuthor): ?> +

author ?>

+ + + addDate): ?> +

+ + + addRating): ?> +
+ + + +
+ +
+ + +
+ teaser): ?> + teaser ?> + + text ?> + +
+ +

more ?>

+
+ +
diff --git a/src/Resources/contao/templates/recommendation/recommendation_simple.html5 b/src/Resources/contao/templates/recommendation/recommendation_simple.html5 new file mode 100644 index 0000000..ad9dadf --- /dev/null +++ b/src/Resources/contao/templates/recommendation/recommendation_simple.html5 @@ -0,0 +1,46 @@ + +
+ + addInternalImage): ?> + insert('image', $this->arrData); ?> + addExternalImage): ?> +
+ +
+ + +
+ hasTitle): ?> +

headline ?>

+ + + addAuthor || $this->addDate || $this->addRating): ?> +
+ addAuthor): ?> +

author ?>

+ + + addDate): ?> +

+ + + addRating): ?> +
+ + + +
+ +
+ + +
+ teaser): ?> + teaser ?> + + text ?> + +
+
+ +