Initial commit - Typo3 11.5.41

This commit is contained in:
Matteo Gallo
2026-07-03 17:53:31 +02:00
commit 5ca4743197
6811 changed files with 568848 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Backend\FormDataProvider;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace GeorgRinger\News\Backend\FieldInformation;
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Core\Localization\LanguageService;
class StaticText extends AbstractNode
{
/**
* Handler for single nodes
*
* @return array
*/
public function render(): array
{
$languageService = $this->getLanguageService();
$labels = [];
foreach ((array)($this->data['renderData']['fieldInformationOptions']['labels'] ?? []) as $labelConfiguration) {
$label = htmlspecialchars($languageService->sL($labelConfiguration['label']));
if (!empty($labelConfiguration['italic'])) {
$label = '<em>' . $label . '</em>';
}
if (!empty($labelConfiguration['bold'])) {
$label = '<strong>' . $label . '</strong>';
}
$labels[] = $label;
}
return [
'requireJsModules' => [
'TYPO3/CMS/News/TagSuggestWizard',
],
'html' => '<div class="form-control-wrap news-taggable">'
. implode('<br />', $labels)
. '</div>',
];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}

View File

@@ -0,0 +1,282 @@
<?php
namespace GeorgRinger\News\Backend\FormDataProvider;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fill the news records with default values
*/
class NewsFlexFormManipulation implements FormDataProviderInterface
{
/**
* Fields which are removed in detail view
*
* @var array
*/
protected $removedFieldsInDetailView = [
'sDEF' => [
'orderBy', 'orderDirection', 'categories', 'categoryConjunction', 'includeSubCategories',
'archiveRestriction', 'timeRestriction', 'timeRestrictionHigh', 'topNewsRestriction',
'dateField'
],
'additional' => [
'limit', 'offset', 'hidePagination', 'topNewsFirst', 'listPid', 'list.paginate.itemsPerPage'
],
'template' => [ 'cropMaxCharacters' ],
];
/**
* Fields which are removed in list view
*
* @var array
*/
protected $removedFieldsInListView = [
'sDEF' => [
'dateField', 'singleNews', 'previewHiddenRecords'
],
'additional' => [],
'template' => [],
];
/**
* Fields which are removed in dateMenu view
*
* @var array
*/
protected $removedFieldsInDateMenuView = [
'sDEF' => [
'orderBy', 'singleNews'
],
'additional' => [
'limit', 'offset', 'hidePagination', 'topNewsFirst' , 'backPid', 'previewHiddenRecords', 'excludeAlreadyDisplayedNews',
'list.paginate.itemsPerPage'
],
'template' => [
'cropMaxCharacters', 'media.maxWidth', 'media.maxHeight'
],
];
/**
* Fields which are removed in search form view
*
* @var array
*/
protected $removedFieldsInSearchFormView = [
'sDEF' => [
'orderBy', 'orderDirection', 'categories', 'categoryConjunction', 'includeSubCategories',
'archiveRestriction', 'timeRestriction', 'timeRestrictionHigh', 'topNewsRestriction',
'startingpoint', 'recursive', 'dateField', 'singleNews', 'previewHiddenRecords'
],
'additional' => [
'limit', 'offset', 'hidePagination', 'topNewsFirst', 'detailPid', 'backPid', 'excludeAlreadyDisplayedNews',
'list.paginate.itemsPerPage'
],
'template' => [
'cropMaxCharacters', 'media.maxWidth', 'media.maxHeight'
],
];
/**
* Fields which are removed in category list view
*
* @var array
*/
protected $removedFieldsInCategoryListView = [
'sDEF' => [
'orderBy', 'orderDirection', 'categoryConjunction', 'includeSubCategories',
'archiveRestriction', 'timeRestriction', 'timeRestrictionHigh', 'topNewsRestriction',
'recursive', 'dateField', 'singleNews', 'previewHiddenRecords',
],
'additional' => [
'limit', 'offset', 'hidePagination', 'topNewsFirst', 'detailPid', 'backPid', 'excludeAlreadyDisplayedNews',
'list.paginate.itemsPerPage'
],
'template' => [
'cropMaxCharacters', 'media.maxWidth', 'media.maxHeight'
],
];
/**
* Fields which are removed in tag list view
*
* @var array
*/
protected $removedFieldsInTagListView = [
'sDEF' => [
'categories', 'categoryConjunction', 'includeSubCategories',
'archiveRestriction', 'timeRestriction', 'timeRestrictionHigh', 'topNewsRestriction',
'dateField', 'singleNews', 'previewHiddenRecords'
],
'additional' => [
'hidePagination', 'topNewsFirst', 'detailPid', 'backPid', 'excludeAlreadyDisplayedNews',
'list.paginate.itemsPerPage'
],
'template' => [
'cropMaxCharacters', 'media.maxWidth', 'media.maxHeight'
]
];
/**
* @var EmConfiguration
*/
protected $configuration;
public function __construct()
{
$this->configuration = GeneralUtility::makeInstance(EmConfiguration::class);
}
/**
* Remove fields depending on switchable controller action in tt_content
* Restrict category selection based on configuration in tt_content
*
* @param array $result
* @return array
*/
public function addData(array $result): array
{
if ($result['tableName'] === 'tt_content'
&& $result['databaseRow']['CType'] === 'list'
&& $result['databaseRow']['list_type'] === 'news_pi1'
&& is_array($result['processedTca']['columns']['pi_flexform']['config']['ds'])
) {
$result = $this->updateFlexForms($result);
if ($this->enabledInTsConfig($result)) {
$result = $this->addCategoryConstraints($result);
}
}
return $result;
}
/**
* Update flexform configuration if a action is selected
*
* @param array $result Full data
* @return array Modified data
*/
protected function updateFlexForms(array $result): array
{
$selectedView = '';
$row = $result['databaseRow'];
$dataStructure = $result['processedTca']['columns']['pi_flexform']['config']['ds'];
// get the first selected action
$flexformSelection = $row['pi_flexform'];
if (is_array($flexformSelection)
&& is_array($flexformSelection['data'])
&& !empty($flexformSelection['data']['sDEF']['lDEF']['switchableControllerActions']['vDEF'])
) {
$selectedView = $flexformSelection['data']['sDEF']['lDEF']['switchableControllerActions']['vDEF'];
$actionParts = GeneralUtility::trimExplode(';', $selectedView, true);
$selectedView = $actionParts[0];
} elseif ($result['command'] === 'new') {
// new plugin element, use List as starting view
$selectedView = 'News->list';
}
if (!empty($selectedView)) {
// Modify the flexform structure depending on the first found action
switch ($selectedView) {
case 'News->list':
case 'News->searchResult':
$dataStructure = $this->deleteFromStructure($dataStructure, $this->removedFieldsInListView);
break;
case 'News->detail':
$dataStructure = $this->deleteFromStructure($dataStructure, $this->removedFieldsInDetailView);
break;
case 'News->searchForm':
$dataStructure = $this->deleteFromStructure($dataStructure, $this->removedFieldsInSearchFormView);
break;
case 'News->dateMenu':
$dataStructure = $this->deleteFromStructure($dataStructure, $this->removedFieldsInDateMenuView);
break;
case 'Category->list':
$dataStructure = $this->deleteFromStructure($dataStructure, $this->removedFieldsInCategoryListView);
break;
case 'Tag->list':
$dataStructure = $this->deleteFromStructure($dataStructure, $this->removedFieldsInTagListView);
break;
default:
}
$params = [
'selectedView' => $selectedView,
'dataStructure' => &$dataStructure,
];
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['Hooks/BackendUtility.php']['updateFlexforms'] ?? [] as $reference) {
GeneralUtility::callUserFunction($reference, $params, $this);
}
}
$result['processedTca']['columns']['pi_flexform']['config']['ds'] = $dataStructure;
return $result;
}
/**
* Add category restriction to flexforms
*
* @param array $result
* @return array Modified result
*/
protected function addCategoryConstraints($result): array
{
$structure = $result['processedTca']['columns']['pi_flexform']['config']['ds'];
$categoryRestrictionSetting = $this->configuration->getCategoryRestriction();
$categoryRestriction = '';
switch ($categoryRestrictionSetting) {
case 'current_pid':
$categoryRestriction = ' AND sys_category.pid=###CURRENT_PID### ';
break;
case 'siteroot':
$categoryRestriction = ' AND sys_category.pid IN (###SITEROOT###) ';
break;
case 'page_tsconfig':
$categoryRestriction = ' AND sys_category.pid IN (###PAGE_TSCONFIG_IDLIST###) ';
break;
}
if (!empty($categoryRestriction) && isset($structure['sheets']['sDEF']['ROOT']['el']['settings.categories'])) {
$structure['sheets']['sDEF']['ROOT']['el']['settings.categories']['config']['foreign_table_where'] = $categoryRestriction . $structure['sheets']['sDEF']['ROOT']['el']['settings.categories']['config']['foreign_table_where'];
}
$result['processedTca']['columns']['pi_flexform']['config']['ds'] = $structure;
return $result;
}
/**
* Remove fields from flexform structure
*
* @param array &$dataStructure flexform structure
* @param array $fieldsToBeRemoved fields which need to be removed
* @return array Modified structure
*/
protected function deleteFromStructure(array $dataStructure, array $fieldsToBeRemoved): array
{
foreach ($fieldsToBeRemoved as $sheetName => $fieldsInSheet) {
foreach ($fieldsInSheet as $fieldName) {
unset($dataStructure['sheets'][$sheetName]['ROOT']['el']['settings.' . $fieldName]);
}
}
return $dataStructure;
}
/**
* @param array $result Incoming array
* @return bool
*/
protected function enabledInTsConfig(array $result): bool
{
return (bool)($result['pageTsConfig']['tx_news.']['categoryRestrictionForFlexForms'] ?? false);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace GeorgRinger\News\Backend\FormDataProvider;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fill the news records with default values
*/
class NewsRowInitializeNew implements FormDataProviderInterface
{
/** @var EmConfiguration */
protected $emConfiguration;
public function __construct()
{
$this->emConfiguration = GeneralUtility::makeInstance(EmConfiguration::class);
}
/**
* @param array $result
* @return array
*/
public function addData(array $result): array
{
if ($result['tableName'] !== 'tx_news_domain_model_news') {
return $result;
}
$result = $this->setTagListingId($result);
if ($result['command'] === 'new') {
$result = $this->fillDateField($result);
}
return $result;
}
/**
* @param array $result
* @return array
*/
protected function fillDateField(array $result): array
{
if ($this->emConfiguration->getDateTimeRequired()) {
$result['databaseRow']['datetime'] = $GLOBALS['EXEC_TIME'];
}
if (isset($result['pageTsConfig']['tx_news.']['predefine.'])
&& is_array($result['pageTsConfig']['tx_news.']['predefine.'])
) {
if (isset($result['pageTsConfig']['tx_news.']['predefine.']['author']) && (int)$result['pageTsConfig']['tx_news.']['predefine.']['author'] === 1) {
$result['databaseRow']['author'] = $GLOBALS['BE_USER']->user['realName'];
$result['databaseRow']['author_email'] = $GLOBALS['BE_USER']->user['email'];
}
if (isset($result['pageTsConfig']['tx_news.']['predefine.']['archive'])) {
$calculatedTime = strtotime($result['pageTsConfig']['tx_news.']['predefine.']['archive']);
if ($calculatedTime !== false) {
$result['databaseRow']['archive'] = $calculatedTime;
}
}
}
return $result;
}
/**
* @param array $result
* @return array
*/
protected function setTagListingId(array $result): array
{
if (!isset($result['pageTsConfig']['tx_news.']['tagPid'])) {
return $result;
}
$tagPid = (int)$result['pageTsConfig']['tx_news.']['tagPid'];
if ($tagPid <= 0) {
return $result;
}
$result['processedTca']['columns']['tags']['config']['fieldControl']['listModule']['options']['pid'] = $tagPid;
return $result;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace GeorgRinger\News\Backend\RecordList;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
/**
* Class for the list rendering of administration module
*/
class NewsDatabaseRecordList extends DatabaseRecordList
{
/**
* Creates the URL to this script, including all relevant GPvars
* Fixed GPvars are id, table, imagemode, returnUrl, search_field, search_levels and showLimit
* The GPvars "sortField" and "sortRev" are also included UNLESS they are found in the $excludeList variable.
*
* @param string $alternativeId Alternative id value. Enter blank string for the current id ($this->id)
* @param string $excludeList Comma separated list of fields NOT to include ("sortField" or "sortRev")
* @return string
*/
public function listURL($alternativeId = '', $table = '-1', $excludeList = ''): string
{
$urlParameters = [];
if ((string)$alternativeId !== '') {
$urlParameters['id'] = $alternativeId;
} else {
$urlParameters['id'] = $this->id;
}
if (isset($this->thumbs)) {
$urlParameters['imagemode'] = $this->thumbs;
}
if ($this->returnUrl) {
$urlParameters['returnUrl'] = $this->returnUrl;
}
if ($this->searchString) {
$urlParameters['search_field'] = $this->searchString;
}
if ($this->searchLevels) {
$urlParameters['search_levels'] = $this->searchLevels;
}
if ($this->showLimit) {
$urlParameters['showLimit'] = $this->showLimit;
}
if (isset($this->firstElementNumber)) {
$urlParameters['pointer'] = $this->firstElementNumber;
}
if ((!$excludeList || !GeneralUtility::inList(
$excludeList,
'sortField'
)) && $this->sortField
) {
$urlParameters['sortField'] = $this->sortField;
}
if ((!$excludeList || !GeneralUtility::inList(
$excludeList,
'sortRev'
)) && $this->sortRev
) {
$urlParameters['sortRev'] = $this->sortRev;
}
if (GeneralUtility::_GP('SET')) {
$urlParameters['SET'] = GeneralUtility::_GP('SET');
}
if (GeneralUtility::_GP('show')) {
$urlParameters['show'] = (int)GeneralUtility::_GP('show');
}
$demand = GeneralUtility::_GET('tx_news_web_newsadministration');
if (isset($demand['demand']) && is_array($demand['demand'])) {
$urlParameters['tx_news_web_newsadministration']['demand'] = $demand['demand'];
}
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
return $uriBuilder->buildUriFromRoute('web_NewsAdministration', $urlParameters);
}
}

View File

@@ -0,0 +1,259 @@
<?php
namespace GeorgRinger\News\Backend\RecordList;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Service\CategoryService;
use GeorgRinger\News\Utility\ConstraintHelper;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class for the list rendering of administration module
*/
class RecordListConstraint
{
const TABLE = 'tx_news_domain_model_news';
/**
* Check if current module is the news administration module
*
* @return bool
*/
public function isInAdministrationModule(): bool
{
$vars = GeneralUtility::_GET('route');
return strpos($vars, '/module/web/NewsAdministration') !== false;
}
public function extendQuery(array &$parameters, array $arguments): void
{
$parameters['whereDoctrine'] = [];
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_news_domain_model_news');
$expressionBuilder = $queryBuilder->expr();
// search word
if (isset($arguments['searchWord']) && !empty($arguments['searchWord'])) {
$words = GeneralUtility::trimExplode(' ', $arguments['searchWord'], true);
$fields = ['title', 'teaser', 'bodytext'];
$fieldParts = [];
foreach ($fields as $field) {
$likeParts = [];
$nameParts = str_getcsv($arguments['searchWord'], ' ');
foreach ($nameParts as $part) {
$part = trim($part);
if ($part !== '') {
$likeParts[] = $expressionBuilder->like($field, $queryBuilder->quote('%' . $queryBuilder->escapeLikeWildcards($part) . '%'));
}
}
if (!empty($likeParts)) {
$fieldParts[] = $expressionBuilder->orX(...$likeParts);
}
}
$parameters['whereDoctrine'][] = $expressionBuilder->orX(...$fieldParts);
$parameters['where'][] = $expressionBuilder->orX(...$fieldParts);
}
// top news
$topNewsSetting = (int)$arguments['topNewsRestriction'];
if ($topNewsSetting > 0) {
if ($topNewsSetting === 1) {
$parameters['where'][] = 'istopnews=1';
$parameters['whereDoctrine'][] = $expressionBuilder->eq('istopnews', 1);
} elseif ($topNewsSetting === 2) {
$parameters['where'][] = 'istopnews=0';
$parameters['whereDoctrine'][] = $expressionBuilder->eq('istopnews', 0);
}
}
// archived (1==active, 2==archived)
$archived = (int)$arguments['archived'];
if ($archived > 0) {
$currentTime = $GLOBALS['EXEC_TIME'];
if ($archived === 1) {
$parameters['where'][] = '(archive > ' . $currentTime . ' OR archive=0)';
$parameters['whereDoctrine'][] = $expressionBuilder->orX(
$expressionBuilder->gt('archive', $currentTime),
$expressionBuilder->eq('archive', 0)
);
} elseif ($archived === 2) {
$parameters['where'][] = 'archive > 0 AND archive <' . $currentTime;
$parameters['whereDoctrine'][] = $expressionBuilder->andX(
$expressionBuilder->gt('archive', 0),
$expressionBuilder->lt('archive', $currentTime)
);
}
}
// hidden
$hidden = (int)$arguments['hidden'];
if ($hidden > 0) {
if ($hidden === 1) {
$parameters['where'][] = 'hidden=1';
$parameters['whereDoctrine'][] = $expressionBuilder->eq('hidden', 1);
} elseif ($hidden === 2) {
$parameters['where'][] = 'hidden=0';
$parameters['whereDoctrine'][] = $expressionBuilder->eq('hidden', 0);
}
}
// time constraint low
if (isset($arguments['timeRestriction']) && !empty($arguments['timeRestriction'])) {
try {
$limit = ConstraintHelper::getTimeRestrictionLow($arguments['timeRestriction']);
$parameters['where'][] = 'datetime >=' . $limit;
$parameters['whereDoctrine'][] = $expressionBuilder->gte('datetime', $limit);
} catch (\Exception $e) {
// @todo add flash message
}
}
// time constraint high
if (isset($arguments['timeRestrictionHigh']) && !empty($arguments['timeRestrictionHigh'])) {
try {
$limit = ConstraintHelper::getTimeRestrictionHigh($arguments['timeRestrictionHigh']);
$parameters['where'][] = 'datetime <=' . $limit;
$parameters['whereDoctrine'][] = $expressionBuilder->lte('datetime', $limit);
} catch (\Exception $e) {
// @todo add flash message
}
}
// categories
if (isset($arguments['selectedCategories']) && is_array($arguments['selectedCategories'])) {
$categoryMode = strtolower($arguments['categoryConjunction']);
foreach ($arguments['selectedCategories'] as $key => $category) {
if ((int)$category === 0) {
unset($arguments['selectedCategories'][$key]);
}
}
if (!empty($arguments['selectedCategories'])) {
if ((int)$arguments['includeSubCategories'] === 1) {
$categoryList = implode(',', $arguments['selectedCategories']);
$listWithSubCategories = CategoryService::getChildrenCategories($categoryList);
$arguments['selectedCategories'] = explode(',', $listWithSubCategories);
}
switch ($categoryMode) {
case 'and':
foreach ($arguments['selectedCategories'] as $category) {
$idList = $this->getNewsIdsOfCategory($category);
if (empty($idList)) {
$parameters['where'][] = '1=2';
$parameters['whereDoctrine'][] = $expressionBuilder->eq('uid', 0);
} else {
$parameters['where'][] = sprintf('uid IN(%s)', implode(',', $idList));
$parameters['whereDoctrine'][] = $expressionBuilder->in('uid', $idList);
}
}
break;
case 'or':
$orConstraint = $orConstraintDoctrine = [];
foreach ($arguments['selectedCategories'] as $category) {
$idList = $this->getNewsIdsOfCategory($category);
if (!empty($idList)) {
$orConstraint[] = sprintf('uid IN(%s)', implode(',', $idList));
$orConstraintDoctrine[] = $expressionBuilder->in('uid', $idList);
}
}
if (empty($orConstraint)) {
$parameters['where'][] = '1=2';
$parameters['whereDoctrine'][] = $expressionBuilder->eq('uid', 0);
} else {
$parameters['where'][] = implode(' OR ', $orConstraint);
$parameters['whereDoctrine'][] = $expressionBuilder->orX(...$orConstraintDoctrine);
}
break;
// @todo test that
case 'notor':
$orConstraint = $orConstraintDoctrine = [];
foreach ($arguments['selectedCategories'] as $category) {
$idList = $this->getNewsIdsOfCategory($category);
if (!empty($idList)) {
$orConstraint[] = sprintf('(uid IN (%s))', implode(',', $idList));
$orConstraintDoctrine[] = $expressionBuilder->notIn('uid', $idList);
} else {
$orConstraint[] = '1=2';
$parameters['whereDoctrine'][] = $expressionBuilder->eq('uid', 0);
}
}
if (empty($orConstraint)) {
$parameters['where'][] = '1=2';
$parameters['whereDoctrine'][] = $expressionBuilder->eq('uid', 0);
} else {
$orConstraint = array_unique($orConstraint);
$parameters['where'][] = ' NOT (' . implode(' OR ', $orConstraint) . ')';
$parameters['whereDoctrine'][] = $expressionBuilder->andX(...$orConstraintDoctrine);
}
break;
case 'notand':
foreach ($arguments['selectedCategories'] as $category) {
$idList = $this->getNewsIdsOfCategory($category);
if (!empty($idList)) {
$parameters['where'][] = sprintf('uid NOT IN(%s)', implode(',', $idList));
$parameters['whereDoctrine'][] = $expressionBuilder->notIn('uid', $idList);
}
}
break;
}
}
}
// order
if (isset($arguments['sortingField']) && isset($GLOBALS['TCA']['tx_news_domain_model_news']['columns'][$arguments['sortingField']])) {
$direction = ($arguments['sortingDirection'] === 'asc' || $arguments['sortingDirection'] === 'desc') ? $arguments['sortingDirection'] : '';
$parameters['orderBy'] = [[$arguments['sortingField'], $direction]];
}
}
/**
* @param int $categoryId
* @return array
*/
protected function getNewsIdsOfCategory($categoryId): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_news_domain_model_news');
$queryBuilder->getRestrictions()
->removeByType(StartTimeRestriction::class)
->removeByType(HiddenRestriction::class)
->removeByType(EndTimeRestriction::class);
$res = $queryBuilder
->select('tx_news_domain_model_news.uid', 'sys_category.title')
->from('tx_news_domain_model_news')
->rightJoin(
'tx_news_domain_model_news',
'sys_category_record_mm',
'sys_category_record_mm',
$queryBuilder->expr()->eq('tx_news_domain_model_news.uid', $queryBuilder->quoteIdentifier('sys_category_record_mm.uid_foreign'))
)
->rightJoin(
'sys_category_record_mm',
'sys_category',
'sys_category',
$queryBuilder->expr()->eq('sys_category.uid', $queryBuilder->quoteIdentifier('sys_category_record_mm.uid_local'))
)
->where(
$queryBuilder->expr()->eq('sys_category_record_mm.tablenames', $queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)),
$queryBuilder->expr()->isNotNull('tx_news_domain_model_news.uid'),
$queryBuilder->expr()->eq('sys_category.uid', $queryBuilder->createNamedParameter($categoryId, \PDO::PARAM_INT))
)
->execute();
$idList = [];
while ($row = $res->fetch()) {
$idList[] = $row['uid'];
}
return $idList;
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Backend\Wizard;
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver;
use TYPO3\CMS\Backend\Utility\BackendUtility as BackendUtilityCore;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
class SuggestWizardReceiver extends SuggestWizardDefaultReceiver
{
public function queryTable(&$params, $recursionCounter = 0)
{
$rows = parent::queryTable($params, $recursionCounter);
$searchString = strtolower($params['value']);
$matchRow = array_filter($rows, function ($value) use ($searchString) {
return strtolower($value['label']) === $searchString;
});
if (empty($matchRow)) {
$newUid = StringUtility::getUniqueId('NEW');
$rows[$this->table . '_' . $newUid] = [
'class' => '',
'label' => $params['value'],
'path' => '',
'sprite' => $this->iconFactory->getIconForRecord($this->table, [], Icon::SIZE_SMALL)->render(),
'style' => '',
'table' => $this->table,
'text' => sprintf($this->getLanguageService()->sL('LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:tag_suggest'), $params['value']),
'uid' => $newUid,
];
$configuration = GeneralUtility::makeInstance(EmConfiguration::class);
$pid = $configuration->getTagPid();
if ($pid === 0) {
$pid = $this->getTagPidFromTsConfig($params['uid']);
}
if ($pid !== 0) {
$rows[$this->table . '_' . $newUid]['pid'] = $pid;
}
}
return $rows;
}
/**
* Get pid for tags from TsConfig
*
* @param int $newsUid uid of current news record
* @return int
*/
protected function getTagPidFromTsConfig($newsUid): int
{
$pid = 0;
$newsRecord = BackendUtilityCore::getRecord('tx_news_domain_model_news', (int)$newsUid);
$pagesTsConfig = BackendUtilityCore::getPagesTSconfig($newsRecord['pid']);
if (isset($pagesTsConfig['tx_news.']) && isset($pagesTsConfig['tx_news.']['tagPid'])) {
$pid = (int)$pagesTsConfig['tx_news.']['tagPid'];
}
return $pid;
}
}

View File

@@ -0,0 +1,649 @@
<?php
namespace GeorgRinger\News\Controller;
use GeorgRinger\News\Backend\RecordList\NewsDatabaseRecordList;
use GeorgRinger\News\Domain\Model\Dto\AdministrationDemand;
use GeorgRinger\News\Domain\Repository\AdministrationRepository;
use GeorgRinger\News\Domain\Repository\CategoryRepository;
use GeorgRinger\News\Domain\Repository\NewsRepository;
use GeorgRinger\News\Domain\Repository\TagRepository;
use GeorgRinger\News\Event\AdministrationExtendMenuEvent;
use GeorgRinger\News\Event\AdministrationIndexActionEvent;
use GeorgRinger\News\Event\AdministrationNewsPidListingActionEvent;
use GeorgRinger\News\Utility\Page;
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\Menu\Menu;
use TYPO3\CMS\Backend\Utility\BackendUtility as BackendUtilityCore;
use TYPO3\CMS\Backend\View\BackendTemplateView;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Administration controller
*/
class AdministrationController extends NewsController
{
/** @var int */
protected $pageUid = 0;
/** @var array */
protected $tsConfiguration = [];
/**
* @var \GeorgRinger\News\Domain\Repository\AdministrationRepository
*/
protected $administrationRepository;
/** @var ConfigurationManagerInterface */
protected $configurationManager;
/** @var array */
protected $pageInformation = [];
/** @var array */
protected $allowedNewTables = [];
/** @var array */
protected $deniedNewTables = [];
public function initializeAction()
{
$this->pageUid = (int)GeneralUtility::_GET('id');
$this->pageInformation = BackendUtilityCore::readPageAccess($this->pageUid, '');
$this->setTsConfig();
parent::initializeAction();
}
/** @var BackendTemplateView */
protected $view;
/** @var IconFactory */
protected $iconFactory;
/** @var string */
protected $defaultViewObjectName = BackendTemplateView::class;
/**
* AdministrationController constructor.
* @param NewsRepository $newsRepository
* @param CategoryRepository $categoryRepository
* @param TagRepository $tagRepository
* @param ConfigurationManagerInterface $configurationManager
* @param AdministrationRepository $administrationRepository
* @param AdministrationRepository $iconFactory
*/
public function __construct(
NewsRepository $newsRepository,
CategoryRepository $categoryRepository,
TagRepository $tagRepository,
ConfigurationManagerInterface $configurationManager,
AdministrationRepository $administrationRepository,
IconFactory $iconFactory
) {
parent::__construct($newsRepository, $categoryRepository, $tagRepository, $configurationManager);
$this->administrationRepository = $administrationRepository;
$this->iconFactory = $iconFactory;
}
/**
* @param ViewInterface $view
*/
protected function initializeView(ViewInterface $view)
{
parent::initializeView($view);
$view->getModuleTemplate()->getDocHeaderComponent()->setMetaInformation([]);
$pageRenderer = $this->view->getModuleTemplate()->getPageRenderer();
$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker');
$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/AjaxDataHandler');
$pageRenderer->loadRequireJsModule('TYPO3/CMS/News/AdministrationModule');
$pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf');
$dateFormat = ($GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? ['MM-DD-YYYY', 'HH:mm MM-DD-YYYY'] : ['DD-MM-YYYY', 'HH:mm DD-MM-YYYY']);
$pageRenderer->addInlineSetting('DateTimePicker', 'DateFormat', $dateFormat);
$web_list_modTSconfig = BackendUtilityCore::getPagesTSconfig($this->pageUid)['mod.']['web_list.'] ?? [];
$this->allowedNewTables = GeneralUtility::trimExplode(
',',
$web_list_modTSconfig['allowedNewTables'] ?? '',
true
);
$this->deniedNewTables = GeneralUtility::trimExplode(
',',
$web_list_modTSconfig['deniedNewTables'] ?? '',
true
);
$this->createMenu();
$this->createButtons();
$view->assign('showSupportArea', $this->showSupportArea());
}
protected function createMenu(): void
{
$uriBuilder = $this->objectManager->get(UriBuilder::class);
$uriBuilder->setRequest($this->request);
$menu = $this->view->getModuleTemplate()->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
$menu->setIdentifier('news');
$actions = [
['action' => 'index', 'label' => 'newsListing'],
['action' => 'newsPidListing', 'label' => 'newsPidListing'],
['action' => 'donate', 'label' => 'donate']
];
foreach ($actions as $action) {
$item = $menu->makeMenuItem()
->setTitle($this->getLanguageService()->sL('LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:module.' . $action['label']))
->setHref($uriBuilder->uriFor($action['action'], [], 'Administration', 'News', 'web_NewsAdministration'))
->setActive($this->request->getControllerActionName() === $action['action']);
$menu->addMenuItem($item);
}
$menu = $this->extendMenu($menu);
if ($menu instanceof Menu) {
$this->view->getModuleTemplate()->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);
}
if (is_array($this->pageInformation)) {
$this->view->getModuleTemplate()->getDocHeaderComponent()->setMetaInformation($this->pageInformation);
}
}
/**
* Extends menu selector with items from 3rd party extensions.
*
* @param Menu $menu
* @return Menu
*/
protected function extendMenu(Menu $menu): Menu
{
$event = $this->eventDispatcher->dispatch(new AdministrationExtendMenuEvent($this, $menu));
return $event->getMenu();
}
protected function createButtons(): void
{
$buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar();
$uriBuilder = $this->objectManager->get(UriBuilder::class);
$uriBuilder->setRequest($this->request);
if ($this->request->getControllerActionName() === 'index' && $this->isFilteringEnabled()) {
$toggleButton = $buttonBar->makeLinkButton()
->setHref('#')
->setDataAttributes([
'togglelink' => '1',
'toggle' => 'tooltip',
'placement' => 'bottom',
])
->setTitle($this->getLanguageService()->sL('LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:administration.toggleForm'))
->setIcon($this->iconFactory->getIcon('actions-filter', Icon::SIZE_SMALL));
$buttonBar->addButton($toggleButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
}
$buttons = [
[
'table' => 'tx_news_domain_model_news',
'label' => 'module.createNewNewsRecord',
'action' => 'newNews',
'icon' => 'ext-news-type-default'
],
[
'table' => 'tx_news_domain_model_tag',
'label' => 'module.createNewTag',
'action' => 'newTag',
'icon' => 'ext-news-tag'
],
[
'table' => 'sys_category',
'label' => 'module.createNewCategory',
'action' => 'newCategory',
'icon' => 'mimetypes-x-sys_category'
]
];
foreach ($buttons as $key => $tableConfiguration) {
if ($this->showButton($tableConfiguration['table'])) {
$title = $this->getLanguageService()->sL('LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:' . $tableConfiguration['label']);
$viewButton = $buttonBar->makeLinkButton()
->setHref($uriBuilder->reset()->setRequest($this->request)->uriFor(
$tableConfiguration['action'],
[],
'Administration'
))
->setDataAttributes([
'toggle' => 'tooltip',
'placement' => 'bottom',
'title' => $title])
->setTitle($title)
->setIcon($this->iconFactory->getIcon($tableConfiguration['icon'], Icon::SIZE_SMALL, 'overlay-new'));
$buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
}
}
$clipBoard = GeneralUtility::makeInstance(Clipboard::class);
$clipBoard->initializeClipboard();
$elFromTable = $clipBoard->elFromTable('tx_news_domain_model_news');
if (!empty($elFromTable)) {
$viewButton = $buttonBar->makeLinkButton()
->setHref($clipBoard->pasteUrl('', $this->pageUid))
->setOnClick('return ' . $clipBoard->confirmMsgText(
'pages',
BackendUtilityCore::getRecord('pages', $this->pageUid),
'into',
$elFromTable
))
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_pasteInto'))
->setIcon($this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL));
$buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
}
// Donation
$donationButton = $buttonBar->makeLinkButton()
->setHref($uriBuilder->reset()->setRequest($this->request)->uriFor(
'donate',
[],
'Administration'
))
->setTitle($this->getLanguageService()->sL('LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:administration.donation.title'))
->setIcon($this->iconFactory->getIcon('ext-news-donation', Icon::SIZE_SMALL));
$buttonBar->addButton($donationButton, ButtonBar::BUTTON_POSITION_RIGHT);
// Refresh
$refreshButton = $buttonBar->makeLinkButton()
->setHref(GeneralUtility::getIndpEnv('REQUEST_URI'))
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
$buttonBar->addButton($refreshButton, ButtonBar::BUTTON_POSITION_RIGHT);
// Shortcut
if ($this->getBackendUser()->mayMakeShortcut()) {
$shortcutButton = $buttonBar->makeShortcutButton()
->setGetVariables(['route', 'module', 'id'])
->setModuleName($this->request->getPluginName())
->setDisplayName('Shortcut');
$buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT);
}
}
protected function showButton(string $table): bool
{
if (!$this->getBackendUser()->check('tables_modify', $table)) {
return false;
}
// No deny/allow tables are set:
if (empty($this->allowedNewTables) && empty($this->deniedNewTables)) {
return true;
}
$showButton = !in_array($table, $this->deniedNewTables, true) &&
(empty($this->allowedNewTables) || in_array($table, $this->allowedNewTables, true));
return $showButton;
}
public function indexAction(): void
{
$this->redirectToPageOnStart();
$demandVars = GeneralUtility::_GET('tx_news_web_newsadministration');
$demand = GeneralUtility::makeInstance(AdministrationDemand::class);
$autoSubmitForm = 0;
if (!empty($demandVars['demand'] ?? [])) {
foreach ($demandVars['demand'] as $key => $value) {
if (property_exists(AdministrationDemand::class, $key)) {
$getter = 'set' . ucfirst($key);
$demand->$getter($value);
}
}
} else {
// Preselect by TsConfig (e.g. tx_news.module.preselect.topNewsRestriction = 1)
if (isset($this->tsConfiguration['preselect.'])
&& is_array($this->tsConfiguration['preselect.'])
) {
$anyPropertySet = false;
unset($this->tsConfiguration['preselect.']['orderByAllowed']);
foreach ($this->tsConfiguration['preselect.'] as $propertyName => $propertyValue) {
$propertySet = ObjectAccess::setProperty($demand, $propertyName, $propertyValue);
if ($propertySet) {
$anyPropertySet = true;
}
}
if ($anyPropertySet && !GeneralUtility::_GET('formSubmitted')) {
$autoSubmitForm = 1;
}
}
if (!(bool)($this->tsConfiguration['alwaysShowFilter'] ?? false) || !$this->isFilteringEnabled()) {
$this->view->assign('hideForm', true);
}
}
$this->view->assign('autoSubmitForm', $autoSubmitForm);
$categories = $this->categoryRepository->findParentCategoriesByPid($this->pageUid);
$idList = [];
foreach ($categories as $c) {
$idList[] = $c->getUid();
}
if (empty($idList) && !$this->getBackendUser()->isAdmin()) {
$idList = $this->getBackendUser()->getCategoryMountPoints();
}
if (!empty($this->tsConfiguration['allowedCategoryRootIds'])) {
$allowedList = GeneralUtility::intExplode(',', $this->tsConfiguration['allowedCategoryRootIds'], true);
if (!empty($allowedList)) {
$idList = array_intersect($idList, $allowedList);
}
}
// Initialize the dblist object:
$dblist = GeneralUtility::makeInstance(NewsDatabaseRecordList::class);
$this->view->getModuleTemplate()->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ActionDispatcher');
$this->view->getModuleTemplate()->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
$pageinfo = BackendUtilityCore::readPageAccess($this->pageUid, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
$dblist->pageRow = $pageinfo;
$majorVersion = GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion();
if ($majorVersion <= 10) {
$dblist->calcPerms = Permission::CONTENT_EDIT;
} else {
$dblist->calcPerms->set(Permission::CONTENT_EDIT);
if ($majorVersion >= 11) {
$dblist->displayColumnSelector = false;
$dblist->displayRecordDownload = false;
}
}
$dblist->disableSingleTableView = true;
$dblist->clickTitleMode = 'edit';
$dblist->allFields = 1;
$dblist->displayFields = false;
$dblist->dontShowClipControlPanels = true;
$pointer = MathUtility::forceIntegerInRange(GeneralUtility::_GP('pointer'), 0);
$limit = isset($this->settings['list']['paginate']['itemsPerPage']) ? (int)$this->settings['list']['paginate']['itemsPerPage'] : 20;
$dblist->start(
$this->pageUid,
'tx_news_domain_model_news',
$pointer,
'',
$demand->getRecursive(),
$limit
);
$dblist->setDispFields();
$dblist->noControlPanels = !(bool)($this->tsConfiguration['controlPanels'] ?? false);
$dblist->setFields = [
'tx_news_domain_model_news' => GeneralUtility::trimExplode(',', $this->tsConfiguration['columns'] ?? 'teaser,istopnews,datetime,categories', true)
];
$tableRendering = $dblist->generateList();
if (!$tableRendering && GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion() <= 10) {
$tableRendering = $dblist->HTMLcode;
}
$tableRendering = trim($tableRendering);
$counter = !empty($tableRendering);
$this->view->getModuleTemplate()->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Recordlist/Recordlist');
$assignedValues = [
'moduleToken' => $this->getToken(true),
'page' => $this->pageUid,
'demand' => $demand,
'news' => $tableRendering,
'newsCount' => $counter,
'showSearchForm' => (!is_null($demand) || $counter > 0),
'requestUri' => GeneralUtility::quoteJSvalue(rawurlencode(GeneralUtility::getIndpEnv('REQUEST_URI'))),
'categories' => $this->categoryRepository->findTree($idList),
'filters' => $this->tsConfiguration['filters.'],
'enableFiltering' => $this->isFilteringEnabled(),
'dateformat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']
];
$event = $this->eventDispatcher->dispatch(new AdministrationIndexActionEvent($this, $assignedValues));
$this->view->assignMultiple($event->getAssignedValues());
}
/**
* Shows a page tree including count of news + category records
*/
public function newsPidListingAction(int $treeLevel = 2): void
{
$tree = Page::pageTree($this->pageUid, $treeLevel);
$rawTree = [];
foreach ($tree->tree as $row) {
$this->countRecordsOnPage($row);
$rawTree[] = $row;
}
$event = $this->eventDispatcher->dispatch(new AdministrationNewsPidListingActionEvent($this, $rawTree, $treeLevel));
$this->view->assignMultiple([
'tree' => $event->getRawTree(),
'treeLevel' => $event->getTreeLevel(),
]);
}
public function donateAction(): void
{
$this->view->assignMultiple([
'counts' => $this->administrationRepository->getTotalCounts()
]);
}
/**
* Redirect to form to create a news record
*
* @return void
*/
public function newNewsAction(): void
{
$this->redirectToCreateNewRecord('tx_news_domain_model_news');
}
/**
* Redirect to form to create a category record
*
* @return void
*/
public function newCategoryAction(): void
{
$this->redirectToCreateNewRecord('sys_category');
}
/**
* Redirect to form to create a tag record
*
* @return void
*/
public function newTagAction(): void
{
$this->redirectToCreateNewRecord('tx_news_domain_model_tag');
}
/**
* Update page record array with count of news & category records
*
* @param array $row page record
*
* @return void
*/
private function countRecordsOnPage(array &$row): void
{
$pageUid = (int)$row['row']['uid'];
$counts = [
'countNews' => 'tx_news_domain_model_news',
'countCategories' => 'sys_category',
'countTags' => 'tx_news_domain_model_tag',
];
foreach ($counts as $key => $table) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable($table);
$row[$key] = $queryBuilder->count('*')
->from($table)
->where($queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)))
->execute()
->fetchColumn(0);
}
$row['countAll'] = ($row['countNews'] + $row['countCategories'] + $row['countTags']);
}
/**
* Redirect to creating a new record
*
* @param string $table table name
*/
private function redirectToCreateNewRecord(string $table): void
{
$pid = $this->pageUid;
if ($pid === 0 && isset($this->tsConfiguration['defaultPid.'])
&& is_array($this->tsConfiguration['defaultPid.'])
&& isset($this->tsConfiguration['defaultPid.'][$table])
) {
$pid = (int)$this->tsConfiguration['defaultPid.'][$table];
}
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
$returnUrl = $uriBuilder->buildUriFromRoutePath('/module/web/NewsAdministration/', [
'id' => $this->pageUid,
'token' => $this->getToken(true)
]);
$params = [
'edit[' . $table . '][' . $pid . ']' => 'new',
'returnUrl' => (string)$returnUrl
];
$url = $uriBuilder->buildUriFromRoute('record_edit', $params);
HttpUtility::redirect((string)$url);
}
/**
* Set the TsConfig configuration for the extension
*/
protected function setTsConfig(): void
{
$tsConfig = BackendUtilityCore::getPagesTSconfig($this->pageUid);
if (isset($tsConfig['tx_news.']['module.']) && is_array($tsConfig['tx_news.']['module.'])) {
$this->tsConfiguration = $tsConfig['tx_news.']['module.'];
}
}
/**
* Check if at least one filter is enabled
*/
protected function isFilteringEnabled(): bool
{
if (isset($this->tsConfiguration['filters.'])) {
foreach ($this->tsConfiguration['filters.'] as $filter => $enabled) {
if ($enabled == 1) {
// Check dependencies on other filter
if (($filter === 'categoryConjunction' || $filter === 'includeSubCategories')
&& $this->tsConfiguration['filters.']['categories'] == 0) {
continue;
}
return true;
}
}
}
return false;
}
/**
* If defined in TsConfig with tx_news.module.redirectToPageOnStart = 123
* and the current page id is 0, a redirect to the given page will be done
*/
protected function redirectToPageOnStart(): void
{
$allowedPage = (int)($this->tsConfiguration['allowedPage'] ?? 0);
$redirectPageOnStart = (int)($this->tsConfiguration['redirectToPageOnStart'] ?? 0);
if ($allowedPage > 0 && $this->pageUid !== $allowedPage) {
$id = $allowedPage;
} elseif ($this->pageUid === 0 && $redirectPageOnStart > 0) {
$id = $redirectPageOnStart;
}
if (!empty($id)) {
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
$url = $uriBuilder->buildUriFromRoutePath('/module/web/NewsAdministration/', [
'id' => $id,
'token' => $this->getToken(true)
]);
HttpUtility::redirect((string)$url);
}
}
/**
* Get a CSRF token
*
* @param bool $tokenOnly Set it to TRUE to get only the token, otherwise including the &moduleToken= as prefix
*/
protected function getToken(bool $tokenOnly = false): string
{
$tokenParameterName = 'token';
$token = FormProtectionFactory::get('backend')->generateToken('route', 'web_NewsAdministration');
if ($tokenOnly) {
return $token;
}
return '&' . $tokenParameterName . '=' . $token;
}
/**
* Show support area only for admins in given percent of time
*
* @param int $probabilityInPercent
*/
private function showSupportArea(int $probabilityInPercent = 10): bool
{
if (!$this->getBackendUser()->isAdmin()) {
return false;
}
if (mt_rand() % 100 <= $probabilityInPercent) {
return true;
}
return false;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace GeorgRinger\News\Controller;
use GeorgRinger\News\Event\CategoryListActionEvent;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Category controller
*/
class CategoryController extends NewsController
{
/**
* List categories
*
* @param array $overwriteDemand
*
* @return void
*/
public function listAction(array $overwriteDemand = null)
{
$demand = $this->createDemandObjectFromSettings($this->settings);
$demand->setActionAndClass(__METHOD__, __CLASS__);
if ($overwriteDemand !== null && $this->settings['disableOverrideDemand'] != 1) {
$demand = $this->overwriteDemandObject($demand, $overwriteDemand);
}
$idList = is_array($demand->getCategories()) ? $demand->getCategories() : explode(',', $demand->getCategories());
$startingPoint = null;
if (!empty($this->settings['startingpoint'])) {
$startingPoint = $this->settings['startingpoint'];
}
$assignedValues = [
'categories' => $this->categoryRepository->findTree($idList, $startingPoint),
'overwriteDemand' => $overwriteDemand,
'demand' => $demand,
];
$event = $this->eventDispatcher->dispatch(new CategoryListActionEvent($this, $assignedValues));
$this->view->assignMultiple($event->getAssignedValues());
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace GeorgRinger\News\Controller;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use Exception;
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use GeorgRinger\News\Jobs\ImportJobInterface;
use GeorgRinger\News\Utility\ImportJob;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\View\BackendTemplateView;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use UnexpectedValueException;
/**
* Controller to import news records
*/
class ImportController extends ActionController
{
/**
* Backend Template Container
*
* @var BackendTemplateView
*/
protected $defaultViewObjectName = BackendTemplateView::class;
/**
* Retrieve all available import jobs by traversing trough registered
* import jobs and checking "isEnabled".
*
* @return array
*/
protected function getAvailableJobs(): array
{
$availableJobs = [];
$registeredJobs = ImportJob::getRegisteredJobs();
foreach ($registeredJobs as $registeredJob) {
$jobInstance = $this->objectManager->get($registeredJob['className']);
if ($jobInstance instanceof ImportJobInterface && $jobInstance->isEnabled()) {
$availableJobs[$registeredJob['className']] = $GLOBALS['LANG']->sL($registeredJob['title']);
}
}
return $availableJobs;
}
/**
* Shows the import jobs selection .
*
* @return void
*/
public function indexAction(): void
{
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$pageRenderer->loadRequireJsModule('TYPO3/CMS/News/Import');
$this->view->assignMultiple(
[
'error' => $this->checkCorrectConfiguration(),
'availableJobs' => array_merge([0 => ''], $this->getAvailableJobs()),
'moduleUrl' => $uriBuilder->buildUriFromRoute($this->request->getPluginName())
]
);
}
/**
* Check for correct configuration
*
* @return string
* @throws Exception
* @throws \TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException
*/
protected function checkCorrectConfiguration(): string
{
$error = '';
$settings = GeneralUtility::makeInstance(EmConfiguration::class);
try {
$storageId = (int)$settings->getStorageUidImporter();
$path = $settings->getResourceFolderImporter();
if ($storageId === 0) {
throw new UnexpectedValueException('import.error.configuration.storageUidImporter');
}
if (empty($path)) {
throw new UnexpectedValueException('import.error.configuration.resourceFolderImporter');
}
$storage = $this->getResourceFactory()->getStorageObject($storageId);
$pathExists = $storage->hasFolder($path);
if (!$pathExists) {
throw new FolderDoesNotExistException('Folder does not exist', 1474827988);
}
} catch (FolderDoesNotExistException $e) {
$error = 'import.error.configuration.resourceFolderImporter.notExist';
} catch (UnexpectedValueException $e) {
$error = $e->getMessage();
}
return $error;
}
/**
* Runs an actual job.
*
* @param string $jobClassName
* @param int $offset
* @return string
*/
public function runJobAction($jobClassName, $offset = 0): string
{
/** @var ImportJobInterface $job */
$job = $this->objectManager->get($jobClassName);
$job->run($offset);
return 'OK';
}
/**
* Retrieves the job info of a given jobClass
*
* @param string $jobClassName
* @return string
*/
public function jobInfoAction($jobClassName): string
{
$response = null;
try {
/** @var ImportJobInterface $job */
$job = $this->objectManager->get($jobClassName);
$response = $job->getInfo();
} catch (Exception $e) {
$response['message'] = $e->getMessage();
$response['line'] = $e->getLine();
$response['trace'] = $e->getTrace();
HttpUtility::setResponseCode(HttpUtility::HTTP_STATUS_400);
}
return json_encode($response);
}
/**
* @return ResourceFactory
*/
protected function getResourceFactory(): ResourceFactory
{
return GeneralUtility::makeInstance(ResourceFactory::class);
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace GeorgRinger\News\Controller;
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use TYPO3\CMS\Core\Http\ImmediateResponseException;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Frontend\Controller\ErrorController;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Base controller
*/
class NewsBaseController extends ActionController
{
/**
* Initializes the view before invoking an action method.
* Override this method to solve assign variables common for all actions
* or prepare the view in another way before the action is called.
*
* @param \TYPO3\CMS\Extbase\Mvc\View\ViewInterface $view The view to be initialized
*
* @return void
*/
protected function initializeView(ViewInterface $view)
{
$view->assign('contentObjectData', $this->configurationManager->getContentObject()->data);
$view->assign('emConfiguration', GeneralUtility::makeInstance(EmConfiguration::class));
if (isset($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE'])) {
$view->assign('pageData', $GLOBALS['TSFE']->page);
}
parent::initializeView($view);
}
// /**
// * @param RequestInterface $request
// * @param ResponseInterface $response
// * @throws \Exception
// */
// public function processRequest(RequestInterface $request, ResponseInterface $response)
// {
// try {
// parent::processRequest($request, $response);
// } catch (\Exception $exception) {
// $this->handleKnownExceptionsElseThrowAgain($exception);
// }
// }
/**
* @param \Exception $exception
*
* @throws \Exception
*
* @return void
*/
private function handleKnownExceptionsElseThrowAgain(\Exception $exception): void
{
$previousException = $exception->getPrevious();
if (
$this->actionMethodName === 'detailAction'
&& $previousException instanceof \TYPO3\CMS\Extbase\Property\Exception
&& isset($this->settings['detail']['errorHandling'])
) {
$this->handleNoNewsFoundError($this->settings['detail']['errorHandling']);
} else {
throw $exception;
}
}
/**
* Error handling if no news entry is found
*
* @param string $configuration configuration what will be done
* @throws \InvalidArgumentException
*/
protected function handleNoNewsFoundError($configuration): string
{
if (empty($configuration)) {
return '';
}
$options = GeneralUtility::trimExplode(',', $configuration, true);
switch ($options[0]) {
case 'redirectToListView':
$this->redirect('list');
break;
case 'redirectToPage':
if (count($options) === 1 || count($options) > 3) {
$msg = sprintf(
'If error handling "%s" is used, either 2 or 3 arguments, split by "," must be used',
$options[0]
);
throw new \InvalidArgumentException($msg);
}
$this->uriBuilder->reset();
$this->uriBuilder->setTargetPageUid($options[1]);
$this->uriBuilder->setCreateAbsoluteUri(true);
if (GeneralUtility::getIndpEnv('TYPO3_SSL')) {
$this->uriBuilder->setAbsoluteUriScheme('https');
}
$url = $this->uriBuilder->build();
if (isset($options[2])) {
$this->redirectToUri($url, 0, (int)$options[2]);
} else {
$this->redirectToUri($url);
}
break;
case 'pageNotFoundHandler':
$typo3Information = GeneralUtility::makeInstance(Typo3Version::class);
if ($typo3Information->getMajorVersion() === 9) {
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction($GLOBALS['TYPO3_REQUEST'], 'No news entry found.');
throw new ImmediateResponseException($response);
}
$message = 'No news entry found!';
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$GLOBALS['TYPO3_REQUEST'],
$message
);
throw new ImmediateResponseException($response, 1590468229);
break;
case 'showStandaloneTemplate':
if (isset($options[2])) {
$statusCode = constant(HttpUtility::class . '::HTTP_STATUS_' . $options[2]);
} else {
$statusCode = HttpUtility::HTTP_STATUS_404;
}
HttpUtility::setResponseCode($statusCode);
$this->getTypoScriptFrontendController()->set_no_cache('News record not found');
$standaloneTemplate = GeneralUtility::makeInstance(StandaloneView::class);
$standaloneTemplate->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($options[1]));
return $standaloneTemplate->render();
break;
default:
return '';
}
}
/**
* @return TypoScriptFrontendController
*/
protected function getTypoScriptFrontendController(): TypoScriptFrontendController
{
return $GLOBALS['TSFE'];
}
}

View File

@@ -0,0 +1,703 @@
<?php
namespace GeorgRinger\News\Controller;
use GeorgRinger\News\Domain\Model\Dto\NewsDemand;
use GeorgRinger\News\Domain\Model\Dto\Search;
use GeorgRinger\News\Domain\Model\News;
use GeorgRinger\News\Domain\Repository\CategoryRepository;
use GeorgRinger\News\Domain\Repository\NewsRepository;
use GeorgRinger\News\Domain\Repository\TagRepository;
use GeorgRinger\News\Event\NewsCheckPidOfNewsRecordFailedInDetailActionEvent;
use GeorgRinger\News\Event\NewsDateMenuActionEvent;
use GeorgRinger\News\Event\NewsDetailActionEvent;
use GeorgRinger\News\Event\NewsListActionEvent;
use GeorgRinger\News\Event\NewsListSelectedActionEvent;
use GeorgRinger\News\Event\NewsSearchFormActionEvent;
use GeorgRinger\News\Event\NewsSearchResultActionEvent;
use GeorgRinger\News\Pagination\QueryResultPaginator;
use GeorgRinger\News\Seo\NewsTitleProvider;
use GeorgRinger\News\Utility\Cache;
use GeorgRinger\News\Utility\ClassCacheManager;
use GeorgRinger\News\Utility\Page;
use GeorgRinger\News\Utility\TypoScript;
use GeorgRinger\NumberedPagination\NumberedPagination;
use TYPO3\CMS\Core\Pagination\SimplePagination;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Fluid\View\TemplateView;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Controller of news records
*
*/
class NewsController extends NewsBaseController
{
/**
* @var \GeorgRinger\News\Domain\Repository\NewsRepository
*/
protected $newsRepository;
/**
* @var \GeorgRinger\News\Domain\Repository\CategoryRepository
*/
protected $categoryRepository;
/**
* @var \GeorgRinger\News\Domain\Repository\TagRepository
*/
protected $tagRepository;
/**
* @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
*/
protected $configurationManager;
/** @var array */
protected $ignoredSettingsForOverride = ['demandclass', 'orderbyallowed', 'selectedList'];
/**
* Original settings without any magic done by stdWrap and skipping empty values
*
* @var array
*/
protected $originalSettings = [];
/**
* NewsController constructor.
* @param NewsRepository $newsRepository
* @param CategoryRepository $categoryRepository
* @param TagRepository $tagRepository
* @param ConfigurationManagerInterface $configurationManager
*/
public function __construct(
NewsRepository $newsRepository,
CategoryRepository $categoryRepository,
TagRepository $tagRepository,
ConfigurationManagerInterface $configurationManager
) {
$this->newsRepository = $newsRepository;
$this->categoryRepository = $categoryRepository;
$this->tagRepository = $tagRepository;
$this->configurationManager = $configurationManager;
}
/**
* Initializes the current action
*
* @return void
*/
public function initializeAction()
{
GeneralUtility::makeInstance(ClassCacheManager::class)->reBuildSimple();
$this->buildSettings();
if (isset($this->settings['format'])) {
$this->request->setFormat($this->settings['format']);
}
// Only do this in Frontend Context
if (!empty($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE'])) {
// We only want to set the tag once in one request, so we have to cache that statically if it has been done
static $cacheTagsSet = false;
/** @var $typoScriptFrontendController \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController */
$typoScriptFrontendController = $GLOBALS['TSFE'];
if (!$cacheTagsSet) {
$typoScriptFrontendController->addCacheTags(['tx_news']);
$cacheTagsSet = true;
}
}
}
/**
* Create the demand object which define which records will get shown
*
* @param array $settings
* @param string $class optional class which must be an instance of \GeorgRinger\News\Domain\Model\Dto\NewsDemand
* @return \GeorgRinger\News\Domain\Model\Dto\NewsDemand
*/
protected function createDemandObjectFromSettings(
array $settings,
$class = NewsDemand::class
): \GeorgRinger\News\Domain\Model\Dto\NewsDemand {
$class = isset($settings['demandClass']) && !empty($settings['demandClass']) ? $settings['demandClass'] : $class;
/* @var $demand NewsDemand */
$demand = GeneralUtility::makeInstance($class, $settings);
if (!$demand instanceof NewsDemand) {
throw new \UnexpectedValueException(
sprintf(
'The demand object must be an instance of %s, but %s given!',
NewsDemand::class,
$class
),
1423157953
);
}
$demand->setCategories(GeneralUtility::trimExplode(',', $settings['categories'] ?? '', true));
$demand->setCategoryConjunction((string)($settings['categoryConjunction'] ?? ''));
$demand->setIncludeSubCategories((bool)($settings['includeSubCategories'] ?? false));
$demand->setTags((string)($settings['tags'] ?? ''));
$demand->setTopNewsRestriction((int)($settings['topNewsRestriction'] ?? 0));
$demand->setTimeRestriction($settings['timeRestriction'] ?? '');
$demand->setTimeRestrictionHigh($settings['timeRestrictionHigh'] ?? '');
$demand->setArchiveRestriction((string)($settings['archiveRestriction'] ?? ''));
$demand->setExcludeAlreadyDisplayedNews((bool)($settings['excludeAlreadyDisplayedNews'] ?? false));
$demand->setHideIdList((string)($settings['hideIdList'] ?? ''));
if ($settings['orderBy'] ?? '') {
$demand->setOrder($settings['orderBy'] . ' ' . $settings['orderDirection']);
}
$demand->setOrderByAllowed((string)($settings['orderByAllowed'] ?? ''));
$demand->setTopNewsFirst((bool)($settings['topNewsFirst'] ?? false));
$demand->setLimit((int)($settings['limit'] ?? 0));
$demand->setOffset((int)($settings['offset'] ?? 0));
$demand->setSearchFields((string)($settings['search']['fields'] ?? ''));
$demand->setDateField((string)($settings['dateField'] ?? ''));
$demand->setMonth((int)($settings['month'] ?? 0));
$demand->setYear((int)($settings['year'] ?? 0));
$demand->setStoragePage(Page::extendPidListByChildren(
(string)($settings['startingpoint'] ?? ''),
(int)($settings['recursive'] ?? 0)
));
if ($hooks = $GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['Controller/NewsController.php']['createDemandObjectFromSettings'] ?? []) {
$params = [
'demand' => $demand,
'settings' => $settings,
'class' => $class,
];
foreach ($hooks as $reference) {
GeneralUtility::callUserFunction($reference, $params, $this);
}
}
return $demand;
}
/**
* Overwrites a given demand object by an propertyName => $propertyValue array
*
* @param \GeorgRinger\News\Domain\Model\Dto\NewsDemand $demand
* @param array $overwriteDemand
* @return \GeorgRinger\News\Domain\Model\Dto\NewsDemand
*/
protected function overwriteDemandObject(NewsDemand $demand, array $overwriteDemand): \GeorgRinger\News\Domain\Model\Dto\NewsDemand
{
foreach ($this->ignoredSettingsForOverride as $property) {
unset($overwriteDemand[$property]);
}
foreach ($overwriteDemand as $propertyName => $propertyValue) {
if (in_array(strtolower($propertyName), $this->ignoredSettingsForOverride, true)) {
continue;
}
if ($propertyValue !== '' || $this->settings['allowEmptyStringsForOverwriteDemand']) {
if (in_array($propertyName, ['categories'], true)) {
$propertyValue = GeneralUtility::trimExplode(',', $propertyValue, true);
}
ObjectAccess::setProperty($demand, $propertyName, $propertyValue);
}
}
return $demand;
}
/**
* Output a list view of news
*
* @param array|null $overwriteDemand
*
* @return void
*/
public function listAction(array $overwriteDemand = null)
{
$this->forwardToDetailActionWhenRequested();
$demand = $this->createDemandObjectFromSettings($this->settings);
$demand->setActionAndClass(__METHOD__, __CLASS__);
if ((int)($this->settings['disableOverrideDemand'] ?? 1) !== 1 && $overwriteDemand !== null) {
$demand = $this->overwriteDemandObject($demand, $overwriteDemand);
}
$newsRecords = $this->newsRepository->findDemanded($demand);
// pagination
$paginationConfiguration = $this->settings['list']['paginate'] ?? [];
$itemsPerPage = (int)(($paginationConfiguration['itemsPerPage'] ?? '') ?: 10);
$maximumNumberOfLinks = (int)($paginationConfiguration['maximumNumberOfLinks'] ?? 0);
$currentPage = $this->request->hasArgument('currentPage') ? (int)$this->request->getArgument('currentPage') : 1;
$paginator = GeneralUtility::makeInstance(QueryResultPaginator::class, $newsRecords, $currentPage, $itemsPerPage, (int)($this->settings['limit'] ?? 0), (int)($this->settings['offset'] ?? 0));
$paginationClass = $paginationConfiguration['class'] ?? SimplePagination::class;
if (class_exists(NumberedPagination::class) && $paginationClass === NumberedPagination::class && $maximumNumberOfLinks) {
$pagination = GeneralUtility::makeInstance(NumberedPagination::class, $paginator, $maximumNumberOfLinks);
} elseif (class_exists($paginationClass)) {
$pagination = GeneralUtility::makeInstance($paginationClass, $paginator);
} else {
$pagination = GeneralUtility::makeInstance(SimplePagination::class, $paginator);
}
$assignedValues = [
'news' => $newsRecords,
'overwriteDemand' => $overwriteDemand,
'demand' => $demand,
'categories' => null,
'tags' => null,
'settings' => $this->settings,
'pagination' => [
'currentPage' => $currentPage,
'paginator' => $paginator,
'pagination' => $pagination,
]
];
if ($demand->getCategories() !== '') {
$categoriesList = $demand->getCategories();
if (is_string($categoriesList)) {
$categoriesList = GeneralUtility::trimExplode(',', $categoriesList);
}
if (!empty($categoriesList)) {
$assignedValues['categories'] = $this->categoryRepository->findByIdList($categoriesList);
}
}
if ($demand->getTags() !== '') {
$tagList = $demand->getTags();
if (!is_array($tagList)) {
$tagList = GeneralUtility::trimExplode(',', $tagList);
}
if (!empty($tagList)) {
$assignedValues['tags'] = $this->tagRepository->findByIdList($tagList);
}
}
$event = $this->eventDispatcher->dispatch(new NewsListActionEvent($this, $assignedValues));
$this->view->assignMultiple($event->getAssignedValues());
Cache::addPageCacheTagsByDemandObject($demand);
}
/**
* When list action is called along with a news argument, we forward to detail action.
*/
protected function forwardToDetailActionWhenRequested()
{
if (!$this->isActionAllowed('detail')
|| !$this->request->hasArgument('news')
) {
return;
}
$this->forward('detail', null, null, ['news' => $this->request->getArgument('news')]);
}
/**
* Checks whether an action is enabled in switchableControllerActions configuration
*
* @param string $action
* @return bool
*/
protected function isActionAllowed(string $action): bool
{
$frameworkConfiguration = $this->configurationManager->getConfiguration($this->configurationManager::CONFIGURATION_TYPE_FRAMEWORK);
$allowedActions = $frameworkConfiguration['controllerConfiguration']['News']['actions'] ?? [];
return \in_array($action, $allowedActions, true);
}
/**
* Output a selected list view of news
*/
public function selectedListAction(): void
{
$newsRecords = [];
$demand = $this->createDemandObjectFromSettings($this->settings);
$demand->setActionAndClass(__METHOD__, __CLASS__);
if (empty($this->originalSettings['orderBy'] ?? '')) {
$idList = GeneralUtility::trimExplode(',', $this->settings['selectedList'], true);
foreach ($idList as $id) {
$news = $this->newsRepository->findByIdentifier($id);
if ($news) {
$newsRecords[] = $news;
}
}
} else {
$demand->setIdList($this->settings['selectedList']);
$newsRecords = $this->newsRepository->findDemanded($demand);
}
$assignedValues = [
'news' => $newsRecords,
'demand' => $demand,
'settings' => $this->settings
];
$event = $this->eventDispatcher->dispatch(new NewsListSelectedActionEvent($this, $assignedValues));
$this->view->assignMultiple($event->getAssignedValues());
if (!empty($newsRecords) && is_a($newsRecords[0], News::class)) {
Cache::addCacheTagsByNewsRecords($newsRecords);
}
}
/**
* Single view of a news record
*
* @param \GeorgRinger\News\Domain\Model\News $news news item
* @param int $currentPage current page for optional pagination
*
* @return null|string
*/
public function detailAction(News $news = null, $currentPage = 1)
{
if ($news === null || ($this->settings['isShortcut'] ?? false)) {
$previewNewsId = (int)($this->settings['singleNews'] ?? 0);
if ($this->request->hasArgument('news_preview')) {
$previewNewsId = (int)$this->request->getArgument('news_preview');
}
if ($previewNewsId > 0) {
if ($this->isPreviewOfHiddenRecordsEnabled()) {
$news = $this->newsRepository->findByUid($previewNewsId, false);
} else {
$news = $this->newsRepository->findByUid($previewNewsId);
}
}
}
if (is_a($news, News::class) && ($this->settings['detail']['checkPidOfNewsRecord'] ?? false)
) {
$news = $this->checkPidOfNewsRecord($news);
}
$demand = $this->createDemandObjectFromSettings($this->settings);
$demand->setActionAndClass(__METHOD__, __CLASS__);
$assignedValues = [
'newsItem' => $news,
'currentPage' => (int)$currentPage,
'demand' => $demand,
'settings' => $this->settings
];
$event = $this->eventDispatcher->dispatch(new NewsDetailActionEvent($this, $assignedValues));
$assignedValues = $event->getAssignedValues();
$news = $assignedValues['newsItem'];
$this->view->assignMultiple($assignedValues);
// reset news if type is internal or external
if ($news && !($this->settings['isShortcut'] ?? false) && ($news->getType() === '1' || $news->getType() === '2')) {
$news = null;
}
if ($news !== null) {
Page::setRegisterProperties($this->settings['detail']['registerProperties'] ?? false, $news);
Cache::addCacheTagsByNewsRecords([$news]);
if ($this->settings['detail']['pageTitle']['_typoScriptNodeValue'] ?? false) {
$providerConfiguration = $this->settings['detail']['pageTitle'] ?? [];
$providerClass = $providerConfiguration['provider'] ?? NewsTitleProvider::class;
/** @var NewsTitleProvider $provider */
$provider = GeneralUtility::makeInstance($providerClass);
$provider->setTitleByNews($news, $providerConfiguration);
}
} elseif (isset($this->settings['detail']['errorHandling'])) {
$errorContent = $this->handleNoNewsFoundError($this->settings['detail']['errorHandling']);
if ($errorContent) {
return $errorContent;
}
}
}
/**
* Checks if the news pid could be found in the startingpoint settings of the detail plugin and
* if the pid could not be found it return NULL instead of the news object.
*
* @param \GeorgRinger\News\Domain\Model\News $news
* @return \GeorgRinger\News\Domain\Model\News|null
*/
protected function checkPidOfNewsRecord(News $news): ?\GeorgRinger\News\Domain\Model\News
{
$allowedStoragePages = GeneralUtility::trimExplode(
',',
Page::extendPidListByChildren(
$this->settings['startingpoint'],
$this->settings['recursive']
),
true
);
if (count($allowedStoragePages) > 0 && !in_array($news->getPid(), $allowedStoragePages)) {
$this->eventDispatcher->dispatch(new NewsCheckPidOfNewsRecordFailedInDetailActionEvent($this, $news));
$news = null;
}
return $news;
}
/**
* Checks if preview is enabled either in TS or FlexForm
*
* @return bool
*/
protected function isPreviewOfHiddenRecordsEnabled(): bool
{
if (!empty($this->settings['previewHiddenRecords']) && $this->settings['previewHiddenRecords'] == 2) {
$previewEnabled = !empty($this->settings['enablePreviewOfHiddenRecords']);
} else {
$previewEnabled = !empty($this->settings['previewHiddenRecords']);
}
return $previewEnabled;
}
/**
* Render a menu by dates, e.g. years, months or dates
*
* @param array|null $overwriteDemand
*
* @return void
*/
public function dateMenuAction(array $overwriteDemand = null): void
{
$demand = $this->createDemandObjectFromSettings($this->settings);
$demand->setActionAndClass(__METHOD__, __CLASS__);
if ($this->settings['disableOverrideDemand'] != 1 && $overwriteDemand !== null) {
$overwriteDemandTemp = $overwriteDemand;
unset($overwriteDemandTemp['year']);
unset($overwriteDemandTemp['month']);
$demand = $this->overwriteDemandObject(
$demand,
$overwriteDemandTemp
);
unset($overwriteDemandTemp);
}
// It might be that those are set, @see http://forge.typo3.org/issues/44759
$demand->setLimit(0);
$demand->setOffset(0);
// @todo: find a better way to do this related to #13856
if (!$dateField = $this->settings['dateField']) {
$dateField = 'datetime';
}
$demand->setOrder($dateField . ' ' . $this->settings['orderDirection']);
$newsRecords = $this->newsRepository->findDemanded($demand);
$demand->setOrder($this->settings['orderDirection']);
$statistics = $this->newsRepository->countByDate($demand);
$assignedValues = [
'listPid' => ($this->settings['listPid'] ? $this->settings['listPid'] : $GLOBALS['TSFE']->id),
'dateField' => $dateField,
'data' => $statistics,
'news' => $newsRecords,
'overwriteDemand' => $overwriteDemand,
'demand' => $demand,
'settings' => $this->settings
];
$event = $this->eventDispatcher->dispatch(new NewsDateMenuActionEvent($this, $assignedValues));
$this->view->assignMultiple($event->getAssignedValues());
}
/**
* Display the search form
*
* @param \GeorgRinger\News\Domain\Model\Dto\Search $search
* @param array $overwriteDemand
*
* @return void
*/
public function searchFormAction(
Search $search = null,
array $overwriteDemand = []
): void {
$demand = $this->createDemandObjectFromSettings($this->settings);
$demand->setActionAndClass(__METHOD__, __CLASS__);
if ($this->settings['disableOverrideDemand'] != 1 && $overwriteDemand !== null) {
$demand = $this->overwriteDemandObject($demand, $overwriteDemand);
}
if (is_null($search)) {
$search = GeneralUtility::makeInstance(Search::class);
}
$demand->setSearch($search);
$assignedValues = [
'search' => $search,
'overwriteDemand' => $overwriteDemand,
'demand' => $demand,
'settings' => $this->settings
];
$event = $this->eventDispatcher->dispatch(new NewsSearchFormActionEvent($this, $assignedValues));
$this->view->assignMultiple($event->getAssignedValues());
}
/**
* Displays the search result
*
* @param \GeorgRinger\News\Domain\Model\Dto\Search $search
* @param array $overwriteDemand
*
* @return void
*/
public function searchResultAction(
Search $search = null,
array $overwriteDemand = []
): void {
$demand = $this->createDemandObjectFromSettings($this->settings);
$demand->setActionAndClass(__METHOD__, __CLASS__);
if ($this->settings['disableOverrideDemand'] != 1 && $overwriteDemand !== null) {
$demand = $this->overwriteDemandObject($demand, $overwriteDemand);
}
if (!is_null($search)) {
$search->setFields($this->settings['search']['fields']);
$search->setDateField($this->settings['dateField']);
$search->setSplitSubjectWords((bool)$this->settings['search']['splitSearchWord']);
}
$demand->setSearch($search);
$assignedValues = [
'news' => $this->newsRepository->findDemanded($demand),
'overwriteDemand' => $overwriteDemand,
'search' => $search,
'demand' => $demand,
'settings' => $this->settings
];
$event = $this->eventDispatcher->dispatch(new NewsSearchResultActionEvent($this, $assignedValues));
$this->view->assignMultiple($event->getAssignedValues());
}
/**
* initialize search result action
*
* @return void
*/
public function initializeSearchResultAction(): void
{
$this->initializeSearchActions();
}
/**
* Initialize search form action
*
* @return void
*/
public function initializeSearchFormAction(): void
{
$this->initializeSearchActions();
}
/**
* Initialize searchForm and searchResult actions
*
* @return void
*/
protected function initializeSearchActions(): void
{
if ($this->arguments->hasArgument('search')) {
$propertyMappingConfiguration = $this->arguments['search']->getPropertyMappingConfiguration();
$propertyMappingConfiguration->allowAllProperties();
$propertyMappingConfiguration->setTypeConverterOption('TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter', PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, true);
}
}
/***************************************************************************
* helper
**********************/
/**
* Injects the Configuration Manager and is initializing the framework settings
*
* @param ConfigurationManagerInterface $configurationManager Instance of the Configuration Manager
*
* @return void
*/
public function buildSettings()
{
$tsSettings = $this->configurationManager->getConfiguration(
ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK,
'news',
'news_pi1'
);
$originalSettings = $this->configurationManager->getConfiguration(
ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS
);
$propertiesNotAllowedViaFlexForms = ['orderByAllowed'];
foreach ($propertiesNotAllowedViaFlexForms as $property) {
$originalSettings[$property] = ($tsSettings['settings'] ?? [])[$property] ?? ($originalSettings[$property] ?? '');
}
$this->originalSettings = $originalSettings;
// Use stdWrap for given defined settings
if (isset($originalSettings['useStdWrap']) && !empty($originalSettings['useStdWrap'])) {
$typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
$typoScriptArray = $typoScriptService->convertPlainArrayToTypoScriptArray($originalSettings);
$stdWrapProperties = GeneralUtility::trimExplode(',', $originalSettings['useStdWrap'], true);
foreach ($stdWrapProperties as $key) {
if (is_array($typoScriptArray[$key . '.'])) {
$originalSettings[$key] = $this->configurationManager->getContentObject()->stdWrap(
$typoScriptArray[$key],
$typoScriptArray[$key . '.']
);
}
}
}
// start override
if (isset($tsSettings['settings']['overrideFlexformSettingsIfEmpty'])) {
$typoScriptUtility = GeneralUtility::makeInstance(TypoScript::class);
$originalSettings = $typoScriptUtility->override($originalSettings, $tsSettings);
}
foreach ($hooks = ($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['Controller/NewsController.php']['overrideSettings'] ?? []) as $_funcRef) {
$_params = [
'originalSettings' => $originalSettings,
'tsSettings' => $tsSettings,
];
$originalSettings = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
}
$this->settings = $originalSettings;
}
/**
* Injects a view.
* This function is for testing purposes only.
*
* @param \TYPO3\CMS\Fluid\View\TemplateView $view the view to inject
*
* @return void
*/
public function setView(TemplateView $view): void
{
$this->view = $view;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace GeorgRinger\News\Controller;
use GeorgRinger\News\Event\TagListActionEvent;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Tag controller
*/
class TagController extends NewsController
{
/**
* List tags
*
* @param array $overwriteDemand
*
* @return void
*/
public function listAction(array $overwriteDemand = null)
{
// Default value is wrong for tags
if ($this->settings['orderBy'] === 'datetime') {
unset($this->settings['orderBy']);
}
$demand = $this->createDemandObjectFromSettings($this->settings);
$demand->setActionAndClass(__METHOD__, __CLASS__);
if ($overwriteDemand !== null && $this->settings['disableOverrideDemand'] != 1) {
$demand = $this->overwriteDemandObject($demand, $overwriteDemand);
}
$assignedValues = [
'tags' => $this->tagRepository->findDemanded($demand),
'overwriteDemand' => $overwriteDemand,
'demand' => $demand,
];
$event = $this->eventDispatcher->dispatch(new TagListActionEvent($this, $assignedValues));
$this->view->assignMultiple($event->getAssignedValues());
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\DataProcessing;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Add the current news record to any menu, e.g. breadcrumb
*
* 20 = GeorgRinger\News\DataProcessing\AddNewsToMenuProcessor
* 20.menus = breadcrumbMenu,specialMenu
*/
class AddNewsToMenuProcessor implements DataProcessorInterface
{
/**
* @param ContentObjectRenderer $cObj
* @param array $contentObjectConfiguration
* @param array $processorConfiguration
* @param array $processedData
* @return array
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData): array
{
if (!$processorConfiguration['menus']) {
return $processedData;
}
$newsRecord = $this->getNewsRecord();
if ($newsRecord) {
$menus = GeneralUtility::trimExplode(',', $processorConfiguration['menus'], true);
foreach ($menus as $menu) {
if (isset($processedData[$menu])) {
$this->addNewsRecordToMenu($newsRecord, $processedData[$menu]);
}
}
}
return $processedData;
}
/**
* Add the news record to the menu items
*
* @param array $newsRecord
* @param array $menu
*
* @return void
*/
protected function addNewsRecordToMenu(array $newsRecord, array &$menu): void
{
foreach ($menu as &$menuItem) {
$menuItem['current'] = 0;
}
$menu[] = [
'data' => $newsRecord,
'title' => $newsRecord['title'],
'active' => 1,
'current' => 1,
'link' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'),
'isNews' => true
];
}
/**
* Get the news record including possible translations
*
* @return array
*/
protected function getNewsRecord(): array
{
$newsId = 0;
$vars = GeneralUtility::_GET('tx_news_pi1');
if (isset($vars['news'])) {
$newsId = (int)$vars['news'];
}
if ($newsId) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_news_domain_model_news');
$row = $queryBuilder
->select('*')
->from('tx_news_domain_model_news')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT))
)
->execute()
->fetch();
if ($row) {
$row = $this->getTsfe()->sys_page->getRecordOverlay('tx_news_domain_model_news', $row, $this->getCurrentLanguage());
}
if (is_array($row) && !empty($row)) {
return $row;
}
}
return [];
}
/**
* Get current language
*
* @return int
*/
protected function getCurrentLanguage(): int
{
$languageId = 0;
$context = GeneralUtility::makeInstance(Context::class);
try {
$languageId = $context->getPropertyFromAspect('language', 'contentId');
} catch (AspectNotFoundException $e) {
// do nothing
}
return (int)$languageId;
}
/**
* @return TypoScriptFrontendController
*/
protected function getTsfe(): TypoScriptFrontendController
{
return $GLOBALS['TSFE'];
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\DataProcessing;
use GeorgRinger\News\Seo\NewsAvailability;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Disable language item on a detail page if the news is not translated
*
* 20 = GeorgRinger\News\DataProcessing\DisableLanguageMenuProcessor
* 20.menus = languageMenu
*/
class DisableLanguageMenuProcessor implements DataProcessorInterface
{
/**
* @param ContentObjectRenderer $cObj
* @param array $contentObjectConfiguration
* @param array $processorConfiguration
* @param array $processedData
* @return array
*/
public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData): array
{
if (!$processorConfiguration['menus']) {
return $processedData;
}
$newsId = $this->getNewsId();
if ($newsId === 0) {
return $processedData;
}
$menus = GeneralUtility::trimExplode(',', $processorConfiguration['menus'], true);
foreach ($menus as $menu) {
if (isset($processedData[$menu])) {
$this->handleMenu($newsId, $processedData[$menu]);
}
}
return $processedData;
}
/**
* @param int $newsId
* @param array $menu
*/
protected function handleMenu(int $newsId, array &$menu): void
{
$newsAvailability = GeneralUtility::makeInstance(NewsAvailability::class);
foreach ($menu as &$item) {
if (!$item['available']) {
continue;
}
try {
$availability = $newsAvailability->check((int)$item['languageId'], $newsId);
if (!$availability) {
$item['available'] = false;
$item['availableReason'] = 'news';
}
} catch (\Exception $e) {
}
}
}
/**
* @return int
*/
protected function getNewsId(): int
{
$newsId = 0;
/** @var PageArguments $pageArguments */
$pageArguments = $this->getRequest()->getAttribute('routing');
if (isset($pageArguments->getRouteArguments()['tx_news_pi1']['news'])) {
$newsId = (int)$pageArguments->getRouteArguments()['tx_news_pi1']['news'];
} elseif (isset($this->getRequest()->getQueryParams()['tx_news_pi1']['news'])) {
$newsId = (int)$this->getRequest()->getQueryParams()['tx_news_pi1']['news'];
}
return $newsId;
}
/**
* @return ServerRequestInterface
*/
protected function getRequest(): ServerRequestInterface
{
return $GLOBALS['TYPO3_REQUEST'];
}
}

View File

@@ -0,0 +1,383 @@
<?php
namespace GeorgRinger\News\Domain\Model;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Category Model
*/
class Category extends AbstractEntity
{
/**
* @var int
*/
protected $sorting = 0;
/**
* @var \DateTime
*/
protected $crdate;
/**
* @var \DateTime
*/
protected $tstamp;
/**
* @var \DateTime
*/
protected $starttime;
/**
* @var \DateTime
*/
protected $endtime;
/**
* @var bool
*/
protected $hidden = false;
/**
* @var int
*/
protected $sysLanguageUid = 0;
/**
* @var int
*/
protected $l10nParent = 0;
/**
* @var string
*/
protected $title = '';
/**
* @var string
*/
protected $description = '';
/**
* @var \GeorgRinger\News\Domain\Model\Category
* @TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $parentcategory;
/**
* @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\GeorgRinger\News\Domain\Model\FileReference>
* @TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $images;
/**
* @var int
*/
protected $shortcut = 0;
/**
* @var int
*/
protected $singlePid = 0;
/**
* @var string
*/
protected $importId = '';
/**
* @var string
*/
protected $importSource = '';
/**
* keep it as string as it should be only used during imports
* @var string
*/
protected $feGroup = '';
/**
* @var string
*/
protected $seoTitle = '';
/**
* @var string
*/
protected $seoDescription = '';
/**
* @var string
*/
protected $seoHeadline = '';
/**
* @var string
*/
protected $seoText = '';
/**
* @var string
*/
protected $slug = '';
public function __construct()
{
$this->images = new ObjectStorage();
}
public function getSorting(): int
{
return $this->sorting;
}
public function setSorting(int $sorting): void
{
$this->sorting = $sorting;
}
public function getCrdate(): ?\DateTime
{
return $this->crdate;
}
public function setCrdate(\DateTime $crdate): void
{
$this->crdate = $crdate;
}
public function getTstamp(): ?\DateTime
{
return $this->tstamp;
}
public function setTstamp(\DateTime $tstamp): void
{
$this->tstamp = $tstamp;
}
public function getStarttime(): ?\DateTime
{
return $this->starttime;
}
public function setStarttime(\DateTime $starttime): void
{
$this->starttime = $starttime;
}
public function getEndtime(): ?\DateTime
{
return $this->endtime;
}
public function setEndtime(\DateTime $endtime): void
{
$this->endtime = $endtime;
}
public function getHidden(): bool
{
return $this->hidden;
}
public function setHidden(bool $hidden): void
{
$this->hidden = $hidden;
}
public function getSysLanguageUid(): int
{
// int cast is needed as $this->_languageUid is null by default
return (int)$this->_languageUid;
}
public function setSysLanguageUid(int $sysLanguageUid): void
{
$this->_languageUid = $sysLanguageUid;
}
public function getL10nParent(): int
{
return $this->l10nParent;
}
public function setL10nParent(int $l10nParent): void
{
$this->l10nParent = $l10nParent;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): void
{
$this->title = $title;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function getParentcategory(): ?\GeorgRinger\News\Domain\Model\Category
{
return $this->parentcategory instanceof LazyLoadingProxy
? $this->parentcategory->_loadRealInstance()
: $this->parentcategory;
}
public function setParentcategory(\GeorgRinger\News\Domain\Model\Category $category): void
{
$this->parentcategory = $category;
}
/**
* @psalm-return ObjectStorage<FileReference>
*/
public function getImages(): ObjectStorage
{
return $this->images;
}
public function setImages(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $images): void
{
$this->images = $images;
}
public function addImage(FileReference $image): void
{
$this->images->attach($image);
}
public function removeImage(FileReference $image): void
{
$this->images->detach($image);
}
public function getFirstImage(): ?FileReference
{
$images = $this->getImages();
$images->rewind();
return $images->valid() ? $images->current() : null;
}
public function getShortcut(): int
{
return $this->shortcut;
}
public function setShortcut(int $shortcut): void
{
$this->shortcut = $shortcut;
}
public function getSinglePid(): int
{
return $this->singlePid;
}
public function setSinglePid(int $singlePid): void
{
$this->singlePid = $singlePid;
}
public function getImportId(): string
{
return $this->importId;
}
public function setImportId(string $importId): void
{
$this->importId = $importId;
}
public function getImportSource(): string
{
return $this->importSource;
}
public function setImportSource(string $importSource): void
{
$this->importSource = $importSource;
}
public function getFeGroup(): string
{
return $this->feGroup;
}
public function setFeGroup(string $feGroup): void
{
$this->feGroup = $feGroup;
}
public function getSeoTitle(): string
{
return $this->seoTitle;
}
public function setSeoTitle(string $seoTitle): void
{
$this->seoTitle = $seoTitle;
}
public function getSeoDescription(): string
{
return $this->seoDescription;
}
public function setSeoDescription(string $seoDescription): void
{
$this->seoDescription = $seoDescription;
}
public function getSeoHeadline(): string
{
return $this->seoHeadline;
}
public function setSeoHeadline(string $seoHeadline): void
{
$this->seoHeadline = $seoHeadline;
}
public function getSeoText(): string
{
return $this->seoText;
}
public function setSeoText(string $seoText): void
{
$this->seoText = $seoText;
}
public function getSlug(): string
{
return $this->slug;
}
public function setSlug(string $slug): void
{
$this->slug = $slug;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace GeorgRinger\News\Domain\Model;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Demanded repository interface
*/
interface DemandInterface
{
}

View File

@@ -0,0 +1,180 @@
<?php
namespace GeorgRinger\News\Domain\Model\Dto;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Administration Demand model
*/
class AdministrationDemand extends NewsDemand
{
/**
* @var string
*/
protected $recursive = '';
/**
* @var array
*/
protected $selectedCategories = [];
/**
* @var string
*/
protected $sortingField = 'datetime';
/**
* @var string
*/
protected $sortingDirection = 'desc';
/**
* @var string
*/
protected $searchWord = '';
/**
* @var int
*/
protected $hidden = 0;
/**
* @var int
*/
protected $archived = 0;
/**
* @return string
*/
public function getRecursive(): string
{
return $this->recursive;
}
/**
* @param string $recursive
*
* @return void
*/
public function setRecursive(string $recursive): void
{
$this->recursive = $recursive;
}
/**
* @return array
*/
public function getSelectedCategories(): array
{
return $this->selectedCategories;
}
/**
* @param string|array $selectedCategories
*
* @return void
*/
public function setSelectedCategories($selectedCategories)
{
if ($selectedCategories === '0' || $selectedCategories === ['0']) {
return;
}
if (is_string($selectedCategories)) {
$selectedCategories = explode(',', $selectedCategories);
}
$this->selectedCategories = $selectedCategories;
}
/**
* @return string
*/
public function getSortingField(): string
{
return $this->sortingField;
}
/**
* @param string $sortingField
* @return void
*/
public function setSortingField(string $sortingField): void
{
$this->sortingField = $sortingField;
}
/**
* @return string
*/
public function getSortingDirection(): string
{
return $this->sortingDirection;
}
/**
* @param string $sortingDirection
* @return void
*/
public function setSortingDirection(string $sortingDirection): void
{
$this->sortingDirection = $sortingDirection;
}
/**
* @return string
*/
public function getSearchWord(): string
{
return $this->searchWord;
}
/**
* @param string $searchWord
* @return void
*/
public function setSearchWord(string $searchWord): void
{
$this->searchWord = $searchWord;
}
/**
* @return int
*/
public function getHidden(): int
{
return $this->hidden;
}
/**
* @param int $hidden
* @return void
*/
public function setHidden(int $hidden): void
{
$this->hidden = $hidden;
}
/**
* @return int
*/
public function getArchived(): int
{
return $this->archived;
}
/**
* @param int $archived
*
* @return void
*/
public function setArchived(int $archived): void
{
$this->archived = $archived;
}
}

View File

@@ -0,0 +1,188 @@
<?php
namespace GeorgRinger\News\Domain\Model\Dto;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Extension Manager configuration
*/
class EmConfiguration
{
/**
* Fill the properties properly
*
* @param array $configuration em configuration
*/
public function __construct(array $configuration = [])
{
if (empty($configuration)) {
try {
$extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class);
$configuration = $extensionConfiguration->get('news');
} catch (\Exception $exception) {
// do nothing
}
}
foreach ($configuration as $key => $value) {
if (property_exists(__CLASS__, $key)) {
$this->$key = $value;
}
}
}
/** @var int */
protected $tagPid = 0;
/** @var bool */
protected $prependAtCopy = true;
/** @var string */
protected $categoryRestriction = '';
/** @var bool */
protected $categoryBeGroupTceFormsRestriction = false;
/** @var bool */
protected $contentElementRelation = true;
/** @var bool */
protected $contentElementPreview = true;
/** @var bool */
protected $manualSorting = false;
/** @var string */
protected $archiveDate = 'date';
/** @var bool */
protected $dateTimeNotRequired = false;
/** @var bool */
protected $showImporter = false;
/** @var bool */
protected $rteForTeaser = false;
/** @var bool */
protected $showAdministrationModule = true;
/** @var bool */
protected $hidePageTreeForAdministrationModule = false;
/** @var int */
protected $storageUidImporter = 1;
/** @var string */
protected $resourceFolderImporter = '/news_import';
/** @var bool */
protected $advancedMediaPreview = true;
/** @var string */
protected $slugBehaviour = 'unique';
public function getTagPid(): int
{
return (int)$this->tagPid;
}
public function getPrependAtCopy(): bool
{
return (bool)$this->prependAtCopy;
}
public function getCategoryRestriction(): string
{
return $this->categoryRestriction;
}
public function getCategoryBeGroupTceFormsRestriction(): bool
{
return (bool)$this->categoryBeGroupTceFormsRestriction;
}
public function getContentElementRelation(): bool
{
return (bool)$this->contentElementRelation;
}
public function getContentElementPreview(): bool
{
return (bool)$this->contentElementPreview;
}
public function getManualSorting(): bool
{
return (bool)$this->manualSorting;
}
public function getArchiveDate(): string
{
return $this->archiveDate;
}
public function getShowImporter(): bool
{
return (bool)$this->showImporter;
}
public function setShowAdministrationModule($showAdministrationModule): void
{
$this->showAdministrationModule = $showAdministrationModule;
}
public function getShowAdministrationModule(): bool
{
return (bool)$this->showAdministrationModule;
}
public function getRteForTeaser(): bool
{
return (bool)$this->rteForTeaser;
}
public function getResourceFolderImporter(): string
{
return $this->resourceFolderImporter;
}
public function getStorageUidImporter(): int
{
return (int)$this->storageUidImporter;
}
public function getDateTimeNotRequired(): bool
{
return (bool)$this->dateTimeNotRequired;
}
public function getDateTimeRequired(): bool
{
return !(bool)$this->dateTimeNotRequired;
}
public function getHidePageTreeForAdministrationModule(): bool
{
return (bool)$this->hidePageTreeForAdministrationModule;
}
public function isAdvancedMediaPreview(): bool
{
return (bool)$this->advancedMediaPreview;
}
public function getSlugBehaviour(): string
{
return $this->slugBehaviour;
}
}

View File

@@ -0,0 +1,734 @@
<?php
namespace GeorgRinger\News\Domain\Model\Dto;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\DemandInterface;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
/**
* News Demand object which holds all information to get the correct news records.
*/
class NewsDemand extends AbstractEntity implements DemandInterface
{
/**
* @var array
*/
protected $categories = [];
/**
* @var string
*/
protected $categoryConjunction = '';
/**
* @var bool
*/
protected $includeSubCategories = false;
/**
* @var string
*/
protected $author = '';
/** @var string */
protected $tags = '';
/**
* @var string
*/
protected $archiveRestriction = '';
/**
* @var string
*/
protected $timeRestriction = '';
/** @var string */
protected $timeRestrictionHigh = '';
/** @var int */
protected $topNewsRestriction = 0;
/** @var string */
protected $dateField = '';
/** @var int */
protected $month = 0;
/** @var int */
protected $year = 0;
/** @var int */
protected $day = 0;
/** @var string */
protected $searchFields = '';
/** @var Search */
protected $search;
/** @var string */
protected $order = '';
/** @var string */
protected $orderByAllowed = '';
/** @var bool */
protected $topNewsFirst = false;
/** @var string */
protected $storagePage = '';
/** @var int */
protected $limit = 0;
/** @var int */
protected $offset = 0;
/** @var bool */
protected $excludeAlreadyDisplayedNews = false;
/** @var string */
protected $hideIdList = '';
/** @var string */
protected $idList = '';
/** @var string */
protected $action = '';
/** @var string */
protected $class = '';
/**
* List of allowed types
*
* @var array
*/
protected $types = [];
/**
* Holding custom data, use e.g. your ext key as array key
*
* @var array
*/
protected $_customSettings = [];
/**
* Set archive settings
*
* @param string $archiveRestriction archive setting
* @return NewsDemand
*/
public function setArchiveRestriction(string $archiveRestriction): NewsDemand
{
$this->archiveRestriction = $archiveRestriction;
return $this;
}
/**
* Get archive setting
*
* @return string
*/
public function getArchiveRestriction(): string
{
return $this->archiveRestriction;
}
/**
* List of allowed categories
*
* @param array $categories categories
* @return NewsDemand
*/
public function setCategories(array $categories): NewsDemand
{
$this->categories = $categories;
return $this;
}
/**
* Get allowed categories
*
* @return array
*/
public function getCategories(): array
{
return $this->categories;
}
/**
* Set category mode
*
* @param string $categoryConjunction
* @return NewsDemand
*/
public function setCategoryConjunction(string $categoryConjunction): NewsDemand
{
$this->categoryConjunction = $categoryConjunction;
return $this;
}
/**
* Get category mode
*
* @return string
*/
public function getCategoryConjunction(): string
{
return $this->categoryConjunction;
}
/**
* Get include sub categories
* @return bool
*/
public function getIncludeSubCategories(): bool
{
return (boolean)$this->includeSubCategories;
}
/**
* @param bool $includeSubCategories
* @return NewsDemand
*/
public function setIncludeSubCategories(bool $includeSubCategories): NewsDemand
{
$this->includeSubCategories = $includeSubCategories;
return $this;
}
/**
* Set author
*
* @param string $author
* @return NewsDemand
*/
public function setAuthor(string $author): NewsDemand
{
$this->author = $author;
return $this;
}
/**
* Get author
*
* @return string
*/
public function getAuthor(): string
{
return $this->author;
}
/**
* Get Tags
*
* @return string
*/
public function getTags(): string
{
return $this->tags;
}
/**
* Set Tags
*
* @param string $tags tags
* @return NewsDemand
*/
public function setTags(string $tags): NewsDemand
{
$this->tags = $tags;
return $this;
}
/**
* Set time limit low, either integer or string
*
* @param string|int $timeRestriction
* @return NewsDemand
*/
public function setTimeRestriction($timeRestriction): NewsDemand
{
$this->timeRestriction = $timeRestriction;
return $this;
}
/**
* Get time limit low
*
* @return string|int
*/
public function getTimeRestriction()
{
return $this->timeRestriction;
}
/**
* Get time limit high
*
* @return string|int
*/
public function getTimeRestrictionHigh()
{
return $this->timeRestrictionHigh;
}
/**
* Set time limit high
*
* @param string|int $timeRestrictionHigh
* @return NewsDemand
*/
public function setTimeRestrictionHigh($timeRestrictionHigh): NewsDemand
{
$this->timeRestrictionHigh = $timeRestrictionHigh;
return $this;
}
/**
* Set order
*
* @param string $order order
* @return NewsDemand
*/
public function setOrder(string $order): NewsDemand
{
$this->order = $order;
return $this;
}
/**
* Get order
*
* @return string
*/
public function getOrder(): string
{
return $this->order;
}
/**
* Set order allowed
*
* @param string $orderByAllowed allowed fields for ordering
* @return NewsDemand
*/
public function setOrderByAllowed(string $orderByAllowed): NewsDemand
{
$this->orderByAllowed = $orderByAllowed;
return $this;
}
/**
* Get allowed order fields
*
* @return string
*/
public function getOrderByAllowed(): string
{
return $this->orderByAllowed;
}
/**
* Set order respect top news flag
*
* @param bool $topNewsFirst respect top news flag
* @return NewsDemand
*/
public function setTopNewsFirst(bool $topNewsFirst): NewsDemand
{
$this->topNewsFirst = $topNewsFirst;
return $this;
}
/**
* Get order respect top news flag
*
* @return bool
*/
public function getTopNewsFirst(): bool
{
return $this->topNewsFirst;
}
/**
* Set search fields
*
* @param string $searchFields search fields
*
* @return NewsDemand
*/
public function setSearchFields(string $searchFields): NewsDemand
{
$this->searchFields = $searchFields;
return $this;
}
/**
* Get search fields
*
* @return string
*/
public function getSearchFields(): string
{
return $this->searchFields;
}
/**
* Set top news setting
*
* @param int $topNewsRestriction top news settings
* @return NewsDemand
*/
public function setTopNewsRestriction(int $topNewsRestriction): NewsDemand
{
$this->topNewsRestriction = $topNewsRestriction;
return $this;
}
/**
* Get top news setting
*
* @return int
*/
public function getTopNewsRestriction(): int
{
return $this->topNewsRestriction;
}
/**
* Set list of storage pages
*
* @param string $storagePage storage page list
* @return NewsDemand
*/
public function setStoragePage(string $storagePage): NewsDemand
{
$this->storagePage = $storagePage;
return $this;
}
/**
* Get list of storage pages
*
* @return string
*/
public function getStoragePage(): string
{
return $this->storagePage;
}
/**
* Get day restriction
*
* @return int
*/
public function getDay(): int
{
return $this->day;
}
/**
* Set day restriction
*
* @param int $day
* @return NewsDemand
*/
public function setDay(int $day): NewsDemand
{
$this->day = (int)$day;
return $this;
}
/**
* Get month restriction
*
* @return int
*/
public function getMonth(): int
{
return $this->month;
}
/**
* Set month restriction
*
* @param int $month month
* @return NewsDemand
*/
public function setMonth(int $month): NewsDemand
{
$this->month = (int)$month;
return $this;
}
/**
* Get year restriction
*
* @return int
*/
public function getYear(): int
{
return $this->year;
}
/**
* Set year restriction
*
* @param int $year year
* @return NewsDemand
*/
public function setYear(int $year): NewsDemand
{
$this->year = (int)$year;
return $this;
}
/**
* Set limit
*
* @param int $limit limit
* @return NewsDemand
*/
public function setLimit(int $limit): NewsDemand
{
$this->limit = $limit;
return $this;
}
/**
* Get limit
*
* @return int
*/
public function getLimit(): int
{
return $this->limit;
}
/**
* Set offset
*
* @param int $offset offset
* @return NewsDemand
*/
public function setOffset(int $offset): NewsDemand
{
$this->offset = $offset;
return $this;
}
/**
* Get offset
*
* @return int
*/
public function getOffset(): int
{
return $this->offset;
}
/**
* Set date field which is used for datemenu
*
* @param string $dateField datefield
* @return NewsDemand
*/
public function setDateField(string $dateField): NewsDemand
{
$this->dateField = $dateField;
return $this;
}
/**
* Get date field which is used for datemenu
*
* @return string
*/
public function getDateField(): string
{
if (in_array($this->dateField, ['datetime', 'archive'], true)
|| isset($GLOBALS['TCA']['tx_news_domain_model_news']['columns'][$this->dateField])) {
return $this->dateField;
}
return '';
}
/**
* Get search object
*
* @return null|Search
*/
public function getSearch(): ?Search
{
return $this->search;
}
/**
* Set search object
*
* @param null|Search $search search object
* @return NewsDemand
*/
public function setSearch(Search $search = null): NewsDemand
{
$this->search = $search;
return $this;
}
/**
* Set flag if displayed news records should be excluded
*
* @param bool $excludeAlreadyDisplayedNews
* @return NewsDemand
*/
public function setExcludeAlreadyDisplayedNews(bool $excludeAlreadyDisplayedNews): NewsDemand
{
$this->excludeAlreadyDisplayedNews = $excludeAlreadyDisplayedNews;
return $this;
}
/**
* Get flag if displayed news records should be excluded
*
* @return bool
*/
public function getExcludeAlreadyDisplayedNews(): bool
{
return $this->excludeAlreadyDisplayedNews;
}
/**
* @return string
*/
public function getHideIdList(): string
{
return $this->hideIdList;
}
/**
* @param string $hideIdList
* @return NewsDemand
*/
public function setHideIdList(string $hideIdList): NewsDemand
{
$this->hideIdList = $hideIdList;
return $this;
}
/**
* @return string
*/
public function getIdList(): string
{
return $this->idList;
}
/**
* @param string $idList
* @return NewsDemand
*/
public function setIdList(string $idList): NewsDemand
{
$this->idList = $idList;
return $this;
}
/**
* @return string
*/
public function getAction(): string
{
return $this->action;
}
/**
* @param string $action
* @return NewsDemand
*/
public function setAction(string $action): NewsDemand
{
$this->action = $action;
return $this;
}
/**
* @return string
*/
public function getClass(): string
{
return $this->class;
}
/**
* @param string $class
* @return NewsDemand
*/
public function setClass(string $class): NewsDemand
{
$this->class = $class;
return $this;
}
/**
* @param string $action
* @param string $controller
* @return NewsDemand
*/
public function setActionAndClass(string $action, string $controller): NewsDemand
{
$this->action = $action;
$this->class = $controller;
return $this;
}
/**
* Get allowed types
*
* @return array
*/
public function getTypes(): array
{
return $this->types;
}
/**
* Set allowed types
*
* @param array $types
*
* @return void
*/
public function setTypes(array $types): void
{
$this->types = $types;
}
/**
* @return array
*/
public function getCustomSettings(): array
{
return $this->_customSettings;
}
/**
* @param array $customSettings
*
* @return void
*/
public function setCustomSettings(array $customSettings): void
{
$this->_customSettings = $customSettings;
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace GeorgRinger\News\Domain\Model\Dto;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* News Demand object which holds all information to get the correct
* news records.
*/
class Search extends AbstractEntity
{
/**
* Basic search word
*
* @var string
*/
protected $subject = '';
/**
* Search fields
*
* @var string
*/
protected $fields = '';
/**
* Minimum date
*
* @var string
*/
protected $minimumDate = '';
/**
* Maximum date
*
* @var string
*/
protected $maximumDate = '';
/**
* Field using for date queries
*
* @var string
*/
protected $dateField = '';
/** @var bool */
protected $splitSubjectWords = false;
/**
* Get the subject
*
* @return string
*/
public function getSubject(): string
{
return $this->subject;
}
/**
* Set subject
*
* @param string $subject
*/
public function setSubject(string $subject): void
{
$this->subject = $subject;
}
/**
* Get fields
*
* @return string
*/
public function getFields(): string
{
return $this->fields;
}
/**
* Set fields
*
* @param string $fields
*
* @return void
*/
public function setFields(string $fields): void
{
$this->fields = $fields;
}
/**
* @param string $maximumDate
*
* @return void
*/
public function setMaximumDate($maximumDate): void
{
$this->maximumDate = $maximumDate;
}
/**
* @return string
*/
public function getMaximumDate(): string
{
return $this->maximumDate;
}
/**
* @param string $minimumDate
*
* @return void
*/
public function setMinimumDate(string $minimumDate): void
{
$this->minimumDate = $minimumDate;
}
/**
* @return string
*/
public function getMinimumDate(): string
{
return $this->minimumDate;
}
/**
* @param string $dateField
*
* @return void
*/
public function setDateField($dateField): void
{
$this->dateField = $dateField;
}
/**
* @return string
*/
public function getDateField(): string
{
return $this->dateField;
}
/**
* @return bool
*/
public function isSplitSubjectWords(): bool
{
return $this->splitSubjectWords;
}
/**
* @param bool $splitSubjectWords
*
* @return void
*/
public function setSplitSubjectWords($splitSubjectWords): void
{
$this->splitSubjectWords = $splitSubjectWords;
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace GeorgRinger\News\Domain\Model;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* File Reference
*/
class FileReference extends \TYPO3\CMS\Extbase\Domain\Model\FileReference
{
const VIEW_DETAIL_ONLY = 0;
const VIEW_LIST_AND_DETAIL = 1;
const VIEW_LIST_ONLY = 2;
/**
* Obsolete when foreign_selector is supported by ExtBase persistence layer
*
* @var int
*/
protected $uidLocal = 0;
/**
* @var string
*/
protected $title = '';
/**
* @var string
*/
protected $description = '';
/**
* @var string
*/
protected $alternative = '';
/**
* @var string
*/
protected $link = '';
/**
* @var int
*/
protected $showinpreview = 0;
/**
* Set File uid
*
* @param int $fileUid
*
* @return void
*/
public function setFileUid($fileUid): void
{
$this->uidLocal = $fileUid;
}
/**
* Get File UID
*
* @return int
*/
public function getFileUid(): int
{
return $this->uidLocal;
}
/**
* Set alternative
*
* @param string $alternative
*
* @return void
*/
public function setAlternative($alternative): void
{
$this->alternative = $alternative;
}
/**
* Get alternative
*
* @return string
*/
public function getAlternative(): string
{
return (string)($this->alternative !== '' ? $this->alternative : $this->getOriginalResource()->getAlternative());
}
/**
* Set description
*
* @param string $description
*
* @return void
*/
public function setDescription($description): void
{
$this->description = $description;
}
/**
* Get description
*
* @return string
*/
public function getDescription(): string
{
return (string)($this->description !== '' ? $this->description : $this->getOriginalResource()->getDescription());
}
/**
* Set link
*
* @param string $link
*
* @return void
*/
public function setLink($link): void
{
$this->link = $link;
}
/**
* Get link
*
* @return mixed
*/
public function getLink()
{
return (string)($this->link !== '' ? $this->link : $this->getOriginalResource()->getLink());
}
/**
* Set title
*
* @param string $title
*
* @return void
*/
public function setTitle($title): void
{
$this->title = $title;
}
/**
* Get title
*
* @return string
*/
public function getTitle(): string
{
return (string)($this->title !== '' ? $this->title : $this->getOriginalResource()->getTitle());
}
/**
* Set showinpreview
*
* @param int $showinpreview
*
* @return void
*/
public function setShowinpreview($showinpreview): void
{
$this->showinpreview = $showinpreview;
}
/**
* Get showinpreview
*
* @return int
*/
public function getShowinpreview(): int
{
return $this->showinpreview;
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace GeorgRinger\News\Domain\Model;
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Link model
*/
class Link extends AbstractValueObject
{
/**
* @var \DateTime
*/
protected $crdate;
/**
* @var \DateTime
*/
protected $tstamp;
/**
* @var string
*/
protected $title = '';
/**
* @var string
*/
protected $description = '';
/**
* @var string
*/
protected $uri = '';
/**
* @var int
*/
protected $l10nParent = 0;
/**
* Get creation date
*
* @return null|\DateTime
*/
public function getCrdate(): ?\DateTime
{
return $this->crdate;
}
/**
* Set creation date
*
* @param \DateTime $crdate creation date
*
* @return void
*/
public function setCrdate($crdate): void
{
$this->crdate = $crdate;
}
/**
* Get timestamp
*
* @return null|\DateTime
*/
public function getTstamp(): ?\DateTime
{
return $this->tstamp;
}
/**
* Set timestamp
*
* @param \DateTime $tstamp timestamp
*
* @return void
*/
public function setTstamp($tstamp): void
{
$this->tstamp = $tstamp;
}
/**
* Get title
*
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* Set title
*
* @param string $title title
*
* @return void
*/
public function setTitle($title): void
{
$this->title = $title;
}
/**
* Get description
*
* @return string
*/
public function getDescription(): string
{
return $this->description;
}
/**
* Set description
*
* @param string $description description
*
* @return void
*/
public function setDescription($description): void
{
$this->description = $description;
}
/**
* Get uri
*
* @return string
*/
public function getUri(): string
{
return $this->uri;
}
/**
* Set uri
*
* @param string $uri uri
*
* @return void
*/
public function setUri($uri): void
{
$this->uri = $uri;
}
/**
* Set sys language
*
* @param int $sysLanguageUid
*
* @return void
*/
public function setSysLanguageUid($sysLanguageUid): void
{
$this->_languageUid = $sysLanguageUid;
}
/**
* Get sys language
*
* @return int
*/
public function getSysLanguageUid(): int
{
return $this->_languageUid;
}
/**
* Set l10n parent
*
* @param int $l10nParent
*
* @return void
*/
public function setL10nParent($l10nParent): void
{
$this->l10nParent = $l10nParent;
}
/**
* Get l10n parent
*
* @return int
*/
public function getL10nParent(): int
{
return $this->l10nParent;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
<?php
namespace GeorgRinger\News\Domain\Model;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* News model for default news
*/
class NewsDefault extends News
{
}

View File

@@ -0,0 +1,17 @@
<?php
namespace GeorgRinger\News\Domain\Model;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* News model for external news
*/
class NewsExternal extends News
{
}

View File

@@ -0,0 +1,17 @@
<?php
namespace GeorgRinger\News\Domain\Model;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* News model for internal news
*/
class NewsInternal extends News
{
}

View File

@@ -0,0 +1,212 @@
<?php
namespace GeorgRinger\News\Domain\Model;
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Tag model
*/
class Tag extends AbstractValueObject
{
/**
* @var \DateTime
*/
protected $crdate;
/**
* @var \DateTime
*/
protected $tstamp;
/**
* @var string
*/
protected $title = '';
/**
* @var string
*/
protected $seoTitle = '';
/**
* @var string
*/
protected $seoDescription = '';
/**
* @var string
*/
protected $seoHeadline = '';
/**
* @var string
*/
protected $seoText = '';
/** @var string */
protected $slug = '';
/**
* Get crdate
*
* @return null|\DateTime
*/
public function getCrdate(): ?\DateTime
{
return $this->crdate;
}
/**
* Set crdate
*
* @param \DateTime $crdate crdate
*
* @return void
*/
public function setCrdate($crdate): void
{
$this->crdate = $crdate;
}
/**
* Get Tstamp
*
* @return null|\DateTime
*/
public function getTstamp(): ?\DateTime
{
return $this->tstamp;
}
/**
* Set tstamp
*
* @param \DateTime $tstamp tstamp
*
* @return void
*/
public function setTstamp($tstamp): void
{
$this->tstamp = $tstamp;
}
/**
* Get title
*
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* Set title
*
* @param string $title title
*
* @return void
*/
public function setTitle($title): void
{
$this->title = $title;
}
/**
* @return string
*/
public function getSeoTitle(): string
{
return $this->seoTitle;
}
/**
* @param string $seoTitle
*
* @return void
*/
public function setSeoTitle($seoTitle): void
{
$this->seoTitle = $seoTitle;
}
/**
* @return string
*/
public function getSeoDescription(): string
{
return $this->seoDescription;
}
/**
* @param string $seoDescription
*
* @return void
*/
public function setSeoDescription($seoDescription): void
{
$this->seoDescription = $seoDescription;
}
/**
* @return string
*/
public function getSeoHeadline(): string
{
return $this->seoHeadline;
}
/**
* @param string $seoHeadline
*
* @return void
*/
public function setSeoHeadline($seoHeadline): void
{
$this->seoHeadline = $seoHeadline;
}
/**
* @return string
*/
public function getSeoText(): string
{
return $this->seoText;
}
/**
* @param string $seoText
*
* @return void
*/
public function setSeoText($seoText): void
{
$this->seoText = $seoText;
}
/**
* @return string
*/
public function getSlug(): string
{
return $this->slug;
}
/**
* @param string $slug
*
* @return void
*/
public function setSlug($slug): void
{
$this->slug = $slug;
}
}

View File

@@ -0,0 +1,573 @@
<?php
namespace GeorgRinger\News\Domain\Model;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Model of tt_content
*/
class TtContent extends AbstractEntity
{
/**
* @var \DateTime
*/
protected $crdate;
/**
* @var \DateTime
*/
protected $tstamp;
/**
* @var string
*/
protected $CType = '';
/**
* @var string
*/
protected $header = '';
/**
* @var string
*/
protected $headerPosition = '';
/**
* @var string
*/
protected $bodytext = '';
/**
* @var int
*/
protected $colPos = 0;
/**
* @var string
*/
protected $image = '';
/**
* @var int
*/
protected $imagewidth = 0;
/**
* @var int
*/
protected $imageorient = 0;
/**
* @var string
*/
protected $imagecaption = '';
/**
* @var int
*/
protected $imagecols = 0;
/**
* @var int
*/
protected $imageborder = 0;
/**
* @var string
*/
protected $media = '';
/**
* @var string
*/
protected $layout = '';
/**
* @var int
*/
protected $cols = 0;
/**
* @var string
*/
protected $subheader = '';
/**
* @var string
*/
protected $headerLink = '';
/**
* @var string
*/
protected $imageLink = '';
/**
* @var string
*/
protected $imageZoom = '';
/**
* @var string
*/
protected $altText = '';
/**
* @var string
*/
protected $titleText = '';
/**
* @var string
*/
protected $headerLayout = '';
/**
* @var string
*/
protected $listType = '';
/**
* @return null|\DateTime
*/
public function getCrdate(): ?\DateTime
{
return $this->crdate;
}
/**
* @param \DateTime $crdate
*
* @return void
*/
public function setCrdate($crdate): void
{
$this->crdate = $crdate;
}
/**
* @return null|\DateTime
*/
public function getTstamp(): ?\DateTime
{
return $this->tstamp;
}
/**
* @param \DateTime $tstamp
*
* @return void
*/
public function setTstamp($tstamp): void
{
$this->tstamp = $tstamp;
}
/**
* @return string
*/
public function getCType(): string
{
return $this->CType;
}
/**
* @param $ctype
*
* @return void
*/
public function setCType(string $ctype): void
{
$this->CType = $ctype;
}
/**
* @return string
*/
public function getHeader(): string
{
return $this->header;
}
/**
* @param $header
*
* @return void
*/
public function setHeader(string $header): void
{
$this->header = $header;
}
/**
* @return string
*/
public function getHeaderPosition(): string
{
return $this->headerPosition;
}
/**
* @param $headerPosition
*
* @return void
*/
public function setHeaderPosition(string $headerPosition): void
{
$this->headerPosition = $headerPosition;
}
/**
* @return string
*/
public function getBodytext(): string
{
return $this->bodytext;
}
/**
* @param $bodytext
*
* @return void
*/
public function setBodytext(string $bodytext): void
{
$this->bodytext = $bodytext;
}
/**
* Get the colpos
*
* @return int
*/
public function getColPos(): int
{
return (int)$this->colPos;
}
/**
* Set colpos
*
* @param int $colPos
*
* @return void
*/
public function setColPos($colPos): void
{
$this->colPos = $colPos;
}
/**
* @return string
*/
public function getImage(): string
{
return $this->image;
}
/**
* @param $image
*
* @return void
*/
public function setImage(string $image): void
{
$this->image = $image;
}
/**
* @return int
*/
public function getImagewidth(): int
{
return $this->imagewidth;
}
/**
* @param $imagewidth
*
* @return void
*/
public function setImagewidth(int $imagewidth): void
{
$this->imagewidth = $imagewidth;
}
/**
* @return int
*/
public function getImageorient(): int
{
return $this->imageorient;
}
/**
* @param $imageorient
*
* @return void
*/
public function setImageorient(int $imageorient): void
{
$this->imageorient = $imageorient;
}
/**
* @return string
*/
public function getImagecaption(): string
{
return $this->imagecaption;
}
/**
* @param $imagecaption
*
* @return void
*/
public function setImagecaption(string $imagecaption): void
{
$this->imagecaption = $imagecaption;
}
/**
* @return int
*/
public function getImagecols(): int
{
return $this->imagecols;
}
/**
* @param $imagecols
*
* @return void
*/
public function setImagecols(int $imagecols): void
{
$this->imagecols = $imagecols;
}
/**
* @return int
*/
public function getImageborder(): int
{
return $this->imageborder;
}
/**
* @param $imageborder
*
* @return void
*/
public function setImageborder(int $imageborder): void
{
$this->imageborder = $imageborder;
}
/**
* @return string
*/
public function getMedia(): string
{
return $this->media;
}
/**
* @param $media
*
* @return void
*/
public function setMedia(string $media): void
{
$this->media = $media;
}
/**
* @return string
*/
public function getLayout(): string
{
return $this->layout;
}
/**
* @param $layout
*
* @return void
*/
public function setLayout(string $layout): void
{
$this->layout = $layout;
}
/**
* @return int
*/
public function getCols(): int
{
return $this->cols;
}
/**
* @param $cols
*
* @return void
*/
public function setCols(int $cols): void
{
$this->cols = $cols;
}
/**
* @return string
*/
public function getSubheader(): string
{
return $this->subheader;
}
/**
* @param $subheader
*
* @return void
*/
public function setSubheader(string $subheader): void
{
$this->subheader = $subheader;
}
/**
* @return string
*/
public function getHeaderLink(): string
{
return $this->headerLink;
}
/**
* @param $headerLink
*
* @return void
*/
public function setHeaderLink(string $headerLink): void
{
$this->headerLink = $headerLink;
}
/**
* @return string
*/
public function getImageLink(): string
{
return $this->imageLink;
}
/**
* @param $imageLink
*
* @return void
*/
public function setImageLink(string $imageLink): void
{
$this->imageLink = $imageLink;
}
/**
* @return string
*/
public function getImageZoom(): string
{
return $this->imageZoom;
}
/**
* @param $imageZoom
*
* @return void
*/
public function setImageZoom(string $imageZoom): void
{
$this->imageZoom = $imageZoom;
}
/**
* @return string
*/
public function getAltText(): string
{
return $this->altText;
}
/**
* @param $altText
*
* @return void
*/
public function setAltText(string $altText): void
{
$this->altText = $altText;
}
/**
* @return string
*/
public function getTitleText(): string
{
return $this->titleText;
}
/**
* @param $titleText
*
* @return void
*/
public function setTitleText(string $titleText): void
{
$this->titleText = $titleText;
}
/**
* @return string
*/
public function getHeaderLayout(): string
{
return $this->headerLayout;
}
/**
* @param $headerLayout
*
* @return void
*/
public function setHeaderLayout(string $headerLayout): void
{
$this->headerLayout = $headerLayout;
}
/**
* @return string
*/
public function getListType(): string
{
return $this->listType;
}
/**
* @param string $listType
* @return void
*/
public function setListType(string $listType): void
{
$this->listType = $listType;
}
}

View File

@@ -0,0 +1,193 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
use GeorgRinger\News\Domain\Model\DemandInterface;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Abstract demanded repository
*/
abstract class AbstractDemandedRepository extends Repository implements DemandedRepositoryInterface
{
/**
* @var \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface
*/
protected $storageBackend;
/**
* @param \TYPO3\CMS\Extbase\Persistence\Generic\Storage\BackendInterface $storageBackend
*
* @return void
*/
public function injectStorageBackend(
BackendInterface $storageBackend
): void {
$this->storageBackend = $storageBackend;
}
/**
* Returns an array of constraints created from a given demand object.
*
* @param \TYPO3\CMS\Extbase\Persistence\QueryInterface $query
* @param DemandInterface $demand
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface[]
* @abstract
*/
abstract protected function createConstraintsFromDemand(
QueryInterface $query,
DemandInterface $demand
): array;
/**
* Returns an array of orderings created from a given demand object.
*
* @param DemandInterface $demand
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface[]
* @abstract
*/
abstract protected function createOrderingsFromDemand(DemandInterface $demand): array;
/**
* Returns the objects of this repository matching the demand.
*
* @param DemandInterface $demand
* @param bool $respectEnableFields
* @param bool $disableLanguageOverlayMode
*
* @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array
*/
public function findDemanded(DemandInterface $demand, $respectEnableFields = true, $disableLanguageOverlayMode = false)
{
$query = $this->generateQuery($demand, $respectEnableFields, $disableLanguageOverlayMode);
return $query->execute();
}
/**
* Returns the database query to get the matching result
*
* @param DemandInterface $demand
* @param bool $respectEnableFields
* @param bool $disableLanguageOverlayMode
* @return string
*/
public function findDemandedRaw(DemandInterface $demand, $respectEnableFields = true, $disableLanguageOverlayMode = false): string
{
$query = $this->generateQuery($demand, $respectEnableFields, $disableLanguageOverlayMode);
$versionInformation = GeneralUtility::makeInstance(Typo3Version::class);
if ($versionInformation->getMajorVersion() >= 11) {
$queryParser = GeneralUtility::makeInstance(Typo3DbQueryParser::class);
} else {
$queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
}
$queryBuilder = $queryParser->convertQueryToDoctrineQueryBuilder($query);
$queryParameters = $queryBuilder->getParameters();
$params = [];
foreach ($queryParameters as $key => $value) {
// prefix array keys with ':'
$params[':' . $key] = (is_numeric($value)) ? $value : "'" . $value . "'"; //all non numeric values have to be quoted
unset($params[$key]);
}
// replace placeholders with real values
$query = strtr($queryBuilder->getSQL(), $params);
return $query;
}
/**
* @param DemandInterface $demand
* @param bool $respectEnableFields
* @param bool $disableLanguageOverlayMode
* @return \TYPO3\CMS\Extbase\Persistence\QueryInterface
*/
protected function generateQuery(DemandInterface $demand, $respectEnableFields = true, $disableLanguageOverlayMode = false): \TYPO3\CMS\Extbase\Persistence\QueryInterface
{
$query = $this->createQuery();
$query->getQuerySettings()->setRespectStoragePage(false);
if ($disableLanguageOverlayMode) {
$query->getQuerySettings()->setLanguageOverlayMode(false);
}
$constraints = $this->createConstraintsFromDemand($query, $demand);
// Call hook functions for additional constraints
if ($hooks = $GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['Domain/Repository/AbstractDemandedRepository.php']['findDemanded'] ?? []) {
$params = [
'demand' => $demand,
'respectEnableFields' => &$respectEnableFields,
'query' => $query,
'constraints' => &$constraints,
];
foreach ($hooks as $reference) {
GeneralUtility::callUserFunction($reference, $params, $this);
}
}
if ($respectEnableFields === false) {
$query->getQuerySettings()->setIgnoreEnableFields(true);
$constraints[] = $query->equals('deleted', 0);
}
if (!empty($constraints)) {
$query->matching(
$query->logicalAnd($constraints)
);
}
if ($orderings = $this->createOrderingsFromDemand($demand)) {
$query->setOrderings($orderings);
}
// @todo consider moving this to a separate function as well
if ($demand->getLimit() != null) {
$query->setLimit((int)$demand->getLimit());
}
// @todo consider moving this to a separate function as well
if ($demand->getOffset() != null) {
if (!$query->getLimit()) {
$query->setLimit(PHP_INT_MAX);
}
$query->setOffset((int)$demand->getOffset());
}
return $query;
}
/**
* Returns the total number objects of this repository matching the demand.
*
* @param DemandInterface $demand
* @return int
*/
public function countDemanded(DemandInterface $demand, $respectEnableFields = true, $disableLanguageOverlayMode = false): int
{
$query = $this->generateQuery($demand, $respectEnableFields, $disableLanguageOverlayMode);
if ($constraints = $this->createConstraintsFromDemand($query, $demand)) {
$query->matching(
$query->logicalAnd($constraints)
);
}
$result = $query->execute();
return $result->count();
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Get data used in the administration view
*/
class AdministrationRepository
{
public function getTotalCounts(): array
{
$count = [];
$queryBuilder = $this->getQueryBuilder('tx_news_domain_model_news');
$queryBuilder->getRestrictions()->removeAll();
$count['tx_news_domain_model_news'] = $queryBuilder
->count('*')
->from('tx_news_domain_model_news')
->execute()->fetchColumn(0);
$queryBuilder = $this->getQueryBuilder('sys_category_record_mm');
$count['category_relations'] = $queryBuilder
->count('*')
->from('sys_category_record_mm')
->where($queryBuilder->expr()->like(
'tablenames',
$queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)
))
->execute()->fetchColumn(0);
if ($count['tx_news_domain_model_news'] > 0 && $count['category_relations']) {
$count['_both'] = true;
}
return $count;
}
private function getConnection(string $table): Connection
{
return GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
}
private function getQueryBuilder(string $table): QueryBuilder
{
return $this->getConnection($table)->createQueryBuilder();
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use Doctrine\DBAL\Connection;
use GeorgRinger\News\Domain\Model\Category;
use GeorgRinger\News\Domain\Model\DemandInterface;
use GeorgRinger\News\Service\CategoryService;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* Category repository with all callable functionality
*/
class CategoryRepository extends AbstractDemandedRepository
{
protected function createConstraintsFromDemand(QueryInterface $query, DemandInterface $demand): array
{
return [];
}
protected function createOrderingsFromDemand(DemandInterface $demand): array
{
return [];
}
/**
* Find category by import source and import id
*
* @param string $importSource import source
* @param int $importId import id
* @param bool $asArray return result as array
* @return Category|array
*/
public function findOneByImportSourceAndImportId($importSource, $importId, $asArray = false)
{
$query = $this->createQuery();
$query->getQuerySettings()->setRespectStoragePage(false);
$query->getQuerySettings()->setIgnoreEnableFields(true);
$result = $query->matching(
$query->logicalAnd(
$query->equals('importSource', $importSource),
$query->equals('importId', $importId)
)
)->execute($asArray);
if ($asArray) {
if (isset($result[0])) {
return $result[0];
}
return [];
}
return $result->getFirst();
}
/**
* Find categories by a given pid
*
* @param int $pid pid
*
* @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array
*/
public function findParentCategoriesByPid($pid)
{
$query = $this->createQuery();
$query->getQuerySettings()->setRespectStoragePage(false);
return $query->matching(
$query->logicalAnd(
$query->equals('pid', (int)$pid),
$query->equals('parentcategory', 0)
)
)->execute();
}
/**
* Find category tree
*
* @param array $rootIdList list of id s
* @return QueryInterface|array
*/
public function findTree(array $rootIdList, $startingPoint = null)
{
$subCategories = CategoryService::getChildrenCategories(implode(',', $rootIdList));
$idList = explode(',', $subCategories);
if (empty($idList)) {
return [];
}
$ordering = ['sorting' => QueryInterface::ORDER_ASCENDING];
$categories = $this->findByIdList($idList, $ordering, $startingPoint);
$flatCategories = [];
/** @var Category $category */
foreach ($categories as $category) {
$flatCategories[$category->getUid()] = [
'item' => $category,
'parent' => ($category->getParentcategory()) ? $category->getParentcategory()->getUid() : null
];
}
$tree = [];
// If leaves are selected without its parents selected, those are shown as parent
foreach ($flatCategories as $id => &$flatCategory) {
if (!isset($flatCategories[$flatCategory['parent']])) {
$flatCategory['parent'] = null;
}
}
foreach ($flatCategories as $id => &$node) {
if ($node['parent'] === null) {
$tree[$id] = &$node;
} else {
$flatCategories[$node['parent']]['children'][$id] = &$node;
}
}
return $tree;
}
/**
* Find categories by a given pid
*
* @param array $idList list of id s
* @param array $ordering ordering
*
* @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array
*/
public function findByIdList(array $idList, array $ordering = [], $startingPoint = null)
{
if (empty($idList)) {
throw new \InvalidArgumentException('The given id list is empty.', 1484823597);
}
$query = $this->createQuery();
$query->getQuerySettings()->setRespectStoragePage(false);
$query->getQuerySettings()->setRespectSysLanguage(true);
if (count($ordering) > 0) {
$query->setOrderings($ordering);
}
$this->overlayTranslatedCategoryIds($idList);
$conditions = [];
$conditions[] = $query->in('uid', $idList);
if (is_null($startingPoint) === false) {
$conditions[] = $query->in('pid', GeneralUtility::trimExplode(',', $startingPoint, true));
}
return $query->matching(
$query->logicalAnd(
$conditions
)
)->execute();
}
/**
* Find categories by a given parent
*
* @param int $parent parent
*
* @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array
*/
public function findChildren($parent)
{
$query = $this->createQuery();
$query->getQuerySettings()->setRespectStoragePage(false);
return $query->matching(
$query->logicalAnd(
$query->equals('parentcategory', (int)$parent)
)
)->execute();
}
/**
* Overlay the category ids with the ones from current language
*
* @param array $idList
* return void
*
* @return void
*/
protected function overlayTranslatedCategoryIds(array &$idList): void
{
$language = $this->getSysLanguageUid();
if ($language > 0 && !empty($idList)) {
if (isset($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE'])) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$rows = $queryBuilder
->select('l10n_parent', 'uid', 'sys_language_uid')
->from('sys_category')
->where(
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)),
$queryBuilder->expr()->in('l10n_parent', $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY))
)
->execute()->fetchAll();
$idList = $this->replaceCategoryIds($idList, $rows);
}
// @todo currently only implemented for the frontend
}
}
/**
* Get the current sys language uid
*
* @return int
*/
protected function getSysLanguageUid(): int
{
$sysLanguage = 0;
if (GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion() === 10) {
$sysLanguage = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('language', 'id');
} elseif (isset($GLOBALS['TSFE']) && is_object($GLOBALS['TSFE'])) {
$sysLanguage = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('language', 'contentId');
} elseif ((int)GeneralUtility::_GP('L')) {
$sysLanguage = (int)GeneralUtility::_GP('L');
}
return $sysLanguage;
}
/**
* Replace ids in array by the given ones
*
* @param array $idList
* @param array $rows
* @return array
*/
protected function replaceCategoryIds(array $idList, array $rows): array
{
foreach ($rows as $row) {
$pos = array_search($row['l10n_parent'], $idList);
if ($pos !== false) {
$idList[$pos] = (int)$row['uid'];
}
}
return $idList;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\DemandInterface;
/**
* Demand domain model interface
*/
interface DemandedRepositoryInterface
{
public function findDemanded(DemandInterface $demand, $respectEnableFields = true);
public function countDemanded(DemandInterface $demand);
}

View File

@@ -0,0 +1,18 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
use TYPO3\CMS\Extbase\Persistence\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Repository for file objects
*/
class FileRepository extends Repository
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
use TYPO3\CMS\Extbase\Persistence\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Repository for link objects
*/
class LinkRepository extends Repository
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
use TYPO3\CMS\Extbase\Persistence\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Repository for media objects
*/
class MediaRepository extends Repository
{
}

View File

@@ -0,0 +1,17 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* News repository with all the callable functionality
*/
class NewsDefaultRepository extends NewsRepository
{
}

View File

@@ -0,0 +1,519 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\DemandInterface;
use GeorgRinger\News\Domain\Model\Dto\NewsDemand;
use GeorgRinger\News\Service\CategoryService;
use GeorgRinger\News\Utility\ConstraintHelper;
use GeorgRinger\News\Utility\Validation;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* News repository with all the callable functionality
*/
class NewsRepository extends AbstractDemandedRepository
{
/**
* Returns a category constraint created by
* a given list of categories and a junction string
*
* @param QueryInterface $query
* @param array $categories
* @param string $conjunction
* @param bool $includeSubCategories
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface|null
*/
protected function createCategoryConstraint(
QueryInterface $query,
$categories,
$conjunction,
$includeSubCategories = false
): ?\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface {
$constraint = null;
$categoryConstraints = [];
// If "ignore category selection" is used, nothing needs to be done
if (empty($conjunction)) {
return $constraint;
}
if (!is_array($categories)) {
$categories = GeneralUtility::intExplode(',', $categories, true);
}
foreach ($categories as $category) {
if ($includeSubCategories) {
$subCategories = GeneralUtility::trimExplode(
',',
CategoryService::getChildrenCategories($category, 0, '', true),
true
);
$subCategoryConstraint = [];
$subCategoryConstraint[] = $query->contains('categories', $category);
if (count($subCategories) > 0) {
foreach ($subCategories as $subCategory) {
$subCategoryConstraint[] = $query->contains('categories', $subCategory);
}
}
if ($subCategoryConstraint) {
$categoryConstraints[] = $query->logicalOr($subCategoryConstraint);
}
} else {
$categoryConstraints[] = $query->contains('categories', $category);
}
}
if ($categoryConstraints) {
switch (strtolower($conjunction)) {
case 'or':
$constraint = $query->logicalOr($categoryConstraints);
break;
case 'notor':
$constraint = $query->logicalNot($query->logicalOr($categoryConstraints));
break;
case 'notand':
$constraint = $query->logicalNot($query->logicalAnd($categoryConstraints));
break;
case 'and':
default:
$constraint = $query->logicalAnd($categoryConstraints);
}
}
return $constraint;
}
/**
* Returns an array of constraints created from a given demand object.
*
* @param QueryInterface $query
* @param DemandInterface $demand
*
* @throws \UnexpectedValueException
* @throws \InvalidArgumentException
* @throws \Exception
*
* @return (\TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface|\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface|\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface|\TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface|\TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface|null)[]
*
* @psalm-return array<string, \TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface|\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface|\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface|\TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface|\TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface|null>
*/
protected function createConstraintsFromDemand(QueryInterface $query, DemandInterface $demand): array
{
/** @var NewsDemand $demand */
$constraints = [];
if ($demand->getCategories() && $demand->getCategories() !== '0') {
$constraints['categories'] = $this->createCategoryConstraint(
$query,
$demand->getCategories(),
$demand->getCategoryConjunction(),
$demand->getIncludeSubCategories()
);
}
if ($demand->getAuthor()) {
$constraints['author'] = $query->equals('author', $demand->getAuthor());
}
if ($demand->getTypes()) {
$constraints['types'] = $query->in('type', $demand->getTypes());
}
// archived
if ($demand->getArchiveRestriction() === 'archived') {
$constraints['archived'] = $query->logicalAnd(
$query->lessThan('archive', $GLOBALS['SIM_EXEC_TIME']),
$query->greaterThan('archive', 0)
);
} elseif ($demand->getArchiveRestriction() === 'active') {
$constraints['active'] = $query->logicalOr(
$query->greaterThanOrEqual('archive', $GLOBALS['SIM_EXEC_TIME']),
$query->equals('archive', 0)
);
}
// Time restriction greater than or equal
$timeRestrictionField = $demand->getDateField();
$timeRestrictionField = (empty($timeRestrictionField)) ? 'datetime' : $timeRestrictionField;
if ($demand->getTimeRestriction()) {
$timeLimit = ConstraintHelper::getTimeRestrictionLow($demand->getTimeRestriction());
$constraints['timeRestrictionGreater'] = $query->greaterThanOrEqual(
$timeRestrictionField,
$timeLimit
);
}
// Time restriction less than or equal
if ($demand->getTimeRestrictionHigh()) {
$timeLimit = ConstraintHelper::getTimeRestrictionHigh($demand->getTimeRestrictionHigh());
$constraints['timeRestrictionLess'] = $query->lessThanOrEqual(
$timeRestrictionField,
$timeLimit
);
}
// top news
if ($demand->getTopNewsRestriction() == 1) {
$constraints['topNews1'] = $query->equals('istopnews', 1);
} elseif ($demand->getTopNewsRestriction() == 2) {
$constraints['topNews2'] = $query->equals('istopnews', 0);
}
// storage page
if ($demand->getStoragePage()) {
$pidList = GeneralUtility::intExplode(',', $demand->getStoragePage(), true);
$constraints['pid'] = $query->in('pid', $pidList);
}
// month & year OR year only
if ($demand->getYear() > 0) {
if (null === $demand->getDateField()) {
throw new \InvalidArgumentException('No Datefield is set, therefore no Datemenu is possible!');
}
if ($demand->getMonth() > 0) {
if ($demand->getDay() > 0) {
$begin = mktime(0, 0, 0, $demand->getMonth(), $demand->getDay(), $demand->getYear());
$end = mktime(23, 59, 59, $demand->getMonth(), $demand->getDay(), $demand->getYear());
} else {
$begin = mktime(0, 0, 0, $demand->getMonth(), 1, $demand->getYear());
$end = mktime(23, 59, 59, ($demand->getMonth() + 1), 0, $demand->getYear());
}
} else {
$begin = mktime(0, 0, 0, 1, 1, $demand->getYear());
$end = mktime(23, 59, 59, 12, 31, $demand->getYear());
}
$constraints['datetime'] = $query->logicalAnd([
$query->greaterThanOrEqual($demand->getDateField(), $begin),
$query->lessThanOrEqual($demand->getDateField(), $end)
]);
}
// Tags
$tags = $demand->getTags();
if ($tags && is_string($tags)) {
$tagList = explode(',', $tags);
$subConstraints = [];
foreach ($tagList as $singleTag) {
$subConstraints[] = $query->contains('tags', $singleTag);
}
if (count($subConstraints) > 0) {
$constraints['tags'] = $query->logicalOr($subConstraints);
}
}
// Search
$searchConstraints = $this->getSearchConstraints($query, $demand);
if (!empty($searchConstraints)) {
$constraints['search'] = $query->logicalAnd($searchConstraints);
}
// Exclude already displayed
if ($demand->getExcludeAlreadyDisplayedNews() && isset($GLOBALS['EXT']['news']['alreadyDisplayed']) && !empty($GLOBALS['EXT']['news']['alreadyDisplayed'])) {
$constraints['excludeAlreadyDisplayedNews'] = $query->logicalNot(
$query->in(
'uid',
$GLOBALS['EXT']['news']['alreadyDisplayed']
)
);
}
// Hide id list
$hideIdList = $demand->getHideIdList();
if ($hideIdList) {
$constraints['hideIdInList'] = $query->logicalNot(
$query->in(
'uid',
GeneralUtility::intExplode(',', $hideIdList, true)
)
);
}
// Id list
$idList = $demand->getIdList();
if ($idList) {
$constraints['idList'] = $query->in('uid', GeneralUtility::intExplode(',', $idList, true));
}
// Clean not used constraints
foreach ($constraints as $key => $value) {
if (null === $value) {
unset($constraints[$key]);
}
}
return $constraints;
}
/**
* Returns an array of orderings created from a given demand object.
*
* @param DemandInterface $demand
*
* @return string[]
*
* @psalm-return array<string, string>
*/
protected function createOrderingsFromDemand(DemandInterface $demand): array
{
$orderings = [];
if ($demand->getTopNewsFirst()) {
$orderings['istopnews'] = QueryInterface::ORDER_DESCENDING;
}
if (Validation::isValidOrdering($demand->getOrder(), $demand->getOrderByAllowed())) {
$orderList = GeneralUtility::trimExplode(',', $demand->getOrder(), true);
if (!empty($orderList)) {
// go through every order statement
foreach ($orderList as $orderItem) {
$orderSplit = GeneralUtility::trimExplode(' ', $orderItem, true);
$orderField = $orderSplit[0];
$ascDesc = $orderSplit[1] ?? '';
if ($ascDesc) {
$orderings[$orderField] = ((strtolower($ascDesc) === 'desc') ?
QueryInterface::ORDER_DESCENDING :
QueryInterface::ORDER_ASCENDING);
} else {
$orderings[$orderField] = QueryInterface::ORDER_ASCENDING;
}
}
}
}
return $orderings;
}
/**
* Find first news by import and source id
*
* @param string $importSource import source
* @param int $importId import id
* @param bool $asArray return result as array
* @return \GeorgRinger\News\Domain\Model\News|array
*/
public function findOneByImportSourceAndImportId($importSource, $importId, $asArray = false)
{
$query = $this->createQuery();
$query->getQuerySettings()->setRespectStoragePage(false);
$query->getQuerySettings()->setRespectSysLanguage(false);
$query->getQuerySettings()->setIgnoreEnableFields(true);
$result = $query->matching(
$query->logicalAnd(
$query->equals('importSource', $importSource),
$query->equals('importId', $importId)
)
)->execute($asArray);
if ($asArray) {
if (isset($result[0])) {
return $result[0];
}
return [];
}
return $result->getFirst();
}
/**
* Override default findByUid function to enable also the option to turn of
* the enableField setting
*
* @param int $uid id of record
* @param bool $respectEnableFields if set to false, hidden records are shown
* @return \GeorgRinger\News\Domain\Model\News|null
*/
public function findByUid($uid, $respectEnableFields = true): ?\GeorgRinger\News\Domain\Model\News
{
$query = $this->createQuery();
$query->getQuerySettings()->setRespectStoragePage(false);
$query->getQuerySettings()->setRespectSysLanguage(false);
if (!$respectEnableFields) {
$query->getQuerySettings()->setIgnoreEnableFields(true);
$query->getQuerySettings()->setLanguageOverlayMode(false);
}
return $query->matching(
$query->logicalAnd(
$query->equals('uid', $uid),
$query->equals('deleted', 0)
)
)->execute()->getFirst();
}
/**
* Get the count of news records by month/year and
* returns the result compiled as array
*
* @param DemandInterface $demand
* @return array
*/
public function countByDate(DemandInterface $demand): array
{
$data = [];
$sql = $this->findDemandedRaw($demand);
// strip unwanted order by
$sql = $this->stripOrderBy($sql);
// Get the month/year into the result
$field = $demand->getDateField();
$field = empty($field) ? 'datetime' : $field;
$sql = 'SELECT MONTH(FROM_UNIXTIME(0) + INTERVAL ' . $field . ' SECOND ) AS "_Month",' .
' YEAR(FROM_UNIXTIME(0) + INTERVAL ' . $field . ' SECOND) AS "_Year" ,' .
' count(MONTH(FROM_UNIXTIME(0) + INTERVAL ' . $field . ' SECOND )) as count_month,' .
' count(YEAR(FROM_UNIXTIME(0) + INTERVAL ' . $field . ' SECOND)) as count_year' .
' FROM tx_news_domain_model_news ' . substr($sql, strpos($sql, 'WHERE '));
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_news_domain_model_news');
if (TYPO3_MODE === 'FE') {
$sql .= $GLOBALS['TSFE']->sys_page->enableFields('tx_news_domain_model_news');
} else {
$expressionBuilder = $connection
->createQueryBuilder()
->expr();
$sql .= BackendUtility::BEenableFields('tx_news_domain_model_news') .
' AND ' . $expressionBuilder->eq('deleted', 0);
}
// group by custom month/year fields
$orderDirection = strtolower($demand->getOrder());
if ($orderDirection !== 'desc' && $orderDirection !== 'asc') {
$orderDirection = 'asc';
}
$sql .= ' GROUP BY _Month, _Year ORDER BY _Year ' . $orderDirection . ', _Month ' . $orderDirection;
$res = $connection->query($sql);
while ($row = $res->fetch()) {
$month = strlen($row['_Month']) === 1 ? ('0' . $row['_Month']) : $row['_Month'];
$data['single'][$row['_Year']][$month] = $row['count_month'];
}
// Add totals
if (is_array($data['single'])) {
foreach ($data['single'] as $year => $months) {
$countOfYear = 0;
foreach ($months as $month) {
$countOfYear += $month;
}
$data['total'][$year] = $countOfYear;
}
}
return $data;
}
/**
* Get the search constraints
*
* @param QueryInterface $query
* @param DemandInterface $demand
* @return array
* @throws \UnexpectedValueException
*/
protected function getSearchConstraints(QueryInterface $query, DemandInterface $demand): array
{
$constraints = [];
if ($demand->getSearch() === null) {
return $constraints;
}
/* @var $searchObject \GeorgRinger\News\Domain\Model\Dto\Search */
$searchObject = $demand->getSearch();
$searchSubject = $searchObject->getSubject();
if (!empty($searchSubject)) {
$queryBuilder = $this->getQueryBuilder('tx_news_domain_model_news');
$searchFields = GeneralUtility::trimExplode(',', $searchObject->getFields(), true);
$searchConstraints = [];
if (count($searchFields) === 0) {
throw new \UnexpectedValueException('No search fields defined', 1318497755);
}
$searchSubjectSplitted = str_getcsv($searchSubject, ' ');
if ($searchObject->isSplitSubjectWords()) {
foreach ($searchFields as $field) {
$subConstraints = [];
foreach ($searchSubjectSplitted as $searchSubjectSplittedPart) {
$searchSubjectSplittedPart = trim($searchSubjectSplittedPart);
if ($searchSubjectSplittedPart) {
$subConstraints[] = $query->like($field, '%' . $searchSubjectSplittedPart . '%');
}
}
$searchConstraints[] = $query->logicalAnd($subConstraints);
}
if (count($searchConstraints)) {
$constraints[] = $query->logicalOr($searchConstraints);
}
} else {
if (!empty($searchSubject)) {
foreach ($searchFields as $field) {
$searchConstraints[] = $query->like($field, '%' . $searchSubject . '%');
}
}
if (count($searchConstraints)) {
$constraints[] = $query->logicalOr($searchConstraints);
}
}
}
$minimumDate = strtotime($searchObject->getMinimumDate());
if ($minimumDate) {
$field = $searchObject->getDateField();
if (empty($field)) {
throw new \UnexpectedValueException('No date field is defined', 1396348732);
}
$constraints[] = $query->greaterThanOrEqual($field, $minimumDate);
}
$maximumDate = strtotime($searchObject->getMaximumDate());
if ($maximumDate) {
$field = $searchObject->getDateField();
if (empty($field)) {
throw new \UnexpectedValueException('No date field is defined', 1396348733);
}
$maximumDate += 86400;
$constraints[] = $query->lessThanOrEqual($field, $maximumDate);
}
return $constraints;
}
/**
* @param string $table table name
* @return QueryBuilder
*/
protected function getQueryBuilder(string $table): QueryBuilder
{
return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
}
/**
* Return stripped order sql
*
* @param string $str
* @return string
*/
private function stripOrderBy(string $str): string
{
/** @noinspection NotOptimalRegularExpressionsInspection */
return preg_replace('/(?:ORDER[[:space:]]*BY[[:space:]]*.*)+/i', '', trim($str));
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\DemandInterface;
use GeorgRinger\News\Utility\Validation;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
/**
* Repository for tag objects
*/
class TagRepository extends AbstractDemandedRepository
{
/**
* Find categories by a given pid
*
* @param array $idList list of id s
* @param array $ordering ordering
* @param string $startingPoint starting point uid or comma separated list
*
* @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array
*/
public function findByIdList(array $idList, array $ordering = [], $startingPoint = null)
{
if (empty($idList)) {
throw new \InvalidArgumentException('The given id list is empty.', 1484823596);
}
$query = $this->createQuery();
$query->getQuerySettings()->setRespectStoragePage(false);
$query->getQuerySettings()->setRespectSysLanguage(false);
if (count($ordering) > 0) {
$query->setOrderings($ordering);
}
$conditions = [];
$conditions[] = $query->in('uid', $idList);
if ($startingPoint !== null) {
$conditions[] = $query->in('pid', GeneralUtility::trimExplode(',', $startingPoint, true));
}
return $query->matching(
$query->logicalAnd(
$conditions
)
)->execute();
}
/**
* Returns an array of constraints created from a given demand object.
*
* @param QueryInterface $query
* @param DemandInterface $demand
* @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface[]
*/
protected function createConstraintsFromDemand(QueryInterface $query, DemandInterface $demand): array
{
$constraints = [];
// Storage page
if ($demand->getStoragePage()) {
$pidList = GeneralUtility::intExplode(',', $demand->getStoragePage(), true);
$constraints[] = $query->in('pid', $pidList);
}
// Tags
if ($demand->getTags()) {
$tagList = GeneralUtility::intExplode(',', $demand->getTags(), true);
$constraints[] = $query->in('uid', $tagList);
}
// Clean not used constraints
foreach ($constraints as $key => $value) {
if (is_null($value)) {
unset($constraints[$key]);
}
}
return $constraints;
}
/**
* Returns an array of orderings created from a given demand object.
*
* @param DemandInterface $demand
*
* @return string[]
*
* @psalm-return array<string, string>
*/
protected function createOrderingsFromDemand(DemandInterface $demand): array
{
$orderings = [];
if (Validation::isValidOrdering($demand->getOrder(), $demand->getOrderByAllowed())) {
$orderList = GeneralUtility::trimExplode(',', $demand->getOrder(), true);
if (!empty($orderList)) {
// go through every order statement
foreach ($orderList as $orderItem) {
list($orderField, $ascDesc) = GeneralUtility::trimExplode(' ', $orderItem, true);
// count == 1 means that no direction is given
if ($ascDesc) {
$orderings[$orderField] = ((strtolower($ascDesc) == 'desc') ?
QueryInterface::ORDER_DESCENDING :
QueryInterface::ORDER_ASCENDING);
} else {
$orderings[$orderField] = QueryInterface::ORDER_ASCENDING;
}
}
}
}
return $orderings;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace GeorgRinger\News\Domain\Repository;
use TYPO3\CMS\Extbase\Persistence\Repository;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Repository for tt_content objects
*/
class TtContentRepository extends Repository
{
protected $objectType = '\GeorgRinger\News\Domain\Model\Ttcontent';
}

View File

@@ -0,0 +1,169 @@
<?php
namespace GeorgRinger\News\Domain\Service;
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use GeorgRinger\News\Domain\Repository\CategoryRepository;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
class AbstractImportService implements LoggerAwareInterface
{
use LoggerAwareTrait;
const UPLOAD_PATH = 'uploads/tx_news/';
/**
* @var ObjectManager
*/
protected $objectManager;
/**
* @var PersistenceManager
*/
protected $persistenceManager;
/**
* @var array
*/
protected $postPersistQueue = [];
/**
* @var EmConfiguration
*/
protected $emSettings;
/**
* @var Folder
*/
protected $importFolder;
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* @var CategoryRepository
*/
protected $categoryRepository;
/**
* AbstractImportService constructor.
* @param PersistenceManager $persistenceManager
* @param EmConfiguration $emSettings
* @param ObjectManager $objectManager
* @param CategoryRepository $categoryRepository
* @param EventDispatcherInterface $eventDispatcher
*/
public function __construct(
PersistenceManager $persistenceManager,
ObjectManager $objectManager,
CategoryRepository $categoryRepository,
EventDispatcherInterface $eventDispatcher
) {
$this->emSettings = GeneralUtility::makeInstance(EmConfiguration::class);
$this->persistenceManager = $persistenceManager;
$this->objectManager = $objectManager;
$this->categoryRepository = $categoryRepository;
$this->eventDispatcher = $eventDispatcher;
}
/**
* Compares 2 files by using its filesize
*
* @param string $file1 Absolute path and filename to file1
* @param string $file2 Absolute path and filename to file2
* @return bool
*/
protected function filesAreEqual($file1, $file2): bool
{
return filesize($file1) === filesize($file2);
}
/**
* Find a existing file by its hash
*
* @param string $hash
*
* @return File|ProcessedFile|null
*/
protected function findFileByHash($hash)
{
$file = null;
$files = $this->getFileIndexRepository()->findByContentHash($hash);
if (count($files)) {
foreach ($files as $fileInfo) {
if ($fileInfo['storage'] > 0) {
$file = $this->getResourceFactory()->getFileObjectByStorageAndIdentifier(
$fileInfo['storage'],
$fileInfo['identifier']
);
break;
}
}
}
return $file;
}
/**
* Get import Folder
*
* TODO: catch exception when storage/folder does not exist and return readable message to the user
*
* @return Folder
*/
protected function getImportFolder(): Folder
{
if ($this->importFolder === null) {
$this->importFolder = $this->getResourceFactory()->getFolderObjectFromCombinedIdentifier($this->emSettings->getStorageUidImporter() . ':' . $this->emSettings->getResourceFolderImporter());
}
return $this->importFolder;
}
/**
* Returns an instance of the FileIndexRepository
*
* @return FileIndexRepository
*/
protected function getFileIndexRepository(): FileIndexRepository
{
return FileIndexRepository::getInstance();
}
/**
* Get resource storage
*
* @return ResourceStorage
*/
protected function getResourceStorage(): ResourceStorage
{
return $this->getResourceFactory()->getStorageObject($this->emSettings->getStorageUidImporter());
}
/**
* @return ResourceFactory
*/
protected function getResourceFactory(): ResourceFactory
{
/** @var ResourceFactory $resourceFactory */
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
return $resourceFactory;
}
}

View File

@@ -0,0 +1,263 @@
<?php
namespace GeorgRinger\News\Domain\Service;
use GeorgRinger\News\Domain\Model\Category;
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use GeorgRinger\News\Domain\Model\FileReference;
use GeorgRinger\News\Domain\Repository\CategoryRepository;
use GeorgRinger\News\Event\CategoryImportPostHydrateEvent;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
class CategoryImportService extends AbstractImportService
{
const ACTION_SET_PARENT_CATEGORY = 1;
const ACTION_CREATE_L10N_CHILDREN_CATEGORY = 2;
/**
* CategoryImportService constructor.
* @param PersistenceManager $persistenceManager
* @param EmConfiguration $emSettings
* @param ObjectManager $objectManager
* @param CategoryRepository $categoryRepository
* @param EventDispatcherInterface $eventDispatcher
*/
public function __construct(
PersistenceManager $persistenceManager,
ObjectManager $objectManager,
CategoryRepository $categoryRepository,
EventDispatcherInterface $eventDispatcher
) {
parent::__construct($persistenceManager, $objectManager, $categoryRepository, $eventDispatcher);
}
/**
* @param array $importArray
*
* @return void
*/
public function import(array $importArray): void
{
$this->logger->info(sprintf('Starting import for %s categories', count($importArray)));
// Sort import array to import the default language first
foreach ($importArray as $importItem) {
$category = $this->hydrateCategory($importItem);
if (!empty($importItem['title_lang_ol'])) {
$this->postPersistQueue[$importItem['import_id']] = [
'category' => $category,
'importItem' => $importItem,
'action' => self::ACTION_CREATE_L10N_CHILDREN_CATEGORY,
'titleLanguageOverlay' => $importItem['title_lang_ol']
];
}
if ($importItem['parentcategory']) {
$this->postPersistQueue[$importItem['import_id']] = [
'category' => $category,
'action' => self::ACTION_SET_PARENT_CATEGORY,
'parentCategoryOriginUid' => $importItem['parentcategory']
];
}
}
$this->persistenceManager->persistAll();
foreach ($this->postPersistQueue as $queueItem) {
switch ($queueItem['action']) {
case self::ACTION_SET_PARENT_CATEGORY:
$this->setParentCategory($queueItem);
break;
case self::ACTION_CREATE_L10N_CHILDREN_CATEGORY:
$this->createL10nChildrenCategory($queueItem);
break;
default:
// do nothing
break;
}
}
$this->persistenceManager->persistAll();
}
/**
* Hydrate a category record with the given array
*
* @param array $importItem
*
* @return array|object
*/
protected function hydrateCategory(array $importItem)
{
$category = $this->categoryRepository->findOneByImportSourceAndImportId(
$importItem['import_source'],
$importItem['import_id']
);
$this->logger->info(sprintf(
'Import of category from source "%s" with id "%s"',
$importItem['import_source'],
$importItem['import_id']
));
if (is_null($category)) {
$this->logger->info('Category is new');
$category = GeneralUtility::makeInstance(Category::class);
$this->categoryRepository->add($category);
} else {
$this->logger->info(sprintf('Category exists already with id "%s".', $category->getUid()));
}
$category->setPid($importItem['pid']);
$category->setHidden($importItem['hidden']);
$category->setStarttime($importItem['starttime']);
$category->setEndtime($importItem['endtime']);
$category->setCrdate($importItem['crdate']);
$category->setTstamp($importItem['tstamp']);
$category->setTitle($importItem['title']);
$category->setDescription($importItem['description']);
if (!empty($importItem['image'])) {
$this->setFileRelationFromImage($category, $importItem['image']);
}
$category->setShortcut($importItem['shortcut']);
$category->setSinglePid($importItem['single_pid']);
$category->setImportId($importItem['import_id']);
$category->setImportSource($importItem['import_source']);
$event = $this->eventDispatcher->dispatch(new CategoryImportPostHydrateEvent($this, $importItem, $category));
return $event->getCategory();
}
/**
* Add category image when not already present
*
* @param Category $category
* @param $image
*
* @return void
*/
protected function setFileRelationFromImage($category, $image)
{
// get fileObject by given identifier (file UID, combined identifier or path/filename)
try {
$newImage = $this->getResourceFactory()->retrieveFileOrFolderObject($image);
} catch (ResourceDoesNotExistException $exception) {
$newImage = false;
}
// only proceed if image is found
if (!$newImage instanceof File) {
return;
}
// new image found check if this isn't already
$existingImages = $category->getImages();
if (!is_null($existingImages) && $existingImages->count() !== 0) {
/** @var $item FileReference */
foreach ($existingImages as $item) {
// only check already persisted items
if ($item->getFileUid() === (int)$newImage->getUid()
||
($item->getUid() &&
$item->getOriginalResource()->getName() === $newImage->getName() &&
$item->getOriginalResource()->getSize() === (int)$newImage->getSize())
) {
$newImage = false;
break;
}
}
}
if ($newImage) {
// file not inside a storage then search for existing file or copy the one form storage 0 to the import folder
if ($newImage->getStorage()->getUid() === 0) {
// search DB for same file based on hash (to prevent duplicates)
$existingFile = $this->findFileByHash($newImage->getSha1());
// no exciting file then copy file to import folder
if ($existingFile === null) {
$newImage = $this->getResourceStorage()->copyFile($newImage, $this->getImportFolder());
} else {
$newImage = $existingFile;
}
}
$fileReference = GeneralUtility::makeInstance(FileReference::class);
$fileReference->setFileUid($newImage->getUid());
$fileReference->setPid($category->getPid());
$category->addImage($fileReference);
}
}
/**
* Set parent category
*
* @param array $queueItem
*
* @return void
*/
protected function setParentCategory(array $queueItem): void
{
/** @var $category Category */
$category = $queueItem['category'];
$parentCategoryOriginUid = $queueItem['parentCategoryOriginUid'];
if (is_null($parentCategory = $this->postPersistQueue[$parentCategoryOriginUid]['category'])) {
$parentCategory = $this->categoryRepository->findOneByImportSourceAndImportId(
$category->getImportSource(),
$parentCategoryOriginUid
);
}
if ($parentCategory !== null) {
$category->setParentcategory($parentCategory);
$this->categoryRepository->update($category);
}
}
/**
* Create l10n relation
*
* @param array $queueItem
*
* @return void
*/
protected function createL10nChildrenCategory(array $queueItem): void
{
/** @var $category Category */
$category = $queueItem['category'];
$titleLanguageOverlay = GeneralUtility::trimExplode('|', $queueItem['titleLanguageOverlay']);
foreach ($titleLanguageOverlay as $key => $title) {
$sysLanguageUid = $key + 1;
$importItem = $queueItem['importItem'];
$importItem['import_id'] = $importItem['import_id'] . '|L:' . $sysLanguageUid;
/** @var $l10nChildrenCategory Category */
$l10nChildrenCategory = $this->hydrateCategory($importItem);
$this->categoryRepository->add($l10nChildrenCategory);
$l10nChildrenCategory->setTitle($title);
$l10nChildrenCategory->setL10nParent((int)$category->getUid());
$l10nChildrenCategory->setSysLanguageUid((int)$sysLanguageUid);
}
}
}

View File

@@ -0,0 +1,411 @@
<?php
namespace GeorgRinger\News\Domain\Service;
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use GeorgRinger\News\Domain\Model\FileReference;
use GeorgRinger\News\Domain\Model\Link;
use GeorgRinger\News\Domain\Model\News;
use GeorgRinger\News\Domain\Repository\CategoryRepository;
use GeorgRinger\News\Domain\Repository\NewsRepository;
use GeorgRinger\News\Domain\Repository\TtContentRepository;
use GeorgRinger\News\Event\NewsImportPostHydrateEvent;
use GeorgRinger\News\Event\NewsImportPreHydrateEvent;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
class NewsImportService extends AbstractImportService
{
const ACTION_IMPORT_L10N_OVERLAY = 1;
/**
* @var NewsRepository
*/
protected $newsRepository;
/**
* @var TtContentRepository
*/
protected $ttContentRepository;
/**
* @var array
*/
protected $settings = [];
/**
* NewsImportService constructor.
* @param PersistenceManager $persistenceManager
* @param EmConfiguration $emSettings
* @param ObjectManager $objectManager
* @param CategoryRepository $categoryRepository
* @param EventDispatcherInterface $eventDispatcher
* @param NewsRepository $newsRepository
* @param TtContentRepository $ttContentRepository
*/
public function __construct(
PersistenceManager $persistenceManager,
ObjectManager $objectManager,
CategoryRepository $categoryRepository,
EventDispatcherInterface $eventDispatcher,
NewsRepository $newsRepository,
TtContentRepository $ttContentRepository
) {
parent::__construct($persistenceManager, $objectManager, $categoryRepository, $eventDispatcher);
$this->newsRepository = $newsRepository;
$this->ttContentRepository = $ttContentRepository;
}
/**
* @param array $importItem
*
* @return array|object
*/
protected function initializeNewsRecord(array $importItem)
{
$news = null;
$this->logger->info(sprintf(
'Import of news from source "%s" with id "%s"',
$importItem['import_source'],
$importItem['import_id']
));
if ($importItem['import_source'] && $importItem['import_id']) {
$news = $this->newsRepository->findOneByImportSourceAndImportId(
$importItem['import_source'],
$importItem['import_id']
);
}
if ($news === null) {
$news = GeneralUtility::makeInstance(News::class);
$this->newsRepository->add($news);
} else {
$this->logger->info(sprintf('News exists already with id "%s".', $news->getUid()));
$this->newsRepository->update($news);
}
return $news;
}
/**
* @param News $news
* @param array $importItem
* @param array $importItemOverwrite
* @return News
*/
protected function hydrateNewsRecord(
News $news,
array $importItem,
array $importItemOverwrite
): News {
if (!empty($importItemOverwrite)) {
$importItem = array_merge($importItem, $importItemOverwrite);
}
$news->setPid($importItem['pid']);
$news->setHidden($importItem['hidden']);
if ($importItem['starttime']) {
$news->setStarttime($importItem['starttime']);
}
if ($importItem['endtime']) {
$news->setStarttime($importItem['endtime']);
}
if (!empty($importItem['fe_group'])) {
$news->setFeGroup((string)$importItem['fe_group']);
}
$news->setTstamp($importItem['tstamp']);
$news->setCrdate($importItem['crdate']);
$news->setSysLanguageUid($importItem['sys_language_uid']);
$news->setSorting((int)$importItem['sorting']);
$news->setTitle($importItem['title']);
$news->setTeaser($importItem['teaser']);
$news->setBodytext($importItem['bodytext']);
$news->setType((string)$importItem['type']);
$news->setKeywords($importItem['keywords']);
$news->setDescription($importItem['description']);
$news->setDatetime(new \DateTime(date('Y-m-d H:i:sP', $importItem['datetime'])));
$news->setArchive(new \DateTime(date('Y-m-d H:i:sP', $importItem['archive'])));
$contentElementUidArray = GeneralUtility::trimExplode(',', $importItem['content_elements'], true);
foreach ($contentElementUidArray as $contentElementUid) {
if (is_object($contentElement = $this->ttContentRepository->findByUid($contentElementUid))) {
$news->addContentElement($contentElement);
}
}
$news->setInternalurl($importItem['internalurl']);
$news->setExternalurl($importItem['externalurl']);
$news->setAuthor($importItem['author']);
$news->setAuthorEmail($importItem['author_email']);
$news->setImportId($importItem['import_id']);
$news->setImportSource($importItem['import_source']);
$news->setPathSegment($importItem['path_segment']);
if (is_array($importItem['categories'])) {
foreach ($importItem['categories'] as $categoryUid) {
if ($this->settings['findCategoriesByImportSource']) {
$category = $this->categoryRepository->findOneByImportSourceAndImportId(
$this->settings['findCategoriesByImportSource'],
$categoryUid
);
} else {
$category = $this->categoryRepository->findByUid($categoryUid);
}
if ($category) {
$news->addCategory($category);
} else {
$this->logger->warning(sprintf('Category with ID "%s" was not found', $categoryUid));
}
}
}
// media relation
if (is_array($importItem['media'])) {
foreach ($importItem['media'] as $mediaItem) {
// get fileobject by given identifier (file UID, combined identifier or path/filename)
try {
$file = $this->getResourceFactory()->retrieveFileOrFolderObject($mediaItem['image']);
} catch (ResourceDoesNotExistException $exception) {
$file = null;
}
// no file found skip processing of this item
if ($file === null) {
continue;
}
// file not inside a storage then search for same file based on hash (to prevent duplicates)
if ($file->getStorage()->getUid() === 0) {
$existingFile = $this->findFileByHash($file->getSha1());
if ($existingFile !== null) {
$file = $existingFile;
}
}
/** @var $media FileReference */
if (!$media = $this->getIfFalRelationIfAlreadyExists($news->getFalMedia(), $file)) {
// file not inside a storage copy the one form storage 0 to the import folder
if ($file->getStorage()->getUid() === 0) {
$file = $this->getResourceStorage()->copyFile($file, $this->getImportFolder());
}
$media = GeneralUtility::makeInstance(FileReference::class);
$media->setFileUid($file->getUid());
$news->addFalMedia($media);
}
if ($media) {
$media->setTitle($mediaItem['title']);
$media->setAlternative($mediaItem['alt']);
$media->setDescription($mediaItem['caption']);
$media->setShowinpreview($mediaItem['showinpreview']);
$media->setPid($importItem['pid']);
}
}
}
// related files
if (is_array($importItem['related_files'])) {
foreach ($importItem['related_files'] as $fileItem) {
// get fileObject by given identifier (file UID, combined identifier or path/filename)
try {
$file = $this->getResourceFactory()->retrieveFileOrFolderObject($fileItem['file']);
} catch (ResourceDoesNotExistException $exception) {
$file = null;
}
// no file found skip processing of this item
if ($file === null) {
continue;
}
// file not inside a storage then search for same file based on hash (to prevent duplicates)
if ($file->getStorage()->getUid() === 0) {
$existingFile = $this->findFileByHash($file->getSha1());
if ($existingFile !== null) {
$file = $existingFile;
}
}
/** @var $relatedFile FileReference */
if (!$relatedFile = $this->getIfFalRelationIfAlreadyExists($news->getFalRelatedFiles(), $file)) {
// file not inside a storage copy the one form storage 0 to the import folder
if ($file->getStorage()->getUid() === 0) {
$file = $this->getResourceStorage()->copyFile($file, $this->getImportFolder());
}
$relatedFile = GeneralUtility::makeInstance(FileReference::class);
$relatedFile->setFileUid($file->getUid());
$news->addFalRelatedFile($relatedFile);
}
if ($relatedFile) {
$relatedFile->setTitle($fileItem['title']);
$relatedFile->setDescription($fileItem['description']);
$relatedFile->setPid($importItem['pid']);
}
}
}
if (is_array($importItem['related_links'])) {
foreach ($importItem['related_links'] as $link) {
/** @var $relatedLink Link */
if (($relatedLink = $this->getRelatedLinkIfAlreadyExists($news, $link['uri'])) === false) {
$relatedLink = GeneralUtility::makeInstance(Link::class);
$relatedLink->setUri($link['uri']);
$news->addRelatedLink($relatedLink);
}
$relatedLink->setTitle($link['title']);
$relatedLink->setDescription($link['description']);
$relatedLink->setPid($importItem['pid']);
}
}
$event = $this->eventDispatcher->dispatch(new NewsImportPostHydrateEvent($this, $importItem, $news));
return $event->getNews();
}
/**
* Import
*
* @param array $importData
* @param array $importItemOverwrite
* @param array $settings
*
* @return void
*/
public function import(array $importData, array $importItemOverwrite = [], $settings = []): void
{
$this->settings = $settings;
$this->logger->info(sprintf('Starting import for %s news', count($importData)));
foreach ($importData as $importItem) {
$event = $this->eventDispatcher->dispatch(new NewsImportPreHydrateEvent($this, $importItem));
$importItem = $event->getImportItem();
// Store language overlay in post persist queue
if ((int)$importItem['sys_language_uid'] > 0 && (string)$importItem['l10n_parent'] !== '0') {
$this->postPersistQueue[$importItem['import_id']] = [
'action' => self::ACTION_IMPORT_L10N_OVERLAY,
'category' => null,
'importItem' => $importItem
];
continue;
}
$news = $this->initializeNewsRecord($importItem);
$this->hydrateNewsRecord($news, $importItem, $importItemOverwrite);
}
$this->persistenceManager->persistAll();
foreach ($this->postPersistQueue as $queueItem) {
if ($queueItem['action'] == self::ACTION_IMPORT_L10N_OVERLAY) {
$this->importL10nOverlay($queueItem, $importItemOverwrite);
}
}
$this->persistenceManager->persistAll();
}
/**
* @param array $queueItem
* @param array $importItemOverwrite
*
* @return void
*/
protected function importL10nOverlay(array $queueItem, array $importItemOverwrite): void
{
$importItem = $queueItem['importItem'];
$parentNews = $this->newsRepository->findOneByImportSourceAndImportId(
$importItem['import_source'],
$importItem['l10n_parent'],
true
);
if (!empty($parentNews)) {
$news = $this->initializeNewsRecord($importItem);
$this->hydrateNewsRecord($news, $importItem, $importItemOverwrite);
$news->setSysLanguageUid($importItem['sys_language_uid']);
$news->setL10nParent($parentNews['uid']);
}
}
/**
* Get an existing items from the references that matches the file
*
* @param ObjectStorage $items
* @param \TYPO3\CMS\Core\Resource\File $file
*
* @return bool|FileReference
*/
protected function getIfFalRelationIfAlreadyExists(
ObjectStorage $items,
File $file
) {
$result = false;
if ($items->count() !== 0) {
/** @var $item FileReference */
foreach ($items as $item) {
// only check already persisted items
if ($item->getFileUid() === (int)$file->getUid()
||
($item->getUid() &&
$item->getOriginalResource()->getName() === $file->getName() &&
$item->getOriginalResource()->getSize() === (int)$file->getSize())
) {
$result = $item;
break;
}
}
}
return $result;
}
/**
* Get an existing related link object
*
* @param News $news
* @param string $uri
* @return bool|Link
*/
protected function getRelatedLinkIfAlreadyExists(News $news, $uri)
{
$result = false;
$links = $news->getRelatedLinks();
if (!empty($links) && $links->count() !== 0) {
foreach ($links as $link) {
if ($link->getUri() === $uri) {
$result = $link;
break;
}
}
}
return $result;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\AdministrationController;
use TYPO3\CMS\Backend\Template\Components\Menu\Menu;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class AdministrationExtendMenuEvent
{
/**
* @var AdministrationController
*/
private $administrationController;
/**
* @var Menu
*/
private $menu;
public function __construct(AdministrationController $administrationController, Menu $menu)
{
$this->administrationController = $administrationController;
$this->menu = $menu;
}
/**
* Get the administration controller
*/
public function getAdministrationController(): AdministrationController
{
return $this->administrationController;
}
/**
* Set the administration controller
*/
public function setAdministrationController(AdministrationController $administrationController): self
{
$this->administrationController = $administrationController;
return $this;
}
/**
* Get the menu
*/
public function getMenu(): Menu
{
return $this->menu;
}
/**
* Set the menu
*/
public function setMenu(Menu $menu): self
{
$this->menu = $menu;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\AdministrationController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class AdministrationIndexActionEvent
{
/**
* @var AdministrationController
*/
private $administrationController;
/**
* @var array
*/
private $assignedValues;
public function __construct(AdministrationController $administrationController, array $assignedValues)
{
$this->administrationController = $administrationController;
$this->assignedValues = $assignedValues;
}
/**
* Get the administration controller
*/
public function getAdministrationController(): AdministrationController
{
return $this->administrationController;
}
/**
* Set the administration controller
*/
public function setAdministrationController(AdministrationController $administrationController): self
{
$this->administrationController = $administrationController;
return $this;
}
/**
* Get the assignedValues
*/
public function getAssignedValues(): array
{
return $this->assignedValues;
}
/**
* Set the assignedValues
*/
public function setAssignedValues(array $assignedValues): self
{
$this->assignedValues = $assignedValues;
return $this;
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\AdministrationController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class AdministrationNewsPidListingActionEvent
{
/**
* @var AdministrationController
*/
private $administrationController;
/**
* @var array
*/
private $rawTree;
/**
* @var int
*/
private $treeLevel;
public function __construct(AdministrationController $administrationController, array $rawTree, int $treeLevel)
{
$this->administrationController = $administrationController;
$this->rawTree = $rawTree;
$this->treeLevel = $treeLevel;
}
/**
* Get the administration controller
*/
public function getAdministrationController(): AdministrationController
{
return $this->administrationController;
}
/**
* Set the administration controller
*/
public function setAdministrationController(AdministrationController $administrationController): self
{
$this->administrationController = $administrationController;
return $this;
}
/**
* Get the rawTree
*/
public function getRawTree(): array
{
return $this->rawTree;
}
/**
* Set the rawTree
*/
public function setRawTree(array $rawTree): self
{
$this->rawTree = $rawTree;
return $this;
}
/**
* Get the treeLevel
*/
public function getTreeLevel(): int
{
return $this->treeLevel;
}
/**
* Set the treeLevel
*/
public function setTreeLevel(int $treeLevel): self
{
$this->treeLevel = $treeLevel;
return $this;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Domain\Model\Category;
use GeorgRinger\News\Domain\Service\CategoryImportService;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class CategoryImportPostHydrateEvent
{
/**
* @var CategoryImportService
*/
private $categoryImportService;
/**
* @var array
*/
private $importItem;
/**
* @var Category
*/
private $category;
public function __construct(CategoryImportService $categoryImportService, array $importItem, Category $category)
{
$this->categoryImportService = $categoryImportService;
$this->importItem = $importItem;
$this->category = $category;
}
/**
* Get the importer service
*/
public function getCategoryImportService(): CategoryImportService
{
return $this->categoryImportService;
}
/**
* Set the importer Service
*/
public function setCategoryImportService(CategoryImportService $categoryImportService): self
{
$this->categoryImportService = $categoryImportService;
return $this;
}
/**
* Get the importItem
*/
public function getImportItem(): array
{
return $this->importItem;
}
/**
* Set the importItem
*/
public function setImportItem(array $importItem): self
{
$this->importItem = $importItem;
return $this;
}
/**
* Get the category
*/
public function getCategory(): Category
{
return $this->category;
}
/**
* Set the category
*/
public function setCategory(Category $category): self
{
$this->category = $category;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\CategoryController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class CategoryListActionEvent
{
/**
* @var CategoryController
*/
private $categoryController;
/**
* @var array
*/
private $assignedValues;
public function __construct(CategoryController $categoryController, array $assignedValues)
{
$this->categoryController = $categoryController;
$this->assignedValues = $assignedValues;
}
/**
* Get the category controller
*/
public function getCategoryController(): CategoryController
{
return $this->categoryController;
}
/**
* Set the category controller
*/
public function setCategoryController(CategoryController $categoryController): self
{
$this->categoryController = $categoryController;
return $this;
}
/**
* Get the assignedValues
*/
public function getAssignedValues(): array
{
return $this->assignedValues;
}
/**
* Set the assignedValues
*/
public function setAssignedValues(array $assignedValues): self
{
$this->assignedValues = $assignedValues;
return $this;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\NewsController;
use GeorgRinger\News\Domain\Model\News;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class NewsCheckPidOfNewsRecordFailedInDetailActionEvent
{
/**
* @var NewsController
*/
private $newsController;
/**
* @var News
*/
private $news;
public function __construct(NewsController $newsController, News $news)
{
$this->newsController = $newsController;
$this->news = $news;
}
/**
* Get the news controller
*/
public function getNewsController(): NewsController
{
return $this->newsController;
}
/**
* Set the news controller
*/
public function setNewsController(NewsController $newsController): self
{
$this->newsController = $newsController;
return $this;
}
/**
* Get the news
*/
public function getNews(): News
{
return $this->news;
}
/**
* Set the news
*/
public function setNews(News $news): self
{
$this->news = $news;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\NewsController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class NewsDateMenuActionEvent
{
/**
* @var NewsController
*/
private $newsController;
/**
* @var array
*/
private $assignedValues;
public function __construct(NewsController $newsController, array $assignedValues)
{
$this->newsController = $newsController;
$this->assignedValues = $assignedValues;
}
/**
* Get the news controller
*/
public function getNewsController(): NewsController
{
return $this->newsController;
}
/**
* Set the news controller
*/
public function setNewsController(NewsController $newsController): self
{
$this->newsController = $newsController;
return $this;
}
/**
* Get the assignedValues
*/
public function getAssignedValues(): array
{
return $this->assignedValues;
}
/**
* Set the assignedValues
*/
public function setAssignedValues(array $assignedValues): self
{
$this->assignedValues = $assignedValues;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\NewsController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class NewsDetailActionEvent
{
/**
* @var NewsController
*/
private $newsController;
/**
* @var array
*/
private $assignedValues;
public function __construct(NewsController $newsController, array $assignedValues)
{
$this->newsController = $newsController;
$this->assignedValues = $assignedValues;
}
/**
* Get the news controller
*/
public function getNewsController(): NewsController
{
return $this->newsController;
}
/**
* Set the news controller
*/
public function setNewsController(NewsController $newsController): self
{
$this->newsController = $newsController;
return $this;
}
/**
* Get the assignedValues
*/
public function getAssignedValues(): array
{
return $this->assignedValues;
}
/**
* Set the assignedValues
*/
public function setAssignedValues(array $assignedValues): self
{
$this->assignedValues = $assignedValues;
return $this;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Domain\Model\News;
use GeorgRinger\News\Domain\Service\NewsImportService;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class NewsImportPostHydrateEvent
{
/**
* @var NewsImportService
*/
private $newsImportService;
/**
* @var array
*/
private $importItem;
/**
* @var News
*/
private $news;
public function __construct(NewsImportService $newsImportService, array $importItem, News $news)
{
$this->newsImportService = $newsImportService;
$this->importItem = $importItem;
$this->news = $news;
}
/**
* Get the importer service
*/
public function getNewsImportService(): NewsImportService
{
return $this->newsImportService;
}
/**
* Set the importer service
*/
public function setNewsImportService(NewsImportService $newsImportService): self
{
$this->newsImportService = $newsImportService;
return $this;
}
/**
* Get the importItem
*/
public function getImportItem(): array
{
return $this->importItem;
}
/**
* Set the importItem
*/
public function setImportItem(array $importItem): self
{
$this->importItem = $importItem;
return $this;
}
/**
* Get the news
*/
public function getNews(): News
{
return $this->news;
}
/**
* Set the news
*/
public function setNews(News $news): self
{
$this->news = $news;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Domain\Service\NewsImportService;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class NewsImportPreHydrateEvent
{
/**
* @var NewsImportService
*/
private $newsImportService;
/**
* @var array
*/
private $importItem;
public function __construct(NewsImportService $newsImportService, array $importItem)
{
$this->newsImportService = $newsImportService;
$this->importItem = $importItem;
}
/**
* Get the importer service
*/
public function getNewsImportService(): NewsImportService
{
return $this->newsImportService;
}
/**
* Set the importer service
*/
public function setNewsImportService(NewsImportService $newsImportService): self
{
$this->newsImportService = $newsImportService;
return $this;
}
/**
* Get the importItem
*/
public function getImportItem(): array
{
return $this->importItem;
}
/**
* Set the importItem
*/
public function setImportItem(array $importItem): self
{
$this->importItem = $importItem;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\NewsController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class NewsListActionEvent
{
/**
* @var NewsController
*/
private $newsController;
/**
* @var array
*/
private $assignedValues;
public function __construct(NewsController $newsController, array $assignedValues)
{
$this->newsController = $newsController;
$this->assignedValues = $assignedValues;
}
/**
* Get the news controller
*/
public function getNewsController(): NewsController
{
return $this->newsController;
}
/**
* Set the news controller
*/
public function setNewsController(NewsController $newsController): self
{
$this->newsController = $newsController;
return $this;
}
/**
* Get the assignedValues
*/
public function getAssignedValues(): array
{
return $this->assignedValues;
}
/**
* Set the assignedValues
*/
public function setAssignedValues(array $assignedValues): self
{
$this->assignedValues = $assignedValues;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\NewsController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class NewsListSelectedActionEvent
{
/**
* @var NewsController
*/
private $newsController;
/**
* @var array
*/
private $assignedValues;
public function __construct(NewsController $newsController, array $assignedValues)
{
$this->newsController = $newsController;
$this->assignedValues = $assignedValues;
}
/**
* Get the news controller
*/
public function getNewsController(): NewsController
{
return $this->newsController;
}
/**
* Set the news controller
*/
public function setNewsController(NewsController $newsController): self
{
$this->newsController = $newsController;
return $this;
}
/**
* Get the assignedValues
*/
public function getAssignedValues(): array
{
return $this->assignedValues;
}
/**
* Set the assignedValues
*/
public function setAssignedValues(array $assignedValues): self
{
$this->assignedValues = $assignedValues;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\NewsController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class NewsSearchFormActionEvent
{
/**
* @var NewsController
*/
private $newsController;
/**
* @var array
*/
private $assignedValues;
public function __construct(NewsController $newsController, array $assignedValues)
{
$this->newsController = $newsController;
$this->assignedValues = $assignedValues;
}
/**
* Get the news controller
*/
public function getNewsController(): NewsController
{
return $this->newsController;
}
/**
* Set the news controller
*/
public function setNewsController(NewsController $newsController): self
{
$this->newsController = $newsController;
return $this;
}
/**
* Get the assignedValues
*/
public function getAssignedValues(): array
{
return $this->assignedValues;
}
/**
* Set the assignedValues
*/
public function setAssignedValues(array $assignedValues): self
{
$this->assignedValues = $assignedValues;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\NewsController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class NewsSearchResultActionEvent
{
/**
* @var NewsController
*/
private $newsController;
/**
* @var array
*/
private $assignedValues;
public function __construct(NewsController $newsController, array $assignedValues)
{
$this->newsController = $newsController;
$this->assignedValues = $assignedValues;
}
/**
* Get the news controller
*/
public function getNewsController(): NewsController
{
return $this->newsController;
}
/**
* Set the news controller
*/
public function setNewsController(NewsController $newsController): self
{
$this->newsController = $newsController;
return $this;
}
/**
* Get the assignedValues
*/
public function getAssignedValues(): array
{
return $this->assignedValues;
}
/**
* Set the assignedValues
*/
public function setAssignedValues(array $assignedValues): self
{
$this->assignedValues = $assignedValues;
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Event;
use GeorgRinger\News\Controller\TagController;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
final class TagListActionEvent
{
/**
* @var TagController
*/
private $tagController;
/**
* @var array
*/
private $assignedValues;
public function __construct(TagController $tagController, array $assignedValues)
{
$this->tagController = $tagController;
$this->assignedValues = $assignedValues;
}
/**
* Get the tag controller
*/
public function getTagController(): TagController
{
return $this->tagController;
}
/**
* Set the tag controller
*/
public function setTagController(TagController $tagController): self
{
$this->tagController = $tagController;
return $this;
}
/**
* Get the assignedValues
*/
public function getAssignedValues(): array
{
return $this->assignedValues;
}
/**
* Set the assignedValues
*/
public function setAssignedValues(array $assignedValues): self
{
$this->assignedValues = $assignedValues;
return $this;
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace GeorgRinger\News\Hooks\Backend;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Hook into PageLayoutView to hide tt_content elements in page view
*/
class PageViewQueryHook
{
protected static $count = 0;
/**
* Prevent inline tt_content elements in news articles from
* showing in the page module.
*
* @param array $parameters
* @param string $table
* @param int $pageId
* @param array $additionalConstraints
* @param string[] $fieldList
* @param QueryBuilder $queryBuilder
*/
public function modifyQuery(
$parameters,
$table,
$pageId,
$additionalConstraints,
$fieldList,
QueryBuilder $queryBuilder
): void {
if ($table === 'tt_content' && $pageId > 0) {
// Get page record base on page uid
$pageRecord = BackendUtility::getRecord('pages', $pageId, 'uid', " AND doktype='254' AND module='news'");
if (is_array($pageRecord)) {
$tsConfig = BackendUtility::getPagesTSconfig($pageId);
if ((int)($tsConfig['tx_news.']['showContentElementsInNewsSysFolder'] ?? 0) === 1) {
return;
}
// Only hide elements which are inline, allowing for standard
// elements to show
$queryBuilder->andWhere(
$queryBuilder->expr()->eq('tx_news_related_news', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
);
if (self::$count === 0) {
$this->addFlashMessage();
}
self::$count++;
}
}
}
/**
* Render flash message to inform user
* that no elements belonging to news articles
* are rendered in the page module
*
* @return void
*/
private function addFlashMessage(): void
{
$message = GeneralUtility::makeInstance(
FlashMessage::class,
$this->getLanguageService()->sL('LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:hiddenContentElements.description'),
'',
FlashMessage::INFO
);
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($message);
}
/**
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace GeorgRinger\News\Hooks\Backend;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Backend\RecordList\RecordListConstraint;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Hook into DatabaseRecordList to hide tt_content elements in list view
*/
class RecordListQueryHook
{
protected static $count = 0;
/** @var RecordListConstraint */
protected $recordListConstraint;
/**
* RecordListQueryHook constructor.
* @param RecordListConstraint $recordListConstraint
*/
public function __construct(
RecordListConstraint $recordListConstraint
) {
$this->recordListConstraint = $recordListConstraint;
}
/**
* @return void
*/
public function modifyQuery(
array &$parameters,
string $table,
int $pageId,
array $additionalConstraints,
array $fieldList,
QueryBuilder $queryBuilder
) {
if ($table === 'tt_content' && $pageId > 0) {
$pageRecord = BackendUtility::getRecord('pages', $pageId, 'uid', " AND doktype='254' AND module='news'");
if (is_array($pageRecord)) {
$tsConfig = BackendUtility::getPagesTSconfig($pageId);
if ((int)($tsConfig['tx_news.']['showContentElementsInNewsSysFolder'] ?? 0) === 1) {
return;
}
$queryBuilder->where(...['1=2']);
if (self::$count === 0) {
$this->addFlashMessage();
}
self::$count++;
}
} elseif ($table === 'tx_news_domain_model_news' && $this->recordListConstraint->isInAdministrationModule()) {
$vars = GeneralUtility::_GET('tx_news_web_newsadministration');
if (is_array($vars) && is_array($vars['demand'])) {
$vars = $vars['demand'];
$this->recordListConstraint->extendQuery($parameters, $vars);
if (isset($parameters['orderBy'][0])) {
$queryBuilder->orderBy($parameters['orderBy'][0][0], $parameters['orderBy'][0][1]);
unset($parameters['orderBy']);
}
if (!empty($parameters['whereDoctrine'])) {
$queryBuilder->andWhere(...$parameters['whereDoctrine']);
unset($parameters['where']);
}
}
}
}
private function addFlashMessage(): void
{
$message = GeneralUtility::makeInstance(
FlashMessage::class,
$this->getLanguageService()->sL('LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:hiddenContentElements.description'),
'',
FlashMessage::INFO
);
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($message);
}
/**
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}

View File

@@ -0,0 +1,291 @@
<?php
namespace GeorgRinger\News\Hooks;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use TYPO3\CMS\Backend\Utility\BackendUtility as BackendUtilityCore;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Hook into \TYPO3\CMS\Backend\Utility\BackendUtility to change flexform behaviour
* depending on action selection
*/
class BackendUtility
{
/**
* Fields which are removed in detail view
*
* @var array
*/
public $removedFieldsInDetailView = [
'sDEF' => 'orderBy,orderDirection,categories,categoryConjunction,includeSubCategories,
archiveRestriction,timeRestriction,timeRestrictionHigh,topNewsRestriction,
dateField,selectedList',
'additional' => 'limit,offset,hidePagination,topNewsFirst,listPid,list.paginate.itemsPerPage',
'template' => 'cropMaxCharacters'
];
/**
* Fields which are removed in list view
*
* @var array
*/
public $removedFieldsInListView = [
'sDEF' => 'dateField,singleNews,previewHiddenRecords,selectedList',
'additional' => '',
'template' => ''
];
/**
* Fields which are removed in selected list view
*
* @var array
*/
public $removedFieldsInSelectedListView = [
'sDEF' => 'categories,categoryConjunction,includeSubCategories,
archiveRestriction,timeRestriction,timeRestrictionHigh,topNewsRestriction,
startingpoint,recursive,dateField,singleNews,previewHiddenRecords,
previewHiddenRecords,startingpoint,recursive',
'additional' => 'tags,limit,offset,hidePagination,topNewsFirst,backPid,excludeAlreadyDisplayedNews,
list.paginate.itemsPerPage,disableOverrideDemand',
'template' => ''
];
/**
* Fields which are removed in dateMenu view
*
* @var array
*/
public $removedFieldsInDateMenuView = [
'sDEF' => 'orderBy,singleNews,selectedList',
'additional' => 'limit,offset,hidePagination,topNewsFirst,backPid,previewHiddenRecords,excludeAlreadyDisplayedNews,
list.paginate.itemsPerPage',
'template' => 'cropMaxCharacters,media.maxWidth,media.maxHeight'
];
/**
* Fields which are removed in search form view
*
* @var array
*/
public $removedFieldsInSearchFormView = [
'sDEF' => 'orderBy,orderDirection,categories,categoryConjunction,includeSubCategories,
archiveRestriction,timeRestriction,timeRestrictionHigh,topNewsRestriction,
startingpoint,recursive,dateField,singleNews,previewHiddenRecords,selectedList',
'additional' => 'limit,offset,hidePagination,topNewsFirst,detailPid,backPid,excludeAlreadyDisplayedNews,
list.paginate.itemsPerPage',
'template' => 'cropMaxCharacters,media.maxWidth,media.maxHeight'
];
/**
* Fields which are removed in category list view
*
* @var array
*/
public $removedFieldsInCategoryListView = [
'sDEF' => 'orderBy,orderDirection,categoryConjunction,includeSubCategories,
archiveRestriction,timeRestriction,timeRestrictionHigh,topNewsRestriction,
recursive,dateField,singleNews,previewHiddenRecords,selectedList',
'additional' => 'limit,offset,hidePagination,topNewsFirst,detailPid,backPid,excludeAlreadyDisplayedNews,
list.paginate.itemsPerPage',
'template' => 'cropMaxCharacters,media.maxWidth,media.maxHeight'
];
/**
* Fields which are removed in tag list view
*
* @var array
*/
public $removedFieldsInTagListView = [
'sDEF' => 'categories,categoryConjunction,includeSubCategories,
archiveRestriction,timeRestriction,timeRestrictionHigh,topNewsRestriction,
dateField,singleNews,previewHiddenRecords,selectedList',
'additional' => 'limit,offset,hidePagination,topNewsFirst,detailPid,backPid,excludeAlreadyDisplayedNews,
list.paginate.itemsPerPage',
'template' => 'cropMaxCharacters,media.maxWidth,media.maxHeight'
];
/** @var EmConfiguration */
protected $configuration;
/**
* BackendUtility constructor.
* @param EmConfiguration $configuration
*/
public function __construct(
) {
$this->configuration = GeneralUtility::makeInstance(EmConfiguration::class);
}
/**
* @param array $dataStructure
* @param array $identifier
*
* @return array|string
*/
public function parseDataStructureByIdentifierPostProcess(array $dataStructure, array $identifier)
{
if ($identifier['type'] === 'tca' && $identifier['tableName'] === 'tt_content' && $identifier['dataStructureKey'] === 'news_pi1,list') {
$getVars = GeneralUtility::_GET('edit');
if (isset($getVars['tt_content']) && is_array($getVars['tt_content'])) {
$item = array_keys($getVars['tt_content']);
$recordId = (int)$item[0];
if (($getVars['tt_content'][$recordId] ?? '') === 'new') {
$fakeRow = [
'uid' => 'NEW123'
];
$this->updateFlexforms($dataStructure, $fakeRow);
} else {
$row = BackendUtilityCore::getRecord('tt_content', $recordId);
if (is_array($row)) {
$this->updateFlexforms($dataStructure, $row);
}
}
}
}
return $dataStructure;
}
/**
* Update flexform configuration if a action is selected
*
* @param array|string &$dataStructure flexform structure
* @param array $row row of current record
* @param array $dataStructure
*
* @return void
*/
protected function updateFlexforms(array &$dataStructure, array $row): void
{
$selectedView = '';
$flexformSelection = [];
if (isset($row['pi_flexform'])) {
// get the first selected action
if (is_string($row['pi_flexform'])) {
$flexformSelection = GeneralUtility::xml2array($row['pi_flexform']);
} else {
$flexformSelection = $row['pi_flexform'];
}
}
if (is_array($flexformSelection) && isset($flexformSelection['data'])) {
$selectedView = $flexformSelection['data']['sDEF']['lDEF']['switchableControllerActions']['vDEF'] ?? '';
if (!empty($selectedView)) {
$actionParts = GeneralUtility::trimExplode(';', $selectedView, true);
$selectedView = $actionParts[0];
}
// new plugin element
} elseif (str_starts_with((string)$row['uid'], 'NEW')) {
// use List as starting view
$selectedView = 'News->list';
}
if (!empty($selectedView)) {
// Modify the flexform structure depending on the first found action
switch ($selectedView) {
case 'News->list':
case 'News->searchResult':
$this->deleteFromStructure($dataStructure, $this->removedFieldsInListView);
break;
case 'News->detail':
$this->deleteFromStructure($dataStructure, $this->removedFieldsInDetailView);
break;
case 'News->selectedList':
$this->deleteFromStructure($dataStructure, $this->removedFieldsInSelectedListView);
break;
case 'News->searchForm':
$this->deleteFromStructure($dataStructure, $this->removedFieldsInSearchFormView);
break;
case 'News->dateMenu':
$this->deleteFromStructure($dataStructure, $this->removedFieldsInDateMenuView);
break;
case 'Category->list':
$this->deleteFromStructure($dataStructure, $this->removedFieldsInCategoryListView);
break;
case 'Tag->list':
$this->deleteFromStructure($dataStructure, $this->removedFieldsInTagListView);
break;
default:
}
if ($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['Hooks/BackendUtility.php']['updateFlexforms'] ?? []) {
$params = [
'selectedView' => $selectedView,
'dataStructure' => &$dataStructure,
];
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['Hooks/BackendUtility.php']['updateFlexforms'] as $reference) {
GeneralUtility::callUserFunction($reference, $params, $this);
}
}
}
}
/**
* Add category restriction to flexforms
*
* @param array $structure
*
* @return void
*/
protected function addCategoryConstraints(&$structure): void
{
$categoryRestrictionSetting = $this->configuration->getCategoryRestriction();
$categoryRestriction = '';
switch ($categoryRestrictionSetting) {
case 'current_pid':
$categoryRestriction = ' AND sys_category.pid=###CURRENT_PID### ';
break;
case 'siteroot':
$categoryRestriction = ' AND sys_category.pid IN (###SITEROOT###) ';
break;
case 'page_tsconfig':
$categoryRestriction = ' AND sys_category.pid IN (###PAGE_TSCONFIG_IDLIST###) ';
break;
}
if (!empty($categoryRestriction) && isset($structure['sheets']['sDEF']['ROOT']['el']['settings.categories'])) {
$structure['sheets']['sDEF']['ROOT']['el']['settings.categories']['TCEforms']['config']['foreign_table_where'] = $categoryRestriction . $structure['sheets']['sDEF']['ROOT']['el']['settings.categories']['TCEforms']['config']['foreign_table_where'];
}
}
/**
* Remove fields from flexform structure
*
* @param array &$dataStructure flexform structure
* @param array $fieldsToBeRemoved fields which need to be removed
*
* @return void
*/
protected function deleteFromStructure(array &$dataStructure, array $fieldsToBeRemoved): void
{
foreach ($fieldsToBeRemoved as $sheetName => $sheetFields) {
$fieldsInSheet = GeneralUtility::trimExplode(',', $sheetFields, true);
foreach ($fieldsInSheet as $fieldName) {
unset($dataStructure['sheets'][$sheetName]['ROOT']['el']['settings.' . $fieldName]);
}
}
}
/**
* @param int $pageId
* @return bool
*/
protected function enabledInTsConfig($pageId): bool
{
$tsConfig = BackendUtilityCore::getPagesTSconfig($pageId);
if (isset($tsConfig['tx_news.']['categoryRestrictionForFlexForms'])) {
return (bool)$tsConfig['tx_news.']['categoryRestrictionForFlexForms'];
}
return false;
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace GeorgRinger\News\Hooks;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Service\AccessControlService;
use TYPO3\CMS\Backend\Utility\BackendUtility as BackendUtilityCore;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Hook into tcemain which is used to show preview of news item
*/
class DataHandler implements SingletonInterface
{
protected $cacheTagsToFlush = [];
/**
* Flushes the cache if a news record was edited.
* This happens on two levels: by UID and by PID.
*
* @param array $params
*
* @return void
*/
public function clearCachePostProc(array $params): void
{
if (isset($params['table']) && $params['table'] === 'tx_news_domain_model_news') {
if (isset($params['uid'])) {
$this->cacheTagsToFlush[] = 'tx_news_uid_' . $params['uid'];
}
if (isset($params['uid_page'])) {
$this->cacheTagsToFlush[] = 'tx_news_pid_' . $params['uid_page'];
}
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
foreach (array_unique($this->cacheTagsToFlush) as $cacheTag) {
$cacheManager->flushCachesInGroupByTag('pages', $cacheTag);
}
}
}
/**
* Generate a different preview link *
*
* @param string $status status
* @param string $table table name
* @param int $recordUid id of the record
* @param array $fields fieldArray
* @param \TYPO3\CMS\Core\DataHandling\DataHandler $parentObject parent Object
*
* @return void
*/
public function processDatamap_afterDatabaseOperations(
$status,
$table,
$recordUid,
array $fields,
\TYPO3\CMS\Core\DataHandling\DataHandler $parentObject
): void {
// Clear category cache
if ($table === 'sys_category') {
$cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('news_category');
$cache->flush();
}
}
/**
* Prevent saving of a news record if the editor doesn't have access to all categories of the news record
*
* @param array $fieldArray
* @param string $table
* @param int $id
* @param $parentObject \TYPO3\CMS\Core\DataHandling\DataHandler
*
* @return void
*/
public function processDatamap_preProcessFieldArray(&$fieldArray, $table, $id, $parentObject): void
{
if ($table === 'tx_news_domain_model_news') {
// check permissions of assigned categories
if (is_int($id) && !$this->getBackendUser()->isAdmin()) {
$newsRecord = BackendUtilityCore::getRecord($table, $id);
if (!AccessControlService::userHasCategoryPermissionsForRecord($newsRecord)) {
$parentObject->log(
$table,
$id,
2,
0,
1,
"processDatamap: Attempt to modify a record from table '%s' without permission. Reason: the record has one or more categories assigned that are not defined in your BE usergroup.",
1,
[$table]
);
// unset fieldArray to prevent saving of the record
$fieldArray = [];
} else {
// If the category relation has been modified, no | is found anymore
if (isset($fieldArray['categories']) && strpos($fieldArray['categories'], '|') === false) {
$deniedCategories = AccessControlService::getAccessDeniedCategories($newsRecord);
if (is_array($deniedCategories)) {
foreach ($deniedCategories as $deniedCategory) {
$fieldArray['categories'] .= ',' . $deniedCategory['uid'];
}
// Check if the categories are not empty,
if (!empty($fieldArray['categories'])) {
$fieldArray['categories'] = trim($fieldArray['categories'], ',');
}
}
}
}
}
}
}
/**
* Prevent deleting/moving of a news record if the editor doesn't have access to all categories of the news record
*
* @param string $command
* @param string $table
* @param int $id
* @param string $value
* @param $parentObject \TYPO3\CMS\Core\DataHandling\DataHandler
*
* @return void
*/
public function processCmdmap_preProcess($command, &$table, $id, $value, $parentObject): void
{
if ($table === 'tx_news_domain_model_news' && !$this->getBackendUser()->isAdmin() && is_int($id) && $command !== 'undelete') {
$newsRecord = BackendUtilityCore::getRecord($table, $id);
if (is_array($newsRecord) && !AccessControlService::userHasCategoryPermissionsForRecord($newsRecord)) {
$parentObject->log(
$table,
$id,
2,
0,
1,
'processCmdmap: Attempt to ' . $command . " a record from table '%s' without permission. Reason: the record has one or more categories assigned that are not defined in the BE usergroup.",
1,
[$table]
);
// unset table to prevent saving
$table = '';
}
}
}
/**
* Flush cache for old news pid when moving record
*
* @param string $table
* @param int $uid
* @param int $destPid
* @param array $propArr
* @param array $moveRec
* @param int $resolvedPid
* @param bool $recordWasMoved
* @param \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler
*/
public function moveRecord($table, $uid, $destPid, $propArr, $moveRec, $resolvedPid, $recordWasMoved, \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler)
{
if ($table === 'tx_news_domain_model_news') {
$this->cacheTagsToFlush[] = 'tx_news_pid_' . $moveRec['pid'];
}
}
/**
* Returns the current BE user.
*
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
*/
protected function getBackendUser(): \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace GeorgRinger\News\Hooks;
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Backend\Form\Element\InlineElement;
use TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Inline Element Hook
*/
class InlineElementHook implements InlineElementHookInterface
{
/**
* Initializes this hook object.
*
* @param InlineElement $parentObject
*
* @return void
*/
public function init(&$parentObject): void
{
}
/**
* Pre-processing to define which control items are enabled or disabled.
*
* @param string $parentUid The uid of the parent (embedding) record (uid or NEW...)
* @param string $foreignTable The table (foreign_table) we create control-icons for
* @param array $childRecord The current record of that foreign_table
* @param array $childConfig TCA configuration of the current field of the child record
* @param bool $isVirtual Defines whether the current records is only virtually shown and not physically part of the parent record
* @param array &$enabledControls (reference) Associative array with the enabled control items
*
* @return void
*/
public function renderForeignRecordHeaderControl_preProcess(
$parentUid,
$foreignTable,
array $childRecord,
array $childConfig,
$isVirtual,
array &$enabledControls
) {
}
/**
* Post-processing to define which control items to show. Possibly own icons can be added here.
*
* @param string $parentUid The uid of the parent (embedding) record (uid or NEW...)
* @param string $foreignTable The table (foreign_table) we create control-icons for
* @param array $childRecord The current record of that foreign_table
* @param array $childConfig TCA configuration of the current field of the child record
* @param bool $isVirtual Defines whether the current records is only virtually shown and not physically part of the parent record
* @param array &$controlItems (reference) Associative array with the currently available control items
*
* @return void
*/
public function renderForeignRecordHeaderControl_postProcess(
$parentUid,
$foreignTable,
array $childRecord,
array $childConfig,
$isVirtual,
array &$controlItems
) {
$previewSetting = (int)($childRecord['showinpreview'] ?? 0);
if ($foreignTable === 'sys_file_reference' && $previewSetting > 0) {
$ll = 'LLL:EXT:news/Resources/Private/Language/locallang_db.xlf:';
$extensionConfiguration = GeneralUtility::makeInstance(EmConfiguration::class);
if ($extensionConfiguration->isAdvancedMediaPreview()) {
if ($previewSetting === 1) {
$label = $GLOBALS['LANG']->sL($ll . 'tx_news_domain_model_media.showinviews.1');
$extraItem = ['showinpreview' => ' <span class="btn btn-default" title="' . htmlspecialchars($label) . '"><i class="fa fa-check"></i></span>'];
$controlItems = $extraItem + $controlItems;
} elseif ($previewSetting === 2) {
$label = $GLOBALS['LANG']->sL($ll . 'tx_news_domain_model_media.showinviews.2');
$extraItem = ['showinpreview' => ' <span class="btn btn-default" title="' . htmlspecialchars($label) . '"><i class="fa fa-check-square"></i></span>'];
$controlItems = $extraItem + $controlItems;
}
} elseif ($previewSetting === 1) {
$label = $GLOBALS['LANG']->sL($ll . 'tx_news_domain_model_media.showinpreview');
$extraItem = ['showinpreview' => ' <span class="btn btn-default" title="' . htmlspecialchars($label) . '"><i class="fa fa-check"></i></span>'];
$controlItems = $extraItem + $controlItems;
}
}
}
}

View File

@@ -0,0 +1,323 @@
<?php
namespace GeorgRinger\News\Hooks;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Utility\TemplateLayout;
use TYPO3\CMS\Backend\Utility\BackendUtility as BackendUtilityCore;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Userfunc to render alternative label for media elements
*/
class ItemsProcFunc
{
/** @var TemplateLayout $templateLayoutsUtility */
protected $templateLayoutsUtility;
/**
* ItemsProcFunc constructor.
* @param TemplateLayout $templateLayout
*/
public function __construct(
TemplateLayout $templateLayout
) {
$this->templateLayoutsUtility = $templateLayout;
}
/**
* Itemsproc function to extend the selection of templateLayouts in the plugin
*
* @param array &$config configuration array
*
* @return void
*/
public function user_templateLayout(array &$config): void
{
$pageId = 0;
$currentColPos = $config['flexParentDatabaseRow']['colPos'];
$pageId = $this->getPageId($config['flexParentDatabaseRow']['pid']);
if ($pageId > 0) {
$templateLayouts = $this->templateLayoutsUtility->getAvailableTemplateLayouts($pageId);
$templateLayouts = $this->reduceTemplateLayouts($templateLayouts, $currentColPos);
foreach ($templateLayouts as $layout) {
$additionalLayout = [
htmlspecialchars($this->getLanguageService()->sL($layout[0])),
$layout[1]
];
array_push($config['items'], $additionalLayout);
}
}
}
/**
* Reduce the template layouts by the ones that are not allowed in given colPos
*
* @param array $templateLayouts
* @param int $currentColPos
* @return array
*/
protected function reduceTemplateLayouts($templateLayouts, $currentColPos): array
{
$currentColPos = (int)$currentColPos;
$restrictions = [];
$allLayouts = [];
foreach ($templateLayouts as $key => $layout) {
if (is_array($layout[0])) {
if (isset($layout[0]['allowedColPos']) && str_ends_with((string)$layout[1], '.')) {
$layoutKey = substr($layout[1], 0, -1);
$restrictions[$layoutKey] = GeneralUtility::intExplode(',', $layout[0]['allowedColPos'], true);
}
} else {
$allLayouts[$key] = $layout;
}
}
if (!empty($restrictions)) {
foreach ($restrictions as $restrictedIdentifier => $restrictedColPosList) {
if (!in_array($currentColPos, $restrictedColPosList, true)) {
unset($allLayouts[$restrictedIdentifier]);
}
}
}
return $allLayouts;
}
/**
* Modifies the select box of orderBy-options as a category menu
* needs different ones then a news action
*
* @param array &$config configuration array
*
* @return void
*/
public function user_orderBy(array &$config): void
{
$row = $this->getContentElementRow($config['row']['uid']);
// check if the record has been saved once
if (is_array($row) && !empty($row['pi_flexform'])) {
$flexformConfig = GeneralUtility::xml2array($row['pi_flexform']);
// check if there is a flexform configuration
if (isset($flexformConfig['data']['sDEF']['lDEF']['switchableControllerActions']['vDEF'])) {
$selectedActionList = $flexformConfig['data']['sDEF']['lDEF']['switchableControllerActions']['vDEF'] ?? '';
// check for selected action
if (str_starts_with($selectedActionList, 'Category')) {
$newItems = $GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['orderByCategory'];
} elseif (str_starts_with($selectedActionList, 'Tag')) {
$this->removeNonValidOrderFields($config, 'tx_news_domain_model_tag');
$newItems = $GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['orderByTag'];
} else {
$newItems = $GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['orderByNews'];
}
}
}
// if a override configuration is found
if (!empty($newItems)) {
// remove default configuration
$config['items'] = [];
// empty default line
array_push($config['items'], ['', '']);
$newItemArray = GeneralUtility::trimExplode(',', $newItems, true);
$languageKey = 'LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:flexforms_general.orderBy.';
foreach ($newItemArray as $item) {
// label: if empty, key (=field) is used
$label = $this->getLanguageService()->sL($languageKey . $item);
if (empty($label)) {
$label = $item;
}
array_push($config['items'], [htmlspecialchars($label), $item]);
}
}
}
/**
* Remove not valid fields from ordering
*
* @param array $config tca items
* @param string $tableName table name
*
* @return void
*/
protected function removeNonValidOrderFields(array &$config, $tableName): void
{
$allowedFields = array_keys($GLOBALS['TCA'][$tableName]['columns']);
foreach ($config['items'] as $key => $item) {
if ($item[1] != '' && !in_array($item[1], $allowedFields)) {
unset($config['items'][$key]);
}
}
}
/**
* Modifies the selectbox of available actions
*
* @param array &$config
*
* @return void
*/
public function user_switchableControllerActions(array &$config): void
{
if (!empty($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['switchableControllerActions']['list'])) {
$configuration = (int)$GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['switchableControllerActions']['list'];
switch ($configuration) {
case 1:
$this->removeActionFromList($config, 'News->list');
break;
case 2:
$this->removeActionFromList($config, 'News->list;News->detail');
break;
default:
}
}
// Add additional actions
if (isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['switchableControllerActions']['newItems'])
&& is_array($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['switchableControllerActions']['newItems'])
) {
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['switchableControllerActions']['newItems'] as $key => $label) {
array_push($config['items'], [$this->getLanguageService()->sL($label), $key, '']);
}
}
}
/**
* Remove given action from switchableControllerActions
*
* @param array $config available items
* @param string $action action to be removed
*
* @return void
*/
private function removeActionFromList(array &$config, $action): void
{
foreach ($config['items'] as $key => $item) {
if ($item[1] === $action) {
unset($config['items'][$key]);
continue;
}
}
}
/**
* Generate a select box of languages to choose an overlay
*
* @return string select box
*/
public function user_categoryOverlay(): string
{
$html = '';
$languages = $this->getAllLanguages();
// if any language is available
if (count($languages) > 0) {
$html = '<select name="data[newsoverlay]" id="field_newsoverlay" class="form-control">';
if (!isset($languages[0])) {
$html .= '<option value="0">' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.default_value')) . '</option>';
}
foreach ($languages as $language) {
$selected = ((int)($GLOBALS['BE_USER']->uc['newsoverlay'] ?? 0) === (int)$language['uid']) ? ' selected="selected" ' : '';
$html .= '<option ' . $selected . 'value="' . $language['uid'] . '">' . htmlspecialchars($language['title']) . '</option>';
}
$html .= '</select>';
} else {
$html .= htmlspecialchars(
$this->getLanguageService()->sL(
'LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:usersettings.no-languages-available'
)
);
}
return $html;
}
/**
* Get all languages
*
* @return array
*/
protected function getAllLanguages(): array
{
$versionInformation = GeneralUtility::makeInstance(Typo3Version::class);
if ($versionInformation->getMajorVersion() === 10) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_language');
return $queryBuilder->select('*')
->from('sys_language')
->orderBy('sorting')
->execute()
->fetchAll();
}
$siteLanguages = [];
foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
foreach ($site->getAllLanguages() as $languageId => $language) {
if (!isset($siteLanguages[$languageId])) {
$siteLanguages[$languageId] = [
'uid' => $languageId,
'title' => $language->getTitle(),
];
}
}
}
return $siteLanguages;
}
/**
* Get tt_content record
*
* @param int $uid
*
* @return array|null
*/
protected function getContentElementRow($uid): ?array
{
return BackendUtilityCore::getRecord('tt_content', $uid);
}
/**
* Get page id, if negative, then it is a "after record"
*
* @param int $pid
* @return int
*/
protected function getPageId($pid): int
{
$pid = (int)$pid;
if ($pid > 0) {
return $pid;
}
$row = BackendUtilityCore::getRecord('tt_content', abs($pid), 'uid,pid');
return $row['pid'];
}
/**
* Returns LanguageService
*
* @return \TYPO3\CMS\Core\Localization\LanguageService
*/
protected function getLanguageService(): \TYPO3\CMS\Core\Localization\LanguageService
{
return $GLOBALS['LANG'];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace GeorgRinger\News\Hooks;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Service\CategoryService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Userfunc to get alternative label
*/
class Labels
{
/**
* Generate additional label for category records
* including the title of the parent category
*
* @param array $params
*
* @return void
*/
public function getUserLabelCategory(array &$params): void
{
$showTranslationInformation = false;
$getVars = GeneralUtility::_GET();
if (isset($getVars['route']) && $getVars['route'] === '/record/edit'
&& isset($getVars['edit']) && is_array($getVars['edit'])
&& (isset($getVars['edit']['tt_content']) || isset($getVars['edit']['tx_news_domain_model_news']) || isset($getVars['edit']['sys_category']))
) {
$showTranslationInformation = true;
}
if ($showTranslationInformation && is_array($params['row'])) {
$params['title'] = CategoryService::translateCategoryRecord($params['row']['title'], $params['row']);
} else {
$params['title'] = $params['row']['title'];
}
}
}

View File

@@ -0,0 +1,770 @@
<?php
namespace GeorgRinger\News\Hooks;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Utility\TemplateLayout;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\DocumentTemplate;
use TYPO3\CMS\Backend\Utility\BackendUtility as BackendUtilityCore;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/**
* Hook to display verbose information about pi1 plugin in Web>Page module
*/
class PageLayoutView
{
/**
* Extension key
*
* @var string
*/
const KEY = 'news';
/**
* Path to the locallang file
*
* @var string
*/
const LLPATH = 'LLL:EXT:news/Resources/Private/Language/locallang_be.xlf:';
/**
* Max shown settings
*/
const SETTINGS_IN_PREVIEW = 7;
/**
* Table information
*
* @var array
*/
public $tableData = [];
/**
* Flexform information
*
* @var array
*/
public $flexformData = [];
/**
* @var IconFactory
*/
protected $iconFactory;
/** @var TemplateLayout $templateLayoutsUtility */
protected $templateLayoutsUtility;
/**
* PageLayoutView constructor.
* @param TemplateLayout $templateLayout
* @param IconFactory $iconFactory
*/
public function __construct(
TemplateLayout $templateLayout,
IconFactory $iconFactory
) {
$this->templateLayoutsUtility = $templateLayout;
$this->iconFactory = $iconFactory;
}
/**
* Returns information about this extension's pi1 plugin
*
* @param array $params Parameters to the hook
* @return string Information about pi1 plugin
*/
public function getExtensionSummary(array $params): string
{
$actionTranslationKey = $result = '';
$header = '<strong>' . htmlspecialchars($this->getLanguageService()->sL(self::LLPATH . 'pi1_title')) . '</strong>';
if ($params['row']['list_type'] == self::KEY . '_pi1') {
$this->tableData = [];
$this->flexformData = GeneralUtility::xml2array($params['row']['pi_flexform']);
// if flexform data is found
$actions = $this->getFieldFromFlexform('switchableControllerActions');
if (!empty($actions)) {
$actionList = GeneralUtility::trimExplode(';', $actions);
// translate the first action into its translation
$actionTranslationKey = strtolower(str_replace('->', '_', $actionList[0]));
$actionTranslation = $this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.mode.' . $actionTranslationKey);
$header .= '<br><strong style="text-transform: uppercase">' . htmlspecialchars($actionTranslation) . '</strong>';
} else {
$header .= $this->generateCallout($this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.mode.not_configured'));
}
if (is_array($this->flexformData)) {
switch ($actionTranslationKey) {
case 'news_list':
$this->getStartingPoint();
$this->getCategorySettings();
$this->getDetailPidSetting();
$this->getTimeRestrictionSetting();
$this->getTemplateLayoutSettings($params['row']['pid']);
$this->getArchiveSettings();
$this->getTopNewsRestrictionSetting();
$this->getOrderSettings();
$this->getOffsetLimitSettings();
$this->getListPidSetting();
$this->getTagRestrictionSetting();
break;
case 'news_selectedlist':
$this->getSelectedListSetting();
$this->getOrderSettings();
break;
case 'news_detail':
$this->getSingleNewsSettings();
$this->getDetailPidSetting();
$this->getTemplateLayoutSettings($params['row']['pid']);
break;
case 'news_datemenu':
$this->getStartingPoint();
$this->getTimeRestrictionSetting();
$this->getTopNewsRestrictionSetting();
$this->getArchiveSettings();
$this->getDateMenuSettings();
$this->getCategorySettings();
$this->getTemplateLayoutSettings($params['row']['pid']);
break;
case 'category_list':
$this->getCategorySettings(false);
$this->getTemplateLayoutSettings($params['row']['pid']);
break;
case 'tag_list':
$this->getStartingPoint();
$this->getListPidSetting();
$this->getOrderSettings();
$this->getTemplateLayoutSettings($params['row']['pid']);
break;
default:
$this->getTemplateLayoutSettings($params['row']['pid']);
}
if ($hooks = $GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['GeorgRinger\\News\\Hooks\\PageLayoutView']['extensionSummary'] ?? []) {
$params = [
'action' => $actionTranslationKey
];
foreach ($hooks as $reference) {
GeneralUtility::callUserFunction($reference, $params, $this);
}
}
// for all views
$this->getOverrideDemandSettings();
$result = $this->renderSettingsAsTable($header, $params['row']['uid'] ?? 0);
}
}
return $result;
}
/**
* Render archive settings
*
* @return void
*/
public function getArchiveSettings(): void
{
$archive = $this->getFieldFromFlexform('settings.archiveRestriction');
if (!empty($archive)) {
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.archiveRestriction'),
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.archiveRestriction.' . $archive)
];
}
}
/**
* Render single news settings
*
* @return void
*/
public function getSingleNewsSettings(): void
{
$singleNewsRecord = (int)$this->getFieldFromFlexform('settings.singleNews');
if ($singleNewsRecord > 0) {
$newsRecord = BackendUtilityCore::getRecord('tx_news_domain_model_news', $singleNewsRecord);
if (is_array($newsRecord)) {
$pageRecord = BackendUtilityCore::getRecord('pages', $newsRecord['pid']);
if (is_array($pageRecord)) {
$content = $this->getRecordData($newsRecord['uid'], 'tx_news_domain_model_news');
} else {
$text = sprintf(
$this->getLanguageService()->sL(self::LLPATH . 'pagemodule.pageNotAvailable'),
$newsRecord['pid']
);
$content = $this->generateCallout($text);
}
} else {
$text = sprintf(
$this->getLanguageService()->sL(self::LLPATH . 'pagemodule.newsNotAvailable'),
$singleNewsRecord
);
$content = $this->generateCallout($text);
}
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.singleNews'),
$content
];
}
}
/**
* Render single news settings
*
* @return void
*/
public function getDetailPidSetting(): void
{
$detailPid = (int)$this->getFieldFromFlexform('settings.detailPid', 'additional');
if ($detailPid > 0) {
$content = $this->getRecordData($detailPid);
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_additional.detailPid'),
$content
];
}
}
/**
* Render listPid news settings
*
* @return void
*/
public function getListPidSetting(): void
{
$listPid = (int)$this->getFieldFromFlexform('settings.listPid', 'additional');
if ($listPid > 0) {
$content = $this->getRecordData($listPid);
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_additional.listPid'),
$content
];
}
}
/**
* Get the rendered page title including onclick menu
*
* @param int $detailPid
* @return string
* @deprecated use getRecordData() instead
*/
public function getPageRecordData($detailPid): string
{
return $this->getRecordData($detailPid, 'pages');
}
/**
* @param int $id
* @param string $table
* @return string
*/
public function getRecordData($id, $table = 'pages'): string
{
$record = BackendUtilityCore::getRecord($table, $id);
if (is_array($record)) {
$data = '<span data-toggle="tooltip" data-placement="top" data-title="id=' . $record['uid'] . '">'
. $this->iconFactory->getIconForRecord($table, $record, Icon::SIZE_SMALL)->render()
. '</span> ';
$content = BackendUtilityCore::wrapClickMenuOnIcon(
$data,
$table,
$record['uid'],
true,
'',
'+info,edit,history'
);
$linkTitle = htmlspecialchars(BackendUtilityCore::getRecordTitle($table, $record));
if ($table === 'pages') {
$id = $record['uid'];
$currentPageId = (int)GeneralUtility::_GET('id');
$link = htmlspecialchars($this->getEditLink($record, $currentPageId));
$switchLabel = $this->getLanguageService()->sL(self::LLPATH . 'pagemodule.switchToPage');
$content .= ' <a href="#" data-toggle="tooltip" data-placement="top" data-title="' . $switchLabel . '" onclick=\'top.jump("' . $link . '", "web_layout", "web", ' . $id . ');return false\'>' . $linkTitle . '</a>';
} else {
$content .= $linkTitle;
}
} else {
$text = sprintf(
$this->getLanguageService()->sL(self::LLPATH . 'pagemodule.recordNotAvailable'),
$id
);
$content = $this->generateCallout($text);
}
return $content;
}
/**
* Get order settings
*
* @return void
*/
public function getOrderSettings(): void
{
$orderField = $this->getFieldFromFlexform('settings.orderBy');
if (!empty($orderField)) {
$text = $this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.orderBy.' . $orderField);
// Order direction (asc, desc)
$orderDirection = $this->getOrderDirectionSetting();
if ($orderDirection) {
$text .= ', ' . strtolower($orderDirection);
}
// Top news first
$topNews = $this->getTopNewsFirstSetting();
if ($topNews) {
$text .= '<br />' . $topNews;
}
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.orderBy'),
$text
];
}
}
/**
* Get order direction
*
* @return string
*/
public function getOrderDirectionSetting(): string
{
$text = '';
$orderDirection = $this->getFieldFromFlexform('settings.orderDirection');
if (!empty($orderDirection)) {
$text = $this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.orderDirection.' . $orderDirection);
}
return $text;
}
/**
* Get topNewsFirst setting
*
* @return string
*/
public function getTopNewsFirstSetting(): string
{
$text = '';
$topNewsSetting = (int)$this->getFieldFromFlexform('settings.topNewsFirst', 'additional');
if ($topNewsSetting === 1) {
$text = $this->getLanguageService()->sL(self::LLPATH . 'flexforms_additional.topNewsFirst');
}
return $text;
}
/**
* Render category settings
*
* @param bool $showCategoryMode show the category conjunction
*
* @return void
*/
public function getCategorySettings($showCategoryMode = true): void
{
$categories = GeneralUtility::intExplode(',', $this->getFieldFromFlexform('settings.categories') ?? '', true);
if (count($categories) > 0) {
$categoriesOut = [];
foreach ($categories as $id) {
$categoriesOut[] = $this->getRecordData($id, 'sys_category');
}
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.categories'),
implode(', ', $categoriesOut)
];
// Category mode
if ($showCategoryMode) {
$categoryModeSelection = $this->getFieldFromFlexform('settings.categoryConjunction');
if (empty($categoryModeSelection)) {
$categoryMode = $this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.categoryConjunction.all');
} else {
$categoryMode = $this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.categoryConjunction.' . $categoryModeSelection);
}
if (count($categories) > 0 && empty($categoryModeSelection)) {
$categoryMode = $this->generateCallout($categoryMode);
} else {
$categoryMode = htmlspecialchars($categoryMode);
}
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.categoryConjunction'),
$categoryMode
];
}
$includeSubcategories = $this->getFieldFromFlexform('settings.includeSubCategories');
if ($includeSubcategories) {
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.includeSubCategories'),
'<i class="fa fa-check"></i>'
];
}
}
}
/**
* Get the restriction for tags
*
* @return void
*/
public function getTagRestrictionSetting()
{
$tags = GeneralUtility::intExplode(',', $this->getFieldFromFlexform('settings.tags', 'additional') ?? '', true);
if (count($tags) === 0) {
return;
}
$categoryTitles = [];
foreach ($tags as $id) {
$categoryTitles[] = $this->getRecordData($id, 'tx_news_domain_model_tag');
}
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_additional.tags'),
implode(', ', $categoryTitles)
];
}
/**
* Render offset & limit configuration
*
* @return void
*/
public function getOffsetLimitSettings(): void
{
$offset = $this->getFieldFromFlexform('settings.offset', 'additional');
$limit = $this->getFieldFromFlexform('settings.limit', 'additional');
$hidePagination = $this->getFieldFromFlexform('settings.hidePagination', 'additional');
if ($offset) {
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_additional.offset'),
$offset
];
}
if ($limit) {
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_additional.limit'),
$limit
];
}
if ($hidePagination) {
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_additional.hidePagination'),
'<i class="fa fa-check"></i>'
];
}
}
/**
* Render date menu configuration
*
* @return void
*/
public function getDateMenuSettings(): void
{
$dateMenuField = $this->getFieldFromFlexform('settings.dateField');
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.dateField'),
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.dateField.' . $dateMenuField)
];
}
/**
* Render time restriction configuration
*
* @return void
*/
public function getTimeRestrictionSetting(): void
{
$timeRestriction = $this->getFieldFromFlexform('settings.timeRestriction');
if (!empty($timeRestriction)) {
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.timeRestriction'),
htmlspecialchars($timeRestriction)
];
}
$timeRestrictionHigh = $this->getFieldFromFlexform('settings.timeRestrictionHigh');
if (!empty($timeRestrictionHigh)) {
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.timeRestrictionHigh'),
htmlspecialchars($timeRestrictionHigh)
];
}
}
/**
* Render top news restriction configuration
*
* @return void
*/
public function getTopNewsRestrictionSetting(): void
{
$topNewsRestriction = (int)$this->getFieldFromFlexform('settings.topNewsRestriction');
if ($topNewsRestriction > 0) {
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.topNewsRestriction'),
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.topNewsRestriction.' . $topNewsRestriction)
];
}
}
/**
* Render template layout configuration
*
* @param int $pageUid
*
* @return void
*/
public function getTemplateLayoutSettings($pageUid): void
{
$title = '';
$field = $this->getFieldFromFlexform('settings.templateLayout', 'template');
// Find correct title by looping over all options
if (!empty($field)) {
$layouts = $this->templateLayoutsUtility->getAvailableTemplateLayouts($pageUid);
foreach ($layouts as $layout) {
if ((string)$layout[1] === (string)$field) {
$title = $layout[0];
}
}
}
if (!empty($title)) {
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_template.templateLayout'),
$this->getLanguageService()->sL($title)
];
}
}
/**
* Get information if override demand setting is disabled or not
*
* @return void
*/
public function getOverrideDemandSettings(): void
{
$field = $this->getFieldFromFlexform('settings.disableOverrideDemand', 'additional');
if ($field == 1) {
$this->tableData[] = [
$this->getLanguageService()->sL(
self::LLPATH . 'flexforms_additional.disableOverrideDemand'
),
'<i class="fa fa-check"></i>'
];
}
}
/**
* Get the startingpoint
*
* @return void
*/
public function getStartingPoint(): void
{
$value = $this->getFieldFromFlexform('settings.startingpoint');
if (!empty($value)) {
$pageIds = GeneralUtility::intExplode(',', $value, true);
$pagesOut = [];
foreach ($pageIds as $id) {
$pagesOut[] = $this->getRecordData($id, 'pages');
}
$recursiveLevel = (int)$this->getFieldFromFlexform('settings.recursive');
$recursiveLevelText = '';
if ($recursiveLevel === 250) {
$recursiveLevelText = $this->getLanguageService()->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:recursive.I.5');
} elseif ($recursiveLevel > 0) {
$recursiveLevelText = $this->getLanguageService()->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:recursive.I.' . $recursiveLevel);
}
if (!empty($recursiveLevelText)) {
$recursiveLevelText = '<br />' .
htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.recursive')) . ' ' .
$recursiveLevelText;
}
$this->tableData[] = [
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.startingpoint'),
implode(', ', $pagesOut) . $recursiveLevelText
];
}
}
/**
* Get list of selected news items
*
* @return void
*/
protected function getSelectedListSetting(): void
{
$value = $this->getFieldFromFlexform('settings.selectedList');
if (!empty($value)) {
$idList = GeneralUtility::intExplode(',', $value, true);
$out = [];
foreach ($idList as $id) {
$out[] = $this->getRecordData($id, 'tx_news_domain_model_news');
}
$this->tableData[] = [
$this->getLanguageService()->sL(self::LLPATH . 'flexforms_general.selectedList'),
implode('<br>', $out)
];
}
}
/**
* Render an alert box
*
* @param string $text
* @return string
*/
protected function generateCallout($text): string
{
return '<div class="alert alert-warning">
' . htmlspecialchars($text) . '
</div>';
}
/**
* Render the settings as table for Web>Page module
* System settings are displayed in mono font
*
* @param string $header
* @param int $recordUid
* @return string
*/
protected function renderSettingsAsTable($header = '', $recordUid = 0): string
{
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$pageRenderer->loadRequireJsModule('TYPO3/CMS/News/PageLayout');
$pageRenderer->addCssFile('EXT:news/Resources/Public/Css/Backend/PageLayoutView.css');
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:news/Resources/Private/Backend/PageLayoutView.html'));
$view->assignMultiple([
'header' => $header,
'rows' => [
'above' => array_slice($this->tableData, 0, self::SETTINGS_IN_PREVIEW),
'below' => array_slice($this->tableData, self::SETTINGS_IN_PREVIEW)
],
'id' => $recordUid
]);
return $view->render();
}
/**
* Get field value from flexform configuration,
* including checks if flexform configuration is available
*
* @param string $key name of the key
* @param string $sheet name of the sheet
* @return string|null if nothing found, value if found
*/
public function getFieldFromFlexform($key, $sheet = 'sDEF'): ?string
{
$flexform = $this->flexformData;
if (isset($flexform['data'])) {
$flexform = $flexform['data'];
if (isset($flexform[$sheet]['lDEF'][$key]['vDEF'])
) {
return $flexform[$sheet]['lDEF'][$key]['vDEF'];
}
}
return null;
}
/**
* Build a backend edit link based on given record.
*
* @param array $row Current record row from database.
* @param int $currentPageUid current page uid
* @return string Link to open an edit window for record.
* @see \TYPO3\CMS\Backend\Utility\BackendUtilityCore::readPageAccess()
*/
protected function getEditLink($row, $currentPageUid): string
{
$editLink = '';
$localCalcPerms = $GLOBALS['BE_USER']->calcPerms(BackendUtilityCore::getRecord('pages', $row['uid']));
$permsEdit = $localCalcPerms & Permission::PAGE_EDIT;
if ($permsEdit) {
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$returnUrl = $uriBuilder->buildUriFromRoute('web_layout', ['id' => $currentPageUid]);
$editLink = $uriBuilder->buildUriFromRoute('web_layout', [
'id' => $row['uid'],
'returnUrl' => $returnUrl
]);
}
return $editLink;
}
/**
* Return language service instance
*
* @return \TYPO3\CMS\Core\Localization\LanguageService
*/
public function getLanguageService(): \TYPO3\CMS\Core\Localization\LanguageService
{
return $GLOBALS['LANG'];
}
/**
* Get the DocumentTemplate
*
* @return DocumentTemplate
*/
protected function getDocumentTemplate(): DocumentTemplate
{
return $GLOBALS['TBE_TEMPLATE'];
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace GeorgRinger\News\Jobs;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Abstract Import job
*/
abstract class AbstractImportJob implements ImportJobInterface
{
/**
* @var \GeorgRinger\News\Service\Import\DataProviderServiceInterface
*/
protected $importDataProviderService;
/**
* @var \TYPO3\CMS\Core\SingletonInterface
*/
protected $importService;
/**
* @var array
*/
protected $importServiceSettings = [];
/**
* @var array
*/
protected $importItemOverwrite = [];
/*
* @var int
*/
protected $numberOfRecordsPerRun;
/**
* @var int
*/
protected $increaseOffsetPerRunBy;
/**
* Get number of runs
*
* @return int
*/
public function getNumberOfRecordsPerRun(): int
{
// If not explicit defined by the job we import all records at once.
if ($this->numberOfRecordsPerRun === null) {
$this->numberOfRecordsPerRun = $this->importDataProviderService->getTotalRecordCount();
}
return $this->numberOfRecordsPerRun;
}
/**
* Checks if this job is enabled. Do perform some checks her if you need to.
* E.g. check if a certain extension is loaded or similar.
*
* @return bool
*/
public function isEnabled(): bool
{
return true;
}
/**
* Get job info.
*
* @return array
*/
public function getInfo(): array
{
$totalRecordCount = (int)$this->importDataProviderService->getTotalRecordCount();
$info = [
'totalRecordCount' => $totalRecordCount,
'runsToComplete' => $totalRecordCount > 0 ? (ceil($totalRecordCount / $this->getNumberOfRecordsPerRun())) : 0,
'increaseOffsetPerRunBy' => $this->getNumberOfRecordsPerRun(),
];
return $info;
}
/**
* The actual run method.
*
* @param int $offset
*
* @return void
*/
public function run($offset)
{
$importData = $this->importDataProviderService->getImportData($offset, $this->getNumberOfRecordsPerRun());
$this->importService->import($importData, $this->importItemOverwrite, $this->importServiceSettings);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace GeorgRinger\News\Jobs;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Import job interface
*/
interface ImportJobInterface
{
public function getNumberOfRecordsPerRun();
public function getInfo();
public function isEnabled();
public function run($offset);
}

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace GeorgRinger\News\Pagination;
use TYPO3\CMS\Core\Pagination\AbstractPaginator;
abstract class CustomAbstractPaginator extends AbstractPaginator
{
/**
* @var int
*/
protected $currentPageNumber = 1;
/**
* @var int
*/
protected $itemsPerPage = 10;
protected $initialOffset = 0;
protected $initialLimit = 0;
/**
* This method is the heart of the pagination. It updates all internal params and then calls the
* {@see updatePaginatedItems} method which must update the set of paginated items.
*/
protected function updateInternalState(): void
{
// offset
if ($this->currentPageNumber > 1) {
$offset = ($this->itemsPerPage * ($this->currentPageNumber - 1));
$offset += $this->initialOffset;
} elseif ($this->initialOffset > 0) {
$offset = $this->initialOffset;
} else {
$offset = 0;
}
// limit
$limit = $this->itemsPerPage;
$totalAmountOfItems = $this->getTotalAmountOfItems();
/*
* If the total amount of items is zero, then the number of pages is mathematically zero as
* well. As that looks strange in the frontend, the number of pages is forced to be at least
* one.
*/
$this->numberOfPages = max(1, (int)ceil($totalAmountOfItems / $this->itemsPerPage));
/*
* To prevent empty results in case the given current page number exceeds the maximum number
* of pages, we set the current page number to the last page and update the internal state
* with this value again. Such situation should in the first place be prevented by not allowing
* those values to be passed, e.g. by using the "max" attribute in the view. However there are
* valid cases. For example when a user deletes a record while the pagination is already visible
* to another user with, until then, a valid "max" value. Passing invalid values unintentionally
* should therefore just silently be resolved.
*/
if ($this->currentPageNumber > $this->numberOfPages) {
$this->currentPageNumber = $this->numberOfPages;
$this->updateInternalState();
return;
}
$isUpdated = false;
if ($this->currentPageNumber === $this->numberOfPages && $this->initialLimit > 0) {
$difference = $this->initialLimit - ((integer)($this->itemsPerPage * ($this->currentPageNumber - 1)));
if ($difference > 0) {
$this->updatePaginatedItems($difference, $offset);
$isUpdated = true;
$totalAmountOfItems = $this->getTotalAmountOfItems();
$this->numberOfPages = max(1, (int)ceil($totalAmountOfItems / $this->itemsPerPage));
}
}
if (!$isUpdated) {
$this->updatePaginatedItems($limit, $offset);
}
if (!$this->hasItemsOnCurrentPage()) {
$this->keyOfFirstPaginatedItem = 0;
$this->keyOfLastPaginatedItem = 0;
return;
}
$indexOfLastPaginatedItem = min($offset + $this->itemsPerPage, $totalAmountOfItems);
$this->keyOfFirstPaginatedItem = $offset;
$this->keyOfLastPaginatedItem = $indexOfLastPaginatedItem - 1;
}
public function getCurrentPageNumber(): int
{
return $this->currentPageNumber;
}
protected function setItemsPerPage(int $itemsPerPage): void
{
if ($itemsPerPage < 1) {
throw new \InvalidArgumentException(
'Argument $itemsPerPage must be greater than 0',
1573061766
);
}
$this->itemsPerPage = $itemsPerPage;
}
protected function setCurrentPageNumber(int $currentPageNumber): void
{
if ($currentPageNumber < 1) {
throw new \InvalidArgumentException(
'Argument $currentPageNumber must be greater than 0',
1573047338
);
}
$this->currentPageNumber = $currentPageNumber;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
namespace GeorgRinger\News\Pagination;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
final class QueryResultPaginator extends CustomAbstractPaginator
{
/**
* @var QueryResultInterface
*/
private $queryResult;
/**
* @var QueryResultInterface
*/
private $paginatedQueryResult;
public function __construct(
QueryResultInterface $queryResult,
int $currentPageNumber = 1,
int $itemsPerPage = 10,
int $initialLimit = 0,
int $initialOffset = 0
) {
$this->queryResult = $queryResult;
$this->setCurrentPageNumber($currentPageNumber);
$this->setItemsPerPage($itemsPerPage);
$this->initialLimit = $initialLimit;
$this->initialOffset = $initialOffset;
$this->updateInternalState();
}
/**
* @return iterable|QueryResultInterface
*/
public function getPaginatedItems(): iterable
{
return $this->paginatedQueryResult;
}
protected function updatePaginatedItems(int $limit, int $offset): void
{
$this->paginatedQueryResult = $this->queryResult
->getQuery()
->setLimit($limit)
->setOffset($offset)
->execute();
}
protected function getTotalAmountOfItems(): int
{
return count($this->queryResult);
}
protected function getAmountOfItemsOnCurrentPage(): int
{
return count($this->paginatedQueryResult);
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Seo;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor;
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;
/**
* Remove the hreflang for news in strict mode with no translations
*/
class HrefLangEvent
{
/** @var ContentObjectRenderer */
public $cObj;
/** @var LanguageMenuProcessor */
protected $languageMenuProcessor;
public function __construct(ContentObjectRenderer $cObj, LanguageMenuProcessor $languageMenuProcessor)
{
$this->cObj = $cObj;
$this->languageMenuProcessor = $languageMenuProcessor;
}
public function __invoke(ModifyHrefLangTagsEvent $event): void
{
$newsAvailabilityChecker = GeneralUtility::makeInstance(NewsAvailability::class);
if ($newsAvailabilityChecker->getNewsIdFromRequest() > 0) {
$allHrefLangs = $event->getHrefLangs();
$languages = $this->languageMenuProcessor->process($this->cObj, [], [], []);
$errorTriggered = false;
foreach ($languages['languagemenu'] as $language) {
$hreflangKey = $language['hreflang'];
// skip all languages which are not used in hreflang
if (!isset($allHrefLangs[$hreflangKey]) || $hreflangKey === 'x-default') {
continue;
}
try {
$check = $newsAvailabilityChecker->check($language['languageId']);
if (!$check) {
unset($allHrefLangs[$hreflangKey]);
}
} catch (\UnexpectedValueException $e) {
$errorTriggered = true;
}
}
if (!$errorTriggered) {
if (count($allHrefLangs) <= 2) {
unset($allHrefLangs['x-default']);
}
$event->setHrefLangs($allHrefLangs);
}
}
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Seo;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Check if a news record is available
*/
class NewsAvailability
{
/**
* @param int $languageId
* @param int $newsId
* @return bool
*/
public function check(int $languageId, int $newsId = 0): bool
{
// get it from current request
if ($newsId === 0) {
$newsId = $this->getNewsIdFromRequest();
}
if ($newsId === 0) {
throw new \UnexpectedValueException('No news id provided', 1586431984);
}
/** @var SiteInterface $site */
$site = $this->getRequest()->getAttribute('site');
$allAvailableLanguagesOfSite = $site->getAllLanguages();
$targetLanguage = $this->getLanguageFromAllLanguages($allAvailableLanguagesOfSite, $languageId);
if (!$targetLanguage) {
throw new \UnexpectedValueException('Target language could not be found', 1586431985);
}
return $this->mustBeIncluded($newsId, $targetLanguage);
}
protected function mustBeIncluded(int $newsId, SiteLanguage $language): bool
{
if ($language->getFallbackType() === 'strict') {
$newsRecord = $this->getNewsRecord($newsId, $language->getLanguageId());
if (!is_array($newsRecord) || empty($newsRecord)) {
return false;
}
}
return true;
}
/**
* @param SiteLanguage[] $allLanguages
* @param int $languageId
*/
protected function getLanguageFromAllLanguages(array $allLanguages, int $languageId): ?SiteLanguage
{
foreach ($allLanguages as $siteLanguage) {
if ($siteLanguage->getLanguageId() === $languageId) {
return $siteLanguage;
}
}
return null;
}
protected function getNewsRecord(int $newsId, int $language)
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_news_domain_model_news');
if ($language === 0) {
$where = [
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT))
),
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT))
];
} else {
$where = [
$queryBuilder->expr()->orX(
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT))
),
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT))
),
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($language, \PDO::PARAM_INT))
)
)
];
}
$row = $queryBuilder
->select('uid', 'l10n_parent', 'sys_language_uid')
->from('tx_news_domain_model_news')
->where(...$where)
->execute()
->fetch();
return $row ?: null;
}
/**
* @return ServerRequestInterface
*/
protected function getRequest(): ServerRequestInterface
{
return $GLOBALS['TYPO3_REQUEST'];
}
public function getNewsIdFromRequest(): int
{
$newsId = 0;
/** @var PageArguments $pageArguments */
$pageArguments = $this->getRequest()->getAttribute('routing');
if (isset($pageArguments->getRouteArguments()['tx_news_pi1']['news'])) {
$newsId = (int)$pageArguments->getRouteArguments()['tx_news_pi1']['news'];
} elseif (isset($this->getRequest()->getQueryParams()['tx_news_pi1']['news'])) {
$newsId = (int)$this->getRequest()->getQueryParams()['tx_news_pi1']['news'];
}
return $newsId;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Seo;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\News;
use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Generate page title based on properties of the news model
*/
class NewsTitleProvider extends AbstractPageTitleProvider
{
private const DEFAULT_PROPERTIES = 'title';
private const DEFAULT_GLUE = '" "';
/**
* @param News $news
* @param array $configuration
*/
public function setTitleByNews(News $news, array $configuration = []): void
{
$title = '';
$fields = GeneralUtility::trimExplode(',', $configuration['properties'] ?? self::DEFAULT_PROPERTIES, true);
foreach ($fields as $field) {
$getter = 'get' . ucfirst($field);
$value = $news->$getter();
if ($value) {
$title = $value;
break;
}
}
if ($title) {
$this->title = $title;
}
}
public function setTitle(string $title): void
{
$this->title = $title;
}
}

View File

@@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Seo;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\WorkspaceAspect;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Seo\XmlSitemap\AbstractXmlSitemapDataProvider;
use TYPO3\CMS\Seo\XmlSitemap\Exception\MissingConfigurationException;
/**
* Generate sitemap for news records
*/
class NewsXmlSitemapDataProvider extends AbstractXmlSitemapDataProvider
{
/**
* @param ServerRequestInterface $request
* @param string $key
* @param array $config
* @param ContentObjectRenderer|null $cObj
* @throws MissingConfigurationException
*/
public function __construct(ServerRequestInterface $request, string $key, array $config = [], ContentObjectRenderer $cObj = null)
{
parent::__construct($request, $key, $config, $cObj);
$this->generateItems();
}
/**
* @throws MissingConfigurationException
*/
public function generateItems(): void
{
$table = 'tx_news_domain_model_news';
$pids = !empty($this->config['pid']) ? GeneralUtility::intExplode(',', $this->config['pid']) : [];
$lastModifiedField = $this->config['lastModifiedField'] ?? 'tstamp';
$sortField = $this->config['sortField'] ?? 'datetime';
$forGoogleNews = $this->config['googleNews'] ?? false;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable($table);
$constraints = [];
if (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField'])) {
$constraints[] = $queryBuilder->expr()->in(
$GLOBALS['TCA'][$table]['ctrl']['languageField'],
[
-1, // All languages
$this->getLanguageId() // Current language
]
);
}
if (!empty($pids)) {
$recursiveLevel = isset($this->config['recursive']) ? (int)$this->config['recursive'] : 0;
if ($recursiveLevel) {
$newList = [];
foreach ($pids as $pid) {
$list = $this->cObj->getTreeList($pid, $recursiveLevel);
if ($list) {
$newList = array_merge($newList, explode(',', $list));
}
}
$pids = array_merge($pids, $newList);
}
$constraints[] = $queryBuilder->expr()->in('pid', $pids);
}
if ($forGoogleNews) {
$constraints[] = $queryBuilder->expr()->gt($sortField, (new \DateTime('-2 days'))->getTimestamp());
}
if (!empty($this->config['excludedTypes'])) {
$excludedTypes = GeneralUtility::trimExplode(',', $this->config['excludedTypes'], true);
if (!empty($excludedTypes)) {
$constraints[] = $queryBuilder->expr()->notIn(
'type',
$queryBuilder->createNamedParameter($excludedTypes, Connection::PARAM_STR_ARRAY)
);
}
}
if (!empty($this->config['additionalWhere'])) {
$constraints[] = $this->config['additionalWhere'];
}
$queryBuilder->getRestrictions()->add(
GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getCurrentWorkspaceAspect()->getId())
);
$queryBuilder->select('*')
->from($table);
if (!empty($constraints)) {
$queryBuilder->where(
...$constraints
);
}
$rows = $queryBuilder->orderBy($sortField, $forGoogleNews ? 'DESC' : 'ASC')
->execute()
->fetchAll();
foreach ($rows as $row) {
$this->items[] = [
'data' => $row,
'lastMod' => (int)$row[$lastModifiedField],
'priority' => 0.5
];
}
}
/**
* @param array $data
* @return array
*/
protected function defineUrl(array $data): array
{
$pageId = $this->config['url']['pageId'] ?? $GLOBALS['TSFE']->id;
if ($this->config['url']['useCategorySinglePid'] && $pageIdFromCategory = $this->getSinglePidFromCategory($data['data']['uid'])) {
$pageId = $pageIdFromCategory;
}
$additionalParams = [];
$additionalParams = $this->getUrlFieldParameterMap($additionalParams, $data['data']);
$additionalParams = $this->getUrlAdditionalParams($additionalParams);
if (!empty($this->config['url']['hrDate']) && !empty($data['data']['datetime'])) {
// adjust timezone (database field is UTC)
$timezoneCorrectedDatetime = (int)$data['data']['datetime'] + date('Z', (int)$data['data']['datetime']);
$dateTime = \DateTime::createFromFormat('U', (string)$timezoneCorrectedDatetime);
if (!empty($this->config['url']['hrDate']['day'])) {
$additionalParams['tx_news_pi1[day]'] = $dateTime->format($this->config['url']['hrDate']['day']);
}
if (!empty($this->config['url']['hrDate']['month'])) {
$additionalParams['tx_news_pi1[month]'] = $dateTime->format($this->config['url']['hrDate']['month']);
}
if (!empty($this->config['url']['hrDate']['year'])) {
$additionalParams['tx_news_pi1[year]'] = $dateTime->format($this->config['url']['hrDate']['year']);
}
}
$additionalParamsString = http_build_query(
$additionalParams,
'',
'&',
PHP_QUERY_RFC3986
);
$typoLinkConfig = [
'parameter' => $pageId,
'additionalParams' => $additionalParamsString ? '&' . $additionalParamsString : '',
'forceAbsoluteUrl' => 1,
];
$data['loc'] = $this->cObj->typoLink_URL($typoLinkConfig);
return $data;
}
/**
* Obtains a pid for the single view from the category.
*
* @param int $newsId
* @return int
*/
protected function getSinglePidFromCategory(int $newsId): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$categoryRecord = $queryBuilder
->select('title', 'single_pid')
->from('sys_category')
->leftJoin(
'sys_category',
'sys_category_record_mm',
'sys_category_record_mm',
$queryBuilder->expr()->eq('sys_category_record_mm.uid_local', $queryBuilder->quoteIdentifier('sys_category.uid'))
)
->where(
$queryBuilder->expr()->eq('sys_category_record_mm.tablenames', $queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)),
$queryBuilder->expr()->gt('sys_category.single_pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('sys_category_record_mm.uid_foreign', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT))
)
->setMaxResults(1)
->execute()->fetch();
return (int)$categoryRecord['single_pid'];
}
/**
* @param array $additionalParams
* @param array $data
* @return array
*/
protected function getUrlFieldParameterMap(array $additionalParams, array $data): array
{
if (!empty($this->config['url']['fieldToParameterMap']) &&
\is_array($this->config['url']['fieldToParameterMap'])) {
foreach ($this->config['url']['fieldToParameterMap'] as $field => $urlPart) {
$additionalParams[$urlPart] = $data[$field];
}
}
return $additionalParams;
}
/**
* @param array $additionalParams
* @return array
*/
protected function getUrlAdditionalParams(array $additionalParams): array
{
if (!empty($this->config['url']['additionalGetParameters']) &&
is_array($this->config['url']['additionalGetParameters'])) {
foreach ($this->config['url']['additionalGetParameters'] as $extension => $extensionConfig) {
foreach ($extensionConfig as $key => $value) {
$additionalParams[$extension . '[' . $key . ']'] = $value;
}
}
}
return $additionalParams;
}
/**
* @return int
* @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
*/
protected function getLanguageId(): int
{
$context = GeneralUtility::makeInstance(Context::class);
return (int)$context->getPropertyFromAspect('language', 'id');
}
/**
* @return WorkspaceAspect
*/
protected function getCurrentWorkspaceAspect(): WorkspaceAspect
{
return GeneralUtility::makeInstance(Context::class)->getAspect('workspace');
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace GeorgRinger\News\Service;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Domain\Model\Dto\EmConfiguration;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Service for access control related stuff
*/
class AccessControlService
{
/**
* Check if a user has access to all categories of a news record
*
* @param array $newsRecord
* @return bool
*/
public static function userHasCategoryPermissionsForRecord(array $newsRecord): bool
{
$settings = GeneralUtility::makeInstance(EmConfiguration::class);
if (!$settings->getCategoryBeGroupTceFormsRestriction()) {
return true;
}
if (self::getBackendUser()->isAdmin()) {
// an admin may edit all news
return true;
}
// If there are any categories with denied access, the user has no permission
if (count(self::getAccessDeniedCategories($newsRecord))) {
return false;
}
return true;
}
/**
* Get an array with the uid and title of all categories the user doesn't have access to
*
* @param array $newsRecord
* @return array
*/
public static function getAccessDeniedCategories(array $newsRecord): array
{
if (self::getBackendUser()->isAdmin()) {
// an admin may edit all news so no categories without access
return [];
}
// no category mounts set means access to all
$backendUserCategories = self::getBackendUser()->getCategoryMountPoints();
if ($backendUserCategories === []) {
return [];
}
$catService = GeneralUtility::makeInstance(CategoryService::class);
$subCategories = $catService::getChildrenCategories(implode(',', $backendUserCategories));
if (!empty($subCategories)) {
$backendUserCategories = explode(',', $subCategories);
}
$newsRecordCategories = self::getCategoriesForNewsRecord($newsRecord);
// Remove categories the user has access to
foreach ($newsRecordCategories as $key => $newsRecordCategory) {
if (in_array($newsRecordCategory['uid'], $backendUserCategories)) {
unset($newsRecordCategories[$key]);
}
}
return $newsRecordCategories;
}
/**
* Get all categories for a news record respecting l10n_mode
*
* @param array $newsRecord
* @return array
*/
public static function getCategoriesForNewsRecord($newsRecord): array
{
// determine localization overlay mode to select categories either from parent or localized record
if ($newsRecord['sys_language_uid'] > 0 && $newsRecord['l10n_parent'] > 0) {
// localized version of a news record
$categoryL10nMode = $GLOBALS['TCA']['tx_news_domain_model_news']['columns']['categories']['l10n_mode'];
if ($categoryL10nMode === 'mergeIfNotBlank') {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category_record_mm');
$newsRecordCategoriesCount = $queryBuilder->count('*')
->from('sys_category_record_mm')
->where(
$queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($newsRecord['uid'], \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)),
$queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('categories', \PDO::PARAM_STR))
)
->execute()
->fetchColumn(0);
if ($newsRecordCategoriesCount > 0) {
// take categories from localized version
$newsRecordUid = $newsRecord['uid'];
} else {
// inherit categories from parent
$newsRecordUid = $newsRecord['l10n_parent'];
}
} elseif ($categoryL10nMode === 'exclude') {
// exclude: The localized version inherits the categories of the parent
$newsRecordUid = $newsRecord['l10n_parent'];
} else {
// noCopy/prefixLangTitle: no inheritance
$newsRecordUid = $newsRecord['uid'];
}
} else {
$newsRecordUid = $newsRecord['uid'];
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$queryBuilder->getRestrictions()
->removeByType(StartTimeRestriction::class)
->removeByType(HiddenRestriction::class)
->removeByType(EndTimeRestriction::class);
$res = $queryBuilder
->select('sys_category_record_mm.uid_local', 'sys_category.title')
->from('sys_category')
->leftJoin(
'sys_category',
'sys_category_record_mm',
'sys_category_record_mm',
$queryBuilder->expr()->eq('sys_category_record_mm.uid_local', $queryBuilder->quoteIdentifier('sys_category.uid'))
)
->where(
$queryBuilder->expr()->eq('sys_category_record_mm.tablenames', $queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)),
$queryBuilder->expr()->eq('sys_category_record_mm.fieldname', $queryBuilder->createNamedParameter('categories', \PDO::PARAM_STR)),
$queryBuilder->expr()->eq('sys_category_record_mm.uid_foreign', $queryBuilder->createNamedParameter($newsRecordUid, \PDO::PARAM_INT))
)
->execute();
$categories = [];
while ($row =$res->fetch()) {
$categories[] = [
'uid' => $row['uid_local'],
'title' => $row['title']
];
}
return $categories;
}
/**
* Returns the current BE user.
*
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
*/
protected static function getBackendUser(): \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace GeorgRinger\News\Service;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Service for category related stuff
*/
class CategoryService
{
/**
* Get child categories by calling recursive function
* and using the caching framework to save some queries
*
* @param string $idList list of category ids to start
* @param int $counter
* @param string $additionalWhere additional where clause
* @param bool $removeGivenIdListFromResult remove the given id list from result
* @return string comma separated list of category ids
*/
public static function getChildrenCategories(
$idList,
$counter = 0,
$additionalWhere = '',
$removeGivenIdListFromResult = false
): string {
if ($additionalWhere !== '') {
throw new \UnexpectedValueException('The argument $additionalWhere is not supported anymore');
}
$cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('news_category');
$cacheIdentifier = sha1('children' . $idList);
$entry = $cache->get($cacheIdentifier);
if (!$entry) {
$entry = self::getChildrenCategoriesRecursive($idList, $counter, $additionalWhere);
$cache->set($cacheIdentifier, $entry);
}
if ($removeGivenIdListFromResult) {
$entry = self::removeValuesFromString($entry, $idList);
}
return $entry;
}
/**
* Remove values of a comma separated list from another comma separated list
*
* @param string $result string comma separated list
* @param $toBeRemoved string comma separated list
* @return string
*/
public static function removeValuesFromString($result, string $toBeRemoved): string
{
$resultAsArray = GeneralUtility::trimExplode(',', $result, true);
$idListAsArray = GeneralUtility::trimExplode(',', $toBeRemoved, true);
$result = implode(',', array_diff($resultAsArray, $idListAsArray));
return $result;
}
/**
* Get child categories
*
* @param string $idList list of category ids to start
* @param int $counter
* @param string $additionalWhere additional where clause
* @return string comma separated list of category ids
*/
private static function getChildrenCategoriesRecursive($idList, $counter = 0, $additionalWhere = ''): string
{
$result = [];
// add idlist to the output too
if ($counter === 0) {
$result[] = self::cleanIntList($idList);
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$res = $queryBuilder
->select('uid')
->from('sys_category')
->where(
$queryBuilder->expr()->in('parent', $queryBuilder->createNamedParameter(array_map('intval', explode(',', $idList)), Connection::PARAM_INT_ARRAY))
)
->execute();
while (($row = $res->fetch())) {
$counter++;
if ($counter > 10000) {
GeneralUtility::makeInstance(TimeTracker::class)->setTSlogMessage('EXT:news: one or more recursive categories where found');
return implode(',', $result);
}
$subcategories = self::getChildrenCategoriesRecursive($row['uid'], $counter);
$result[] = $row['uid'] . ($subcategories ? ',' . $subcategories : '');
}
$result = implode(',', $result);
return $result;
}
/**
* Translate a category record in the backend
*
* @param string $default default label
* @param array $row category record
* @return string
* @throws \UnexpectedValueException
*/
public static function translateCategoryRecord($default, array $row = []): string
{
if (TYPO3_MODE !== 'BE') {
throw new \UnexpectedValueException('TYPO3 Mode must be BE');
}
$overlayLanguage = (int)($GLOBALS['BE_USER']->uc['newsoverlay'] ?? 0);
$title = '';
if ($row['uid'] > 0 && $overlayLanguage > 0 && !isset($row['sys_language_uid'])) {
$row = BackendUtility::getRecord('sys_category', $row['uid']);
}
if ($row['uid'] > 0 && $overlayLanguage > 0 && $row['sys_language_uid'] === 0) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$overlayRecord = $queryBuilder
->select('title')
->from('sys_category')
->where(
$queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($overlayLanguage, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT))
)
->setMaxResults(1)
->execute()->fetch();
if (is_array($overlayRecord) && !empty($overlayRecord)) {
$title = $overlayRecord['title'] . ' (' . $row['title'] . ')';
}
}
$title = $title ?: $default ?: '';
return $title;
}
/**
* Clean list of integers
*
* @param string $list
* @return string
*/
private static function cleanIntList($list): string
{
return implode(',', GeneralUtility::intExplode(',', $list));
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace GeorgRinger\News\Service\Import;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Import Service interface
*/
interface DataProviderServiceInterface
{
public function getTotalRecordCount();
public function getImportData();
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Service;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
class LinkHandlerTargetPageService
{
/** @var ContentObjectRenderer */
public $cObj;
public function process(string $content = '', array $configuration = []): int
{
$fallbackPageId = (int)($configuration['fallback'] ?? 0);
$newsId = (int)$this->cObj->stdWrapValue('news', $configuration, null);
if ($newsId === 0) {
return $fallbackPageId;
}
$singlePid = $this->getSinglePidFromCategory($newsId);
return $singlePid ?: $fallbackPageId;
}
/**
* Obtains a pid for the single view from the category.
*
* @param int $newsId
* @return int
*/
protected function getSinglePidFromCategory(int $newsId): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$categoryRecord = $queryBuilder
->select('title', 'single_pid')
->from('sys_category')
->leftJoin(
'sys_category',
'sys_category_record_mm',
'sys_category_record_mm',
$queryBuilder->expr()->eq('sys_category_record_mm.uid_local', $queryBuilder->quoteIdentifier('sys_category.uid'))
)
->where(
$queryBuilder->expr()->eq('sys_category_record_mm.tablenames', $queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)),
$queryBuilder->expr()->gt('sys_category.single_pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('sys_category_record_mm.sorting_foreign', $queryBuilder->createNamedParameter($newsId, \PDO::PARAM_INT))
)
->orderBy('sys_category_record_mm.sorting')
->setMaxResults(1)
->execute()->fetch();
return (int)$categoryRecord['single_pid'];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace GeorgRinger\News\Service;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Provide a way to get the configuration just everywhere
*
* Example
* $pluginSettingsService =
* $this->objectManager->get('GeorgRinger\\News\\Service\\SettingsService');
* \TYPO3\CMS\Core\Utility\GeneralUtility::print_array($pluginSettingsService->getSettings());
*
* If objectManager is not available:
* http://forge.typo3.org/projects/typo3v4-mvc/wiki/
* Dependency_Injection_%28DI%29#Creating-Prototype-Objects-through-the-Object-Manager
*/
class SettingsService
{
/**
* @var mixed
*/
protected $settings;
/**
* @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
*/
protected $configurationManager;
/**
* Injects the Configuration Manager and loads the settings
*
* @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager An instance of the Configuration Manager
*
* @return void
*/
public function injectConfigurationManager(
ConfigurationManagerInterface $configurationManager
): void {
$this->configurationManager = $configurationManager;
}
/**
* Returns all settings.
*
* @return array
*/
public function getSettings(): array
{
if ($this->settings === null) {
$this->settings = $this->configurationManager->getConfiguration(
ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS,
'News',
'Pi1'
);
}
return $this->settings;
}
/**
* Returns the settings at path $path, which is separated by ".",
* e.g. "pages.uid".
* "pages.uid" would return $this->settings['pages']['uid'].
*
* If the path is invalid or no entry is found, false is returned.
*
* @param string $path
* @return mixed
*/
public function getByPath($path)
{
return ObjectAccess::getPropertyPath($this->getSettings(), $path);
}
}

View File

@@ -0,0 +1,305 @@
<?php
namespace GeorgRinger\News\Service;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
class SlugService
{
/** @var NewsSlugHelper */
protected $slugService;
public function __construct()
{
$fieldConfig = $GLOBALS['TCA']['tx_news_domain_model_news']['columns']['path_segment']['config'];
$this->slugService = GeneralUtility::makeInstance(SlugHelper::class, 'tx_news_domain_model_news', 'path_segment', $fieldConfig);
}
/**
* @return int
*/
public function countOfSlugUpdates(): int
{
/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_news_domain_model_news');
$queryBuilder->getRestrictions()->removeAll();
$elementCount = $queryBuilder->count('uid')
->from('tx_news_domain_model_news')
->where(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq('path_segment', $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)),
$queryBuilder->expr()->isNull('path_segment')
)
)
->execute()->fetchColumn(0);
return $elementCount;
}
/**
* @return array
*/
public function performUpdates(): array
{
$databaseQueries = [];
/** @var Connection $connection */
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_news_domain_model_news');
/** @var QueryBuilder $queryBuilder */
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder->select('*')
->from('tx_news_domain_model_news')
->where(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq('path_segment', $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)),
$queryBuilder->expr()->isNull('path_segment')
)
)
->execute();
while ($record = $statement->fetch()) {
if ((string)$record['title'] !== '') {
$slug = $this->slugService->generate($record, $record['pid']);
/** @var QueryBuilder $queryBuilder */
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->update('tx_news_domain_model_news')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
)
)
->set('path_segment', $this->getUniqueValue($record['uid'], $record['sys_language_uid'], $slug));
$databaseQueries[] = $queryBuilder->getSQL();
$queryBuilder->execute();
}
}
return $databaseQueries;
}
/**
* @param int $uid
* @param string $slug
* @return string
*/
protected function getUniqueValue(int $uid, int $languageId, string $slug): string
{
$statement = $this->getUniqueCountStatement($uid, $languageId, $slug);
if ($statement->fetchColumn()) {
for ($counter = 1; $counter <= 100; $counter++) {
$newSlug = $slug . '-' . $counter;
$statement->bindValue(1, $newSlug);
$statement->execute();
if (!$statement->fetchColumn()) {
break;
}
}
}
return $newSlug ?? $slug;
}
/**
* @param int $uid
* @param int $languageId
* @param string $slug
* @return \Doctrine\DBAL\Driver\Statement|int
*/
protected function getUniqueCountStatement(int $uid, int $languageId, string $slug)
{
/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_news_domain_model_news');
/** @var DeletedRestriction $deleteRestriction */
$deleteRestriction = GeneralUtility::makeInstance(DeletedRestriction::class);
$queryBuilder->getRestrictions()->removeAll()->add($deleteRestriction);
return $queryBuilder
->count('uid')
->from('tx_news_domain_model_news')
->where(
$queryBuilder->expr()->eq(
'path_segment',
$queryBuilder->createPositionalParameter($slug, \PDO::PARAM_STR)
),
$queryBuilder->expr()->eq(
'sys_language_uid',
$queryBuilder->createPositionalParameter($languageId, \PDO::PARAM_INT)
),
$queryBuilder->expr()->neq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT))
)->execute();
}
/**
* Count valid entries from EXT:realurl table tx_realurl_uniqalias which can be migrated
* Checks also for existance of third party extension table 'tx_realurl_uniqalias'
* EXT:realurl requires not to be installed
*
* @return int
*/
public function countOfRealurlAliasMigrations(): int
{
$elementCount = 0;
// Check if table 'tx_realurl_uniqalias' exists
/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_realurl_uniqalias');
$schemaManager = $queryBuilder->getConnection()->getSchemaManager();
if ($schemaManager->tablesExist(['tx_realurl_uniqalias']) === true) {
// Count valid aliases for news
$queryBuilder->getRestrictions()->removeAll();
$elementCount = $queryBuilder->selectLiteral('COUNT(DISTINCT tx_news_domain_model_news.uid)')
->from('tx_realurl_uniqalias')
->join(
'tx_realurl_uniqalias',
'tx_news_domain_model_news',
'tx_news_domain_model_news',
$queryBuilder->expr()->eq(
'tx_realurl_uniqalias.value_id',
$queryBuilder->quoteIdentifier('tx_news_domain_model_news.uid')
)
)
->where(
$queryBuilder->expr()->andX(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq(
'tx_news_domain_model_news.path_segment',
$queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
),
$queryBuilder->expr()->isNull('tx_news_domain_model_news.path_segment')
),
$queryBuilder->expr()->eq(
'tx_news_domain_model_news.sys_language_uid',
'tx_realurl_uniqalias.lang'
),
$queryBuilder->expr()->eq(
'tx_realurl_uniqalias.tablename',
$queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)
),
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq(
'tx_realurl_uniqalias.expire',
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
),
$queryBuilder->expr()->gte(
'tx_realurl_uniqalias.expire',
$queryBuilder->createNamedParameter($GLOBALS['ACCESS_TIME'], \PDO::PARAM_INT)
)
)
)
)
->execute()->fetchColumn(0);
}
return $elementCount;
}
/**
* Perform migration of EXT:realurl unique alias into empty news slugs
*
* @return array
*/
public function performRealurlAliasMigration(): array
{
$databaseQueries = [];
// Check if table 'tx_realurl_uniqalias' exists
/** @var QueryBuilder $queryBuilderForRealurl */
$queryBuilderForRealurl = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_realurl_uniqalias');
$schemaManager = $queryBuilderForRealurl->getConnection()->getSchemaManager();
if ($schemaManager->tablesExist(['tx_realurl_uniqalias']) === true) {
/** @var Connection $connection */
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_news_domain_model_news');
$queryBuilder = $connection->createQueryBuilder();
// Get entries to update
$statement = $queryBuilder
->selectLiteral(
'DISTINCT tx_news_domain_model_news.uid, tx_realurl_uniqalias.value_alias, tx_news_domain_model_news.uid, tx_news_domain_model_news.l10n_parent,tx_news_domain_model_news.sys_language_uid'
)
->from('tx_news_domain_model_news')
->join(
'tx_news_domain_model_news',
'tx_realurl_uniqalias',
'tx_realurl_uniqalias',
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq(
'tx_news_domain_model_news.uid',
$queryBuilder->quoteIdentifier('tx_realurl_uniqalias.value_id')
),
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq(
'tx_news_domain_model_news.l10n_parent',
$queryBuilder->quoteIdentifier('tx_realurl_uniqalias.value_id')
),
$queryBuilder->expr()->eq(
'tx_news_domain_model_news.sys_language_uid',
$queryBuilder->quoteIdentifier('tx_realurl_uniqalias.lang')
)
)
)
)
->where(
$queryBuilder->expr()->andX(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq(
'tx_news_domain_model_news.path_segment',
$queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
),
$queryBuilder->expr()->isNull('tx_news_domain_model_news.path_segment')
),
$queryBuilder->expr()->eq(
'tx_news_domain_model_news.sys_language_uid',
'tx_realurl_uniqalias.lang'
),
$queryBuilder->expr()->eq(
'tx_realurl_uniqalias.tablename',
$queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)
),
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq(
'tx_realurl_uniqalias.expire',
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
),
$queryBuilder->expr()->gte(
'tx_realurl_uniqalias.expire',
$queryBuilder->createNamedParameter($GLOBALS['ACCESS_TIME'], \PDO::PARAM_INT)
)
)
)
)
->execute();
// Update entries
while ($record = $statement->fetch()) {
$slug = $this->slugService->sanitize((string)$record['value_alias']);
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->update('tx_news_domain_model_news')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
)
)
->set('path_segment', $this->getUniqueValue($record['uid'], $record['sys_language_uid'], $slug));
$databaseQueries[] = $queryBuilder->getSQL();
$queryBuilder->execute();
}
}
return $databaseQueries;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace GeorgRinger\News\TreeProvider;
use GeorgRinger\News\Service\CategoryService;
use TYPO3\CMS\Backend\Tree\SortedTreeNodeCollection;
use TYPO3\CMS\Backend\Tree\TreeNode;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeNode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* TCA tree data provider which considers
*/
class DatabaseTreeDataProvider extends \TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeDataProvider
{
/**
* Builds a complete node including children
*
* @param \TYPO3\CMS\Backend\Tree\TreeNode|\TYPO3\CMS\Backend\Tree\TreeNode $basicNode
* @param \TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeNode|null $parent
* @param int $level
* @param bool $restriction
*
* @return null|object node
*/
protected function buildRepresentationForNode(
TreeNode $basicNode,
DatabaseTreeNode $parent = null,
$level = 0,
$restriction = false
): ?object {
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
/**@param $node \TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeNode */
$node = GeneralUtility::makeInstance(DatabaseTreeNode::class);
$row = [];
if ($basicNode->getId() == 0) {
$node->setSelected(false);
$node->setExpanded(true);
$node->setLabel($GLOBALS['LANG']->sL($GLOBALS['TCA'][$this->tableName]['ctrl']['title']));
} else {
$row = BackendUtility::getRecordWSOL($this->tableName, $basicNode->getId(), '*', '', false);
if ($this->getLabelField() !== '') {
$label = CategoryService::translateCategoryRecord($row[$this->getLabelField()], $row);
$node->setLabel($label);
} else {
$node->setLabel($basicNode->getId());
}
$node->setSelected(GeneralUtility::inList($this->getSelectedList(), $basicNode->getId()));
$node->setExpanded($this->isExpanded($basicNode));
$node->setLabel($node->getLabel());
}
$node->setId($basicNode->getId());
// Break to force single category activation
if ($parent != null && $level != 0 && $this->isSingleCategoryAclActivated() && !$this->isCategoryAllowed($node)) {
return null;
}
$node->setSelectable(!GeneralUtility::inList(
$this->getNonSelectableLevelList(),
$level
) && !in_array($basicNode->getId(), $this->getItemUnselectableList()));
if (!empty($this->nodeSortValues)) {
$node->setSortValue($this->nodeSortValues[$basicNode->getId()]);
}
$node->setIcon($iconFactory->getIconForRecord($this->tableName, $row, Icon::SIZE_SMALL));
$node->setParentNode($parent);
if ($basicNode->hasChildNodes()) {
$node->setHasChildren(true);
$childNodes = GeneralUtility::makeInstance(SortedTreeNodeCollection::class);
$foundSomeChild = false;
foreach ($basicNode->getChildNodes() as $child) {
// Change in custom TreeDataProvider by adding the if clause
if ($restriction || $this->isCategoryAllowed($child)) {
$returnedChild = $this->buildRepresentationForNode($child, $node, $level + 1, true);
if (!is_null($returnedChild)) {
$foundSomeChild = true;
$childNodes->append($returnedChild);
} else {
$node->setParentNode(null);
$node->setHasChildren(false);
}
}
// Change in custom TreeDataProvider end
}
if ($foundSomeChild) {
$node->setChildNodes($childNodes);
}
}
return $node;
}
/**
* Check if given category is allowed by the access rights
*
* @param \TYPO3\CMS\Backend\Tree\TreeNode $child
* @return bool
*/
protected function isCategoryAllowed($child): bool
{
$mounts = $GLOBALS['BE_USER']->getCategoryMountPoints();
if (empty($mounts)) {
return true;
}
return in_array($child->getId(), $mounts);
}
/**
* By setting "tx_news.singleCategoryAcl = 1" in UserTsConfig
* every category needs to be activated, no recursive enabling
*
* @return bool
*/
protected function isSingleCategoryAclActivated(): bool
{
$userTsConfig = $GLOBALS['BE_USER']->getTSConfig();
if (is_array($userTsConfig['tx_news.'])
&& $userTsConfig['tx_news.']['singleCategoryAcl'] === '1'
) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Updates;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Service\SlugService;
use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
/**
* Migrate empty slugs
*/
class NewsSlugUpdater implements UpgradeWizardInterface
{
const TABLE = 'tx_news_domain_model_news';
/** @var SlugService */
protected $slugService;
/**
* NewsSlugUpdater constructor.
* @param SlugService $slugService
*/
public function __construct(
SlugService $slugService
) {
$this->slugService = $slugService;
}
public function executeUpdate(): bool
{
$this->slugService->performUpdates();
return true;
}
public function updateNecessary(): bool
{
$elementCount = $this->slugService->countOfSlugUpdates();
return $elementCount > 0;
}
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Get title
*
* @return string
*/
public function getTitle(): string
{
return 'Updates slug field "path_segment" of EXT:news records';
}
/**
* Get description
*
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Fills empty slug field "path_segment" of EXT:news records with urlized title.';
}
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'newsSlug';
}
}

View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Updates;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
/**
* Fills sys_category.slug with a proper value
*/
class PopulateCategorySlugs implements UpgradeWizardInterface
{
protected $table = 'sys_category';
protected $fieldName = 'slug';
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'sysCategorySlugs';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Introduce URL parts ("slugs") to all sys_categories';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Slugs for sys_category records';
}
/**
* Checks whether updates are required.
*
* @return bool Whether an update is required (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$updateNeeded = false;
// Check if the database table even exists
if ($this->checkIfWizardIsRequired()) {
$updateNeeded = true;
}
return $updateNeeded;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the accordant updates.
*
* @return bool Whether everything went smoothly or not
*/
public function executeUpdate(): bool
{
$this->populateSlugs();
return true;
}
/**
* Fills the database table with slugs based on the page title and its configuration.
*
* @return void
*/
protected function populateSlugs(): void
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$statement = $queryBuilder
->select('*')
->from($this->table)
->where(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq($this->fieldName, $queryBuilder->createNamedParameter('')),
$queryBuilder->expr()->isNull($this->fieldName)
)
)
// Ensure that live workspace records are handled first
->addOrderBy('t3ver_wsid', 'asc')
->addOrderBy('pid', 'asc')
->addOrderBy('sorting', 'asc')
->execute();
// Check for existing slugs from realurl
$suggestedSlugs = [];
$fieldConfig = $GLOBALS['TCA'][$this->table]['columns'][$this->fieldName]['config'];
$evalInfo = !empty($fieldConfig['eval']) ? GeneralUtility::trimExplode(',', $fieldConfig['eval'], true) : [];
$hasToBeUniqueInSite = in_array('uniqueInSite', $evalInfo, true);
$hasToBeUniqueInPid = in_array('uniqueInPid', $evalInfo, true);
$slugHelper = GeneralUtility::makeInstance(SlugHelper::class, $this->table, $this->fieldName, $fieldConfig);
while ($record = $statement->fetch()) {
$recordId = (int)$record['uid'];
$pid = (int)$record['pid'];
$languageId = (int)$record['sys_language_uid'];
$recordInDefaultLanguage = $languageId > 0 ? (int)$record['l10n_parent'] : $recordId;
$slug = $suggestedSlugs[$recordInDefaultLanguage][$languageId] ?? '';
if (empty($slug)) {
if ($pid === -1) {
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$liveVersion = $queryBuilder
->select('pid')
->from($this->table)
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($record['t3ver_oid'], \PDO::PARAM_INT))
)->execute()->fetch();
$pid = (int)$liveVersion['pid'];
}
$slug = $slugHelper->generate($record, $pid);
}
$state = RecordStateFactory::forName($this->table)
->fromArray($record, $pid, $recordId);
if ($hasToBeUniqueInSite && !$slugHelper->isUniqueInSite($slug, $state)) {
$slug = $slugHelper->buildSlugForUniqueInSite($slug, $state);
}
if ($hasToBeUniqueInPid && !$slugHelper->isUniqueInPid($slug, $state)) {
$slug = $slugHelper->buildSlugForUniqueInPid($slug, $state);
}
$connection->update(
$this->table,
[$this->fieldName => $slug],
['uid' => $recordId]
);
}
}
/**
* Check if there are record within database table with an empty "slug" field.
*
* @return bool
* @throws \InvalidArgumentException
*/
protected function checkIfWizardIsRequired(): bool
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$numberOfEntries = $queryBuilder
->count('uid')
->from($this->table)
->where(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq($this->fieldName, $queryBuilder->createNamedParameter('')),
$queryBuilder->expr()->isNull($this->fieldName)
)
)
->execute()
->fetchColumn();
return $numberOfEntries > 0;
}
}

View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Updates;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
/**
* Fills tx_news_domain_model_tag.slug with a proper value
*/
class PopulateTagSlugs implements UpgradeWizardInterface
{
protected $table = 'tx_news_domain_model_tag';
protected $fieldName = 'slug';
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'txNewsTagSlugs';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Introduce URL parts ("slugs") to all tags of EXT:news';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Slugs for EXT:news tag records';
}
/**
* Checks whether updates are required.
*
* @return bool Whether an update is required (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$updateNeeded = false;
// Check if the database table even exists
if ($this->checkIfWizardIsRequired()) {
$updateNeeded = true;
}
return $updateNeeded;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the accordant updates.
*
* @return bool Whether everything went smoothly or not
*/
public function executeUpdate(): bool
{
$this->populateSlugs();
return true;
}
/**
* Fills the database table with slugs based on the page title and its configuration.
*
* @return void
*/
protected function populateSlugs(): void
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$statement = $queryBuilder
->select('*')
->from($this->table)
->where(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq($this->fieldName, $queryBuilder->createNamedParameter('')),
$queryBuilder->expr()->isNull($this->fieldName)
)
)
// Ensure that live workspace records are handled first
->addOrderBy('t3ver_wsid', 'asc')
->addOrderBy('pid', 'asc')
->addOrderBy('sorting', 'asc')
->execute();
// Check for existing slugs from realurl
$suggestedSlugs = [];
$fieldConfig = $GLOBALS['TCA'][$this->table]['columns'][$this->fieldName]['config'];
$evalInfo = !empty($fieldConfig['eval']) ? GeneralUtility::trimExplode(',', $fieldConfig['eval'], true) : [];
$hasToBeUniqueInSite = in_array('uniqueInSite', $evalInfo, true);
$hasToBeUniqueInPid = in_array('uniqueInPid', $evalInfo, true);
$slugHelper = GeneralUtility::makeInstance(SlugHelper::class, $this->table, $this->fieldName, $fieldConfig);
while ($record = $statement->fetch()) {
$recordId = (int)$record['uid'];
$pid = (int)$record['pid'];
$languageId = (int)$record['sys_language_uid'];
$recordInDefaultLanguage = $languageId > 0 ? (int)$record['l10n_parent'] : $recordId;
$slug = $suggestedSlugs[$recordInDefaultLanguage][$languageId] ?? '';
if (empty($slug)) {
if ($pid === -1) {
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$liveVersion = $queryBuilder
->select('pid')
->from($this->table)
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($record['t3ver_oid'], \PDO::PARAM_INT))
)->execute()->fetch();
$pid = (int)$liveVersion['pid'];
}
$slug = $slugHelper->generate($record, $pid);
}
$state = RecordStateFactory::forName($this->table)
->fromArray($record, $pid, $recordId);
if ($hasToBeUniqueInSite && !$slugHelper->isUniqueInSite($slug, $state)) {
$slug = $slugHelper->buildSlugForUniqueInSite($slug, $state);
}
if ($hasToBeUniqueInPid && !$slugHelper->isUniqueInPid($slug, $state)) {
$slug = $slugHelper->buildSlugForUniqueInPid($slug, $state);
}
$connection->update(
$this->table,
[$this->fieldName => $slug],
['uid' => $recordId]
);
}
}
/**
* Check if there are record within database table with an empty "slug" field.
*
* @return bool
* @throws \InvalidArgumentException
*/
protected function checkIfWizardIsRequired(): bool
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$numberOfEntries = $queryBuilder
->count('uid')
->from($this->table)
->where(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq($this->fieldName, $queryBuilder->createNamedParameter('')),
$queryBuilder->expr()->isNull($this->fieldName)
)
)
->execute()
->fetchColumn();
return $numberOfEntries > 0;
}
}

View File

@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use GeorgRinger\News\Service\SlugService;
use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
/**
* Migrate EXT:realurl unique alias into empty news slugs
*
* If a lot of similar titles are used it might be a good a idea
* to migrate the unique aliases from realurl to be sure that the same alias is used
*
* Requires existence of DB table tx_realurl_uniqalias, but EXT:realurl requires not to be installed
* Will only appear if missing slugs found between realurl and news, respecting language and expire date from realurl
* Copies values from 'tx_realurl_uniqalias.value_alias' to 'tx_news_domain_model_news.path_segment'
*/
class RealurlAliasNewsSlugUpdater implements UpgradeWizardInterface
{
const TABLE = 'tx_news_domain_model_news';
/** @var SlugService */
protected $slugService;
/**
* RealurlAliasNewsSlugUpdater constructor.
* @param SlugService $slugService
*/
public function __construct(
SlugService $slugService
) {
$this->slugService = $slugService;
}
public function executeUpdate(): bool
{
// user decided to migrate, migrate and mark wizard as done
$queries = $this->slugService->performRealurlAliasMigration();
return true;
}
public function updateNecessary(): bool
{
$updateNeeded = false;
$elementCount = $this->slugService->countOfRealurlAliasMigrations();
if ($elementCount > 0) {
$updateNeeded = true;
}
return $updateNeeded;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Get title
*
* @return string
*/
public function getTitle(): string
{
return '[Optional] Migrate realurl alias to slug field "path_segment" of EXT:news records';
}
/**
* Get description
*
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Migrates EXT:realurl unique alias values into empty slug field "path_segment" of EXT:news records.';
}
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'realurlAliasNewsSlug';
}
/**
* Second step: Ask user to install the extensions
*
* @param string $inputPrefix input prefix, all names of form fields have to start with this. Append custom name in [ ... ]
* @return string HTML output
*/
public function getUserInput($inputPrefix): string
{
return '
<div class="panel panel-danger">
<div class="panel-heading">Are you really sure?</div>
<div class="panel-body">
<p>
You can migrate EXT:realurl unique alias into news slugs,
to ensure that the same alias is used if similar news titles are used.
</p>
<p>
This wizard migrates only matching realurl alias for news entries, where path_segment is empty.
Requires database table "tx_realurl_uniqalias" from EXT:realurl, but EXT:realurl requires not to be installed.
</p>
<p>
Cause only empty news slugs will be generated within this migration,
you may decide to empty all news slugs before.
</p>
<p>
The result of this migration can still left empty slugs fields for news entries.
Therfore you should generate these slugs afterwards using the news slug upater wizard.
</p>
<div class="btn-group clearfix" data-toggle="buttons">
<label class="btn btn-default active">
<input type="radio" name="' . $inputPrefix . '[install]" value="0" checked="checked" /> No, don\'t migrate
</label>
<label class="btn btn-default">
<input type="radio" name="' . $inputPrefix . '[install]" value="1" /> Yes, please migrate
</label>
</div>
</div>
</div>
';
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace GeorgRinger\News\Utility;
use GeorgRinger\News\Domain\Model\Dto\NewsDemand;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Cache Utility class
*/
class Cache
{
/**
* Stack for processed cObjs which has added news relevant cache tags.
* @var array
*/
protected static $processedContentRecords = [];
/**
* Marks as cObj as processed.
*
* @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj
*
* @return void
*/
public function markContentRecordAsProcessed(ContentObjectRenderer $cObj): void
{
$key = 'tt_content_' . $cObj->data['uid'];
self::$processedContentRecords[$key] = true;
}
/**
* Checks if a cObj has already added cache tags.
*
* @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj
* @return bool
*/
public function isContentRecordAlreadyProcessed(ContentObjectRenderer $cObj): bool
{
$key = 'tt_content_' . $cObj->data['uid'];
return array_key_exists($key, self::$processedContentRecords);
}
/**
* Adds cache tags to page cache by news-records.
*
* Following cache tags will be added to tsfe:
* "tx_news_uid_[news:uid]"
*
* @param array|\TYPO3\CMS\Extbase\Persistence\QueryResult $newsRecords with news records
*
* @return void
*/
public static function addCacheTagsByNewsRecords($newsRecords): void
{
$cacheTags = [];
foreach ($newsRecords as $news) {
// cache tag for each news record
$cacheTags[] = 'tx_news_uid_' . $news->getUid();
if ($news->_getProperty('_localizedUid')) {
$cacheTags[] = 'tx_news_uid_' . $news->_getProperty('_localizedUid');
}
}
if (count($cacheTags) > 0) {
$GLOBALS['TSFE']->addCacheTags($cacheTags);
}
}
/**
* Adds page cache tags by used storagePages.
* This adds tags with the scheme tx_news_pid_[news:pid]
*
* @param \GeorgRinger\News\Domain\Model\Dto\NewsDemand $demand
*
* @return void
*/
public static function addPageCacheTagsByDemandObject(NewsDemand $demand): void
{
$cacheTags = [];
if ($demand->getStoragePage()) {
// Add cache tags for each storage page
foreach (GeneralUtility::trimExplode(',', $demand->getStoragePage()) as $pageId) {
$cacheTags[] = 'tx_news_pid_' . $pageId;
}
} else {
$cacheTags[] = 'tx_news_domain_model_news';
}
if (count($cacheTags) > 0) {
$GLOBALS['TSFE']->addCacheTags($cacheTags);
}
}
}

View File

@@ -0,0 +1,227 @@
<?php
namespace GeorgRinger\News\Utility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class ClassCacheManager
*/
class ClassCacheManager
{
/**
* Cache instance
*
* @var PhpFrontend
*/
protected $classCache;
/**
* @var array
*/
protected $constructorLines = [];
/**
* @param PhpFrontend $classCache
*/
public function __construct(PhpFrontend $classCache = null)
{
if ($classCache === null) {
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
if (!$cacheManager->hasCache('news')) {
$cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
}
$this->classCache = $cacheManager->getCache('news');
} else {
$this->classCache = $classCache;
}
}
public function reBuildSimple()
{
$classPath = 'Classes/';
if (!function_exists('token_get_all')) {
throw new \Exception(('The function token_get_all must exist. Please install the module PHP Module Tokenizer'));
}
if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['classes'])) {
return;
}
$files = GeneralUtility::getFilesInDir(Environment::getVarPath() . '/cache/code/news/', 'php');
if (empty($files)) {
$this->reBuild();
}
}
public function reBuild()
{
$classPath = 'Classes/';
if (!function_exists('token_get_all')) {
throw new \Exception(('The function token_get_all must exist. Please install the module PHP Module Tokenizer'));
}
if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['classes'])) {
return;
}
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['classes'] as $key => $extensionsWithThisClass) {
$this->constructorLines = [];
$extendingClassFound = false;
$path = ExtensionManagementUtility::extPath('news') . $classPath . $key . '.php';
if (!is_file($path)) {
throw new \Exception('Given file "' . $path . '" does not exist');
}
$code = $this->parseSingleFile($path, true);
// Get the files from all other extensions
foreach (array_unique($extensionsWithThisClass) as $extensionKey) {
$path = ExtensionManagementUtility::extPath($extensionKey) . $classPath . $key . '.php';
if (is_file($path)) {
$extendingClassFound = true;
$code .= $this->parseSingleFile($path, false);
}
}
if (isset($this->constructorLines['code']) && count($this->constructorLines['code'])) {
$code .= LF . implode("\n", $this->constructorLines['doc']);
$code .= LF . ' public function __construct(' . implode(',', $this->constructorLines['parameters'] ?? []) . ')' . LF . ' {' . LF . implode(LF, $this->constructorLines['code'] ?? []) . LF . ' }' . LF;
}
$code = $this->closeClassDefinition($code);
// If an extending class is found, the file is written and
// added to the autoloader info
if ($extendingClassFound) {
$cacheEntryIdentifier = 'tx_news_' . strtolower(str_replace('/', '_', $key));
try {
$this->classCache->set($cacheEntryIdentifier, $code);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
}
}
}
/**
* Parse a single file and does some magic
* - Remove the <?php tags
* - Remove the class definition (if set)
*
* @param string $filePath path of the file
* @param bool $baseClass If class definition should be removed
* @return string path of the saved file
* @throws \Exception
* @throws \InvalidArgumentException
*/
protected function parseSingleFile($filePath, $baseClass = false): string
{
if (!is_file($filePath)) {
throw new \InvalidArgumentException(sprintf('File "%s" could not be found', $filePath));
}
$code = GeneralUtility::getUrl($filePath);
$classParser = GeneralUtility::makeInstance(ClassParser::class);
$classParser->parse($filePath);
$classParserInformation = $classParser->getFirstClass();
$code = str_replace('<?php', '', $code);
$codeInLines = explode(LF, str_replace(CR, '', $code));
$offsetForInnerPart = 0;
if ($baseClass) {
$innerPart = $codeInLines;
} else {
$offsetForInnerPart = $classParserInformation['start'];
if (isset($classParserInformation['eol'])) {
$innerPart = array_slice(
$codeInLines,
$classParserInformation['start'],
($classParserInformation['eol'] - $classParserInformation['start'] - 1)
);
} else {
$innerPart = array_slice($codeInLines, $classParserInformation['start']);
}
}
if (trim($innerPart[0]) === '{') {
unset($innerPart[0]);
}
$innerPartLine = function ($line) use ($offsetForInnerPart) {
return $line - $offsetForInnerPart;
};
// unset the constructor and save it's lines
if (isset($classParserInformation['functions']['__construct'])) {
$constructorInfo = $classParserInformation['functions']['__construct'];
$constructorInfo['inner_start'] = $constructorInfo['start'] - $offsetForInnerPart;
$constructorInfo['inner_end'] = $constructorInfo['end'] - $offsetForInnerPart;
if ($baseClass) {
$this->constructorLines['doc'] = explode("\n", $constructorInfo['doc']);
} else {
array_splice(
$this->constructorLines['doc'],
-1,
0,
array_filter(explode("\n", $constructorInfo['doc'] ?? ''), function ($value) {
return strpos($value, '@param') !== false;
})
);
}
$codePart = false;
for ($i = $constructorInfo['inner_start']; $i < $constructorInfo['inner_end']; $i++) {
if ($codePart) {
$this->constructorLines['code'][] = $innerPart[$i];
} elseif (trim($innerPart[$i]) === ') {' || trim($innerPart[$i]) === '{') {
$codePart = true;
} elseif (trim($innerPart[$i]) !== ')' && $i >= ($constructorInfo['inner_start'])) {
$this->constructorLines['parameters'][] = LF . rtrim($innerPart[$i], ',');
}
unset($innerPart[$i]);
}
unset($innerPart[$constructorInfo['inner_start'] - 1]);
unset($innerPart[$constructorInfo['inner_end']]);
}
$codePart = implode(LF, $innerPart);
$closingBracket = strrpos($codePart, '}');
$codePart = substr($codePart, 0, $closingBracket);
return $this->getPartialInfo($filePath) . $codePart;
}
/**
* @param string $filePath
* @return string
*/
protected function getPartialInfo($filePath): string
{
return LF . '/*' . str_repeat('*', 70) . LF . "\t" .
'this is partial from: ' . LF . "\t" . str_replace(Environment::getPublicPath(), '', $filePath) . LF . str_repeat(
'*',
70
) . '*/' . LF;
}
/**
* @param string $code
* @return string
*/
protected function closeClassDefinition($code): string
{
return $code . LF . '}';
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace GeorgRinger\News\Utility;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Class ClassLoader
*/
class ClassLoader implements SingletonInterface
{
/**
* @var PhpFrontend
*/
protected $classCache;
/** @var ClassCacheManager */
protected $classCacheManager;
/** @var bool */
protected $isValidInstance = false;
/**
* ClassLoader constructor.
*
* @param PhpFrontend $classCache
*/
public function __construct(PhpFrontend $classCache = null, ClassCacheManager $classCacheManager = null)
{
$versionInformation = GeneralUtility::makeInstance(Typo3Version::class);
if ($versionInformation->getMajorVersion() === 10) {
// Use DI
// something might fail, e.g loading checks in Install Tool
if ($classCacheManager !== null) {
$this->classCacheManager = $classCacheManager;
$this->isValidInstance = true;
}
} else {
$this->classCacheManager = GeneralUtility::makeInstance(ClassCacheManager::class);
$this->isValidInstance = true;
}
if ($this->isValidInstance) {
if ($classCache === null) {
$this->classCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('news');
} else {
$this->classCache = $classCache;
}
}
}
/**
* Register instance of this class as spl autoloader
*
* @return void
*/
public static function registerAutoloader(): void
{
spl_autoload_register([GeneralUtility::makeInstance(self::class), 'loadClass'], true, true);
}
/**
* Loads php files containing classes or interfaces part of the
* classes directory of an extension.
*
* @param string $className Name of the class/interface to load
* @return bool
*/
public function loadClass($className): bool
{
if (!$this->isValidInstance) {
return false;
}
$className = ltrim($className, '\\');
if (!$this->isValidClassName($className)) {
return false;
}
$cacheEntryIdentifier = 'tx_news_' . strtolower(str_replace('/', '_', $this->changeClassName($className)));
if (!$this->classCache->has($cacheEntryIdentifier)) {
$this->classCacheManager->reBuild();
}
$this->classCache->requireOnce($cacheEntryIdentifier);
return true;
}
/**
* Get extension key from namespaced classname
*
* @param string $className
*
* @return null|string
*/
protected function getExtensionKey($className): ?string
{
$extensionKey = null;
if (strpos($className, '\\') !== false) {
$namespaceParts = GeneralUtility::trimExplode(
'\\',
$className,
0,
(substr($className, 0, 9) === 'TYPO3\\CMS' ? 4 : 3)
);
array_pop($namespaceParts);
$extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored(array_pop($namespaceParts));
}
return $extensionKey;
}
/**
* Find out if a class name is valid
*
* @param string $className
* @return bool
*/
protected function isValidClassName($className): bool
{
if ($this->isFirstPartOfStr($className, 'GeorgRinger\\News\\Domain\\') || $this->isFirstPartOfStr($className, 'GeorgRinger\\News\\Controller\\')) {
$modifiedClassName = $this->changeClassName($className);
if (isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['classes'][$modifiedClassName])) {
return true;
}
}
return false;
}
protected function isFirstPartOfStr(string $str, string $partStr): bool
{
return $partStr !== '' && strpos($str, $partStr) === 0;
}
protected function changeClassName(string $className): string
{
return str_replace('\\', '/', str_replace('GeorgRinger\\News\\', '', $className));
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace GeorgRinger\News\Utility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
class ClassParser
{
private $classes = [];
private $extends = [];
private $implements = [];
const STATE_CLASS_HEAD = 100001;
const STATE_FUNCTION_HEAD = 100002;
public function getClasses()
{
return $this->classes;
}
public function getFirstClass()
{
return array_shift($this->classes);
}
public function getClassesImplementing($interface): array
{
$implementers = [];
if (isset($this->implements[$interface])) {
foreach ($this->implements[$interface] as $name) {
$implementers[$name] = $this->classes[$name];
}
}
return $implementers;
}
public function getClassesExtending($class): array
{
$extenders = [];
if (isset($this->extends[$class])) {
foreach ($this->extends[$class] as $name) {
$extenders[$name] = $this->classes[$name];
}
}
return $extenders;
}
public function parse($file): void
{
$file = realpath($file);
$tokens = token_get_all(file_get_contents($file));
$classes = [];
$clsc = 0;
$si = null;
$depth = 0;
$mod = [];
$doc = null;
$state = null;
$inFunction = false;
$functionName = '';
$lastLine = 0;
foreach ($tokens as $idx => &$token) {
if (is_array($token)) {
switch ($token[0]) {
case T_DOC_COMMENT:
$doc = $token[1];
break;
case T_PUBLIC:
case T_PRIVATE:
case T_ABSTRACT:
case T_PROTECTED:
$mod[] = $token[1];
break;
case T_CLASS:
case T_FUNCTION:
$state = $token[0];
break;
case T_EXTENDS:
case T_IMPLEMENTS:
switch ($state) {
case self::STATE_CLASS_HEAD:
case T_EXTENDS:
$state = $token[0];
break;
}
break;
case T_CLOSE_TAG:
$classes[$depth]['eol'] = $token[2];
break;
case T_STRING:
switch ($state) {
case T_CLASS:
$state = self::STATE_CLASS_HEAD;
$si = $token[1];
$classes[] = [
'name' => $token[1],
'modifiers' => $mod,
'doc' => $doc,
'start' => $token[2]
];
break;
case T_FUNCTION:
$state = self::STATE_FUNCTION_HEAD;
$clsc = count($classes);
if ($depth > 0 && $clsc) {
$inFunction = true;
$functionName = $token[1];
$classes[$clsc - 1]['functions'][$token[1]] = [
'modifiers' => $mod,
'doc' => $doc,
'start' => $token[2]
];
}
break;
case T_IMPLEMENTS:
case T_EXTENDS:
$clsc = count($classes);
$classes[$clsc - 1][$state == T_IMPLEMENTS ? 'implements' : 'extends'][] = $token[1];
break;
}
break;
}
$lastLine = $token[2];
} else {
switch ($token) {
case '{':
$depth++;
break;
case '}':
$depth--;
break;
}
if ($token === '}') {
if ($inFunction) {
$classes[$clsc - 1]['functions'][$functionName]['end'] = $lastLine;
$inFunction = false;
}
}
switch ($token) {
case '{':
case '}':
case ';':
$state = 0;
$doc = null;
$mod = [];
break;
}
}
}
foreach ($classes as $class) {
$class['file'] = $file;
$this->classes[$class['name']] = $class;
if (!empty($class['implements'])) {
foreach ($class['implements'] as $name) {
$this->implements[$name][] = $class['name'];
}
}
if (!empty($class['extends'])) {
foreach ($class['extends'] as $name) {
$this->extends[$name][] = $class['name'];
}
}
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace GeorgRinger\News\Utility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use DateTime;
use Exception;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Helper class for building constraints
*/
class ConstraintHelper
{
/**
* @param string|int $timeInput
* @return int
* @throws Exception
*/
public static function getTimeRestrictionLow($timeInput): int
{
$timeLimit = 0;
// integer = timestamp
if (MathUtility::canBeInterpretedAsInteger($timeInput)) {
$timeLimit = $GLOBALS['SIM_EXEC_TIME'] - $timeInput;
} else {
$timeByFormat = DateTime::createFromFormat('HH:mm DD-MM-YYYY', $timeInput);
if ($timeByFormat) {
$timeLimit = $timeByFormat->getTimestamp();
} else {
// try to check strtotime
$timeFromString = strtotime($timeInput);
if ($timeFromString) {
$timeLimit = $timeFromString;
} else {
throw new Exception('Time limit Low could not be resolved to an integer. Given was: ' . htmlspecialchars($timeLimit));
}
}
}
return $timeLimit;
}
/**
* @param string|int $timeInput
* @return int
* @throws Exception
*/
public static function getTimeRestrictionHigh($timeInput): int
{
$timeLimit = 0;
// integer = timestamp
if (MathUtility::canBeInterpretedAsInteger($timeInput)) {
$timeLimit = $GLOBALS['SIM_EXEC_TIME'] + $timeInput;
return $timeLimit;
}
// try to check strtotime
$timeFromStringHigh = strtotime($timeInput);
if ($timeFromStringHigh) {
$timeLimit = $timeFromStringHigh;
return $timeLimit;
}
throw new Exception('Time limit High could not be resolved to an integer. Given was: ' . htmlspecialchars($timeLimit));
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace GeorgRinger\News\Utility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Utility class for import jobs
*/
class ImportJob
{
/**
* @var array
*/
protected static $registeredJobs = [];
/**
* Register an import job.
*
* @param string $className class name
* @param string $title title
* @param string $description description
*
* @static
*
* @return void
*/
public static function register($className, $title, $description): void
{
self::$registeredJobs[] = [
'className' => $className,
'title' => $title,
'description' => $description
];
}
/**
* Get all registered import jobs
*
* @static
* @return array
*/
public static function getRegisteredJobs(): array
{
return self::$registeredJobs;
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace GeorgRinger\News\Utility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Backend\Tree\View\PageTreeView;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\QueryGenerator;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Page Utility class
*/
class Page
{
/**
* Find all ids from given ids and level
*
* @param string $pidList comma separated list of ids
* @param int $recursive recursive levels
* @return string comma separated list of ids
*/
public static function extendPidListByChildren($pidList = '', $recursive = 0): string
{
$recursive = (int)$recursive;
if ($recursive <= 0) {
return $pidList ?? '';
}
$queryGenerator = GeneralUtility::makeInstance(QueryGenerator::class);
$recursiveStoragePids = $pidList;
$storagePids = GeneralUtility::intExplode(',', $pidList);
foreach ($storagePids as $startPid) {
if ($startPid >= 0) {
$pids = $queryGenerator->getTreeList($startPid, $recursive);
if (strlen($pids) > 0) {
$recursiveStoragePids .= ',' . $pids;
}
}
}
return GeneralUtility::uniqueList($recursiveStoragePids);
}
/**
* Set properties of an object/array in cobj->LOAD_REGISTER which can then
* be used to be loaded via TS with register:name
*
* @param string $properties comma separated list of properties
* @param mixed $object object or array to get the properties
* @param string $prefix optional prefix
*
* @return void
*/
public static function setRegisterProperties($properties, $object, $prefix = 'news'): void
{
if (!empty($properties) && $object !== null) {
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$items = GeneralUtility::trimExplode(',', $properties, true);
$register = [];
foreach ($items as $item) {
$key = $prefix . ucfirst($item);
if (is_object($object)) {
$getter = 'get' . ucfirst($item);
try {
$value = $object->$getter();
if ($value instanceof \DateTime) {
$value = $value->getTimestamp();
}
$register[$key] = $value;
} catch (\Exception $e) {
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
$logger->warning($e->getMessage());
}
}
if (is_array($object)) {
$value = $object[$item];
$register[$key] = $value;
}
}
$cObj->cObjGetSingle('LOAD_REGISTER', $register);
}
}
/**
* Return a page tree
*
* @param int $pageUid page to start with
* @param int $treeLevel count of levels
* @return PageTreeView
* @throws \Exception
*/
public static function pageTree($pageUid, $treeLevel): PageTreeView
{
if (TYPO3_MODE !== 'BE') {
throw new \Exception('Page::pageTree does only work in the backend!');
}
$pageUid = (int)$pageUid;
if ($pageUid === 0 && !self::getBackendUser()->isAdmin()) {
$mounts = self::getBackendUser()->returnWebmounts();
$pageUid = array_shift($mounts);
}
/* @var $tree PageTreeView */
$tree = GeneralUtility::makeInstance(PageTreeView::class);
$tree->init('AND ' . self::getBackendUser()->getPagePermsClause(1));
$treeStartingRecord = BackendUtility::getRecord('pages', $pageUid);
BackendUtility::workspaceOL('pages', $treeStartingRecord);
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
// Creating top icon; the current page
$tree->tree[] = [
'row' => $treeStartingRecord,
'HTML' => is_array($treeStartingRecord) ? $iconFactory->getIconForRecord('pages', $treeStartingRecord, Icon::SIZE_SMALL)->render() : ''
];
$tree->getTree($pageUid, $treeLevel, '');
return $tree;
}
/**
* Get backend user
*
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
*/
protected static function getBackendUser(): \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace GeorgRinger\News\Utility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* TemplateLayout utility class
*/
class TemplateLayout implements SingletonInterface
{
/**
* Get available template layouts for a certain page
*
* @param int $pageUid
* @return array
*/
public function getAvailableTemplateLayouts($pageUid): array
{
$templateLayouts = [];
// Check if the layouts are extended by ext_tables
if (isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['templateLayouts'])
&& is_array($GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['templateLayouts'])
) {
$templateLayouts = $GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['templateLayouts'];
}
// Add TsConfig values
foreach ($this->getTemplateLayoutsFromTsConfig($pageUid) as $templateKey => $title) {
if (str_starts_with($title, '--div--')) {
$optGroupParts = GeneralUtility::trimExplode(',', $title, true, 2);
$title = $optGroupParts[1];
$templateKey = $optGroupParts[0];
}
$templateLayouts[] = [$title, $templateKey];
}
return $templateLayouts;
}
/**
* Get template layouts defined in TsConfig
*
* @param $pageUid
* @return array
*/
protected function getTemplateLayoutsFromTsConfig(int $pageUid): array
{
$templateLayouts = [];
$pagesTsConfig = BackendUtility::getPagesTSconfig($pageUid);
if (isset($pagesTsConfig['tx_news.']['templateLayouts.']) && is_array($pagesTsConfig['tx_news.']['templateLayouts.'])) {
$templateLayouts = $pagesTsConfig['tx_news.']['templateLayouts.'];
}
return $templateLayouts;
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace GeorgRinger\News\Utility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* TypoScript Utility class
*/
class TypoScript
{
/**
* @param array $base
* @param array $overload
* @return array
*/
public function override(array $base, array $overload): array
{
$configuration = $overload['settings']['overrideFlexformSettingsIfEmpty'] ?? '';
$validFields = GeneralUtility::trimExplode(',', $configuration, true);
foreach ($validFields as $fieldName) {
// Multilevel field
if (strpos($fieldName, '.') !== false) {
$keyAsArray = explode('.', $fieldName);
$foundInCurrentTs = $this->getValue($base, $keyAsArray);
if (is_string($foundInCurrentTs) && strlen($foundInCurrentTs) === 0) {
$foundInOriginal = $this->getValue($overload['settings'], $keyAsArray);
if ($foundInOriginal) {
$base = $this->setValue($base, $keyAsArray, $foundInOriginal);
}
}
} else {
// if flexform setting is empty and value is available in TS
if (((!isset($base[$fieldName]) || $base[$fieldName] === '0') || (strlen($base[$fieldName]) === 0))
&& isset($overload['settings'][$fieldName])
) {
$base[$fieldName] = $overload['settings'][$fieldName];
}
}
}
return $base;
}
/**
* Get value from array by path
*
* @param mixed $data
* @param mixed $path
* @return mixed
*/
protected function getValue($data, $path)
{
$found = true;
for ($x = 0; ($x < count($path) && $found); $x++) {
$key = $path[$x];
if (isset($data[$key])) {
$data = $data[$key];
} else {
$found = false;
}
}
if ($found) {
return $data;
}
return null;
}
/**
* Set value in array by path
*
* @param array $array
* @param array $path
* @param mixed $value
* @param string[] $path
*
* @return array
*/
protected function setValue(array $array, array $path, $value): array
{
$this->setValueByReference($array, $path, $value);
$final = array_merge_recursive([], $array);
return $final;
}
/**
* Set value by reference
*
* @param array $array
* @param array $path
* @param $value
*
* @return void
*/
private function setValueByReference(array &$array, array $path, $value): void
{
while (count($path) > 1) {
$key = array_shift($path);
if (!isset($array[$key])) {
$array[$key] = [];
}
$array = &$array[$key];
}
$key = reset($path);
$array[$key] = $value;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\Utility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Url Utility class
*/
class Url
{
/**
* Prepend current url if url is relative
*
* @param string $url given url
* @return string
*/
public static function prependDomain(string $url): string
{
if (!str_starts_with($url, GeneralUtility::getIndpEnv('TYPO3_SITE_URL'))) {
$url = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . $url;
}
return $url;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace GeorgRinger\News\Utility;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Validation
*/
class Validation
{
/**
* Validate ordering as extbase can't handle that currently
*
* @param string $fieldToCheck
* @param string $allowedSettings
* @return bool
*/
public static function isValidOrdering($fieldToCheck, $allowedSettings): bool
{
$isValid = true;
if (empty($fieldToCheck)) {
return $isValid;
}
if (empty($allowedSettings)) {
return false;
}
$fields = GeneralUtility::trimExplode(',', $fieldToCheck, true);
foreach ($fields as $field) {
if ($isValid === true) {
$split = GeneralUtility::trimExplode(' ', $field, true);
$count = count($split);
switch ($count) {
case 1:
if (!GeneralUtility::inList($allowedSettings, $split[0])) {
$isValid = false;
}
break;
case 2:
if ((strtolower($split[1]) !== 'desc' && strtolower($split[1]) !== 'asc') ||
!GeneralUtility::inList($allowedSettings, $split[0])
) {
$isValid = false;
}
break;
default:
$isValid = false;
}
}
}
return $isValid;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace GeorgRinger\News\ViewHelpers\Be;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* Check if the checkbox should be active or not
*/
class IsCheckboxActiveViewHelper extends AbstractViewHelper
{
/**
* @var bool
*/
protected $escapeOutput = false;
/**
* Initialize arguments.
*/
public function initializeArguments()
{
parent::initializeArguments();
$this->registerArgument('id', 'int', 'Category id', true);
$this->registerArgument('categories', 'array', 'List of categories', false, []);
}
/**
* @return string
*/
public function render(): string
{
return (isset($this->arguments['categories']) && is_array($this->arguments['categories']) && in_array($this->arguments['id'], $this->arguments['categories'])) ? 'checked="checked"' : '';
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace GeorgRinger\News\ViewHelpers\Category;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
/**
* Get usage count. This ViewHelper retrieves a simple count and does *not* take any additional constraints into account!
* If you need additional constraints like startingpoint, archive, ... duplicate the ViewHelper and implement it on your own!
*
* Example usage
* {n:category.count(categoryUid:category.item.uid) -> f:variable(name: 'categoryUsageCount')}
* {categoryUsageCount}
*/
class CountViewHelper extends AbstractViewHelper implements ViewHelperInterface
{
use CompileWithRenderStatic;
/**
* Initialize arguments
*/
public function initializeArguments()
{
parent::initializeArguments();
$this->registerArgument('categoryUid', 'int', 'Uid of the category', true);
}
/**
* @param array $arguments
* @param \Closure $renderChildrenClosure
* @param RenderingContextInterface $renderingContext
* @return int
*/
public static function renderStatic(
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
): int {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_news_domain_model_news');
$categoryUid = $arguments['categoryUid'];
$languageUid = GeneralUtility::makeInstance(Context::class)->getAspect('language')->getId();
$count = $queryBuilder
->count('tx_news_domain_model_news.title')
->from('tx_news_domain_model_news')
->rightJoin(
'tx_news_domain_model_news',
'sys_category_record_mm',
'sys_category_record_mm',
$queryBuilder->expr()->eq('tx_news_domain_model_news.uid', $queryBuilder->quoteIdentifier('sys_category_record_mm.uid_foreign'))
)
->rightJoin(
'sys_category_record_mm',
'sys_category',
'sys_category',
$queryBuilder->expr()->eq('sys_category.uid', $queryBuilder->quoteIdentifier('sys_category_record_mm.uid_local'))
)
->where(
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq(
'sys_category.uid',
$queryBuilder->createNamedParameter($categoryUid, \PDO::PARAM_INT)
),
$queryBuilder->expr()->eq(
'sys_category_record_mm.tablenames',
$queryBuilder->createNamedParameter('tx_news_domain_model_news', \PDO::PARAM_STR)
),
$queryBuilder->expr()->eq(
'sys_category_record_mm.fieldname',
$queryBuilder->createNamedParameter('categories', \PDO::PARAM_STR)
),
$queryBuilder->expr()->in(
'tx_news_domain_model_news.sys_language_uid',
$queryBuilder->createNamedParameter([-1, $languageUid], Connection::PARAM_INT_ARRAY)
)
)
)
->execute()
->fetchColumn(0);
return $count;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace GeorgRinger\News\ViewHelpers\Check;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use GeorgRinger\News\Seo\NewsAvailability;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper;
/**
* Check if current news page is available
*/
class PageAvailableInLanguageViewHelper extends AbstractConditionViewHelper
{
/**
* Initialize additional argument
*/
public function initializeArguments()
{
$this->registerArgument('language', 'int', 'Language ot check', true);
parent::initializeArguments();
}
/**
* @param array|null $arguments
* @return bool
*/
protected static function evaluateCondition($arguments = null): bool
{
try {
$newsAvailabilityChecker = GeneralUtility::makeInstance(NewsAvailability::class);
return $newsAvailabilityChecker->check((int)$arguments['language']);
} catch (\UnexpectedValueException $e) {
return true;
}
}
/**
* @return mixed
*/
public function render()
{
if (static::evaluateCondition($this->arguments)) {
return $this->renderThenChild();
}
return $this->renderElseChild();
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace GeorgRinger\News\ViewHelpers;
use GeorgRinger\News\Domain\Model\News;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface;
/**
* ViewHelper to exclude news items in other plugins
*
* # Example: Basic example
*
* <code>
* <n:excludeDisplayedNews newsItem="{newsItem}" />
* </code>
* <output>
* None
* </output>
*/
class ExcludeDisplayedNewsViewHelper extends AbstractViewHelper implements ViewHelperInterface
{
use CompileWithRenderStatic;
/**
* Initialize arguments
*/
public function initializeArguments()
{
parent::initializeArguments();
$this->registerArgument('newsItem', News::class, 'news item', true);
}
/**
* @param array $arguments
* @param \Closure $renderChildrenClosure
* @param RenderingContextInterface $renderingContext
*
* @return void
*/
public static function renderStatic(
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
) {
$newsItem = $arguments['newsItem'];
$uid = $newsItem->getUid();
if (empty($GLOBALS['EXT']['news']['alreadyDisplayed'])) {
$GLOBALS['EXT']['news']['alreadyDisplayed'] = [];
}
$GLOBALS['EXT']['news']['alreadyDisplayed'][$uid] = $uid;
// Add localized uid as well
$originalUid = (int)$newsItem->_getProperty('_localizedUid');
if ($originalUid > 0) {
$GLOBALS['EXT']['news']['alreadyDisplayed'][$originalUid] = $originalUid;
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace GeorgRinger\News\ViewHelpers;
/**
* This file is part of the "news" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper;
/**
* Class ExtensionLoadedViewHelper
*/
class ExtensionLoadedViewHelper extends AbstractConditionViewHelper
{
/**
* Initialize additional argument
*/
public function initializeArguments()
{
$this->registerArgument('extensionKey', 'string', 'Extension which must be checked', true);
parent::initializeArguments();
}
/**
* @param array|null $arguments
* @return bool
*/
protected static function evaluateCondition($arguments = null): bool
{
return ExtensionManagementUtility::isLoaded($arguments['extensionKey']);
}
/**
* @return mixed
*/
public function render()
{
if (static::evaluateCondition($this->arguments)) {
return $this->renderThenChild();
}
return $this->renderElseChild();
}
}

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