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,76 @@
<?php
/*
* 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!
*/
/**
* This file represents the configuration for Code Sniffing PSR-2-related
* automatic checks of coding guidelines
* Install @fabpot's great php-cs-fixer tool via
*
* $ composer global require friendsofphp/php-cs-fixer
*
* And then simply run
*
* $ php-cs-fixer fix --config ../Build/.php_cs
*
* inside the TYPO3 directory. Warning: This may take up to 10 minutes.
*
* For more information read:
* http://www.php-fig.org/psr/psr-2/
* http://cs.sensiolabs.org
*/
if (PHP_SAPI !== 'cli') {
die('This script supports command line usage only. Please check your command.');
}
// Define in which folders to search and which folders to exclude
// Exclude some directories that are excluded by Git anyways to speed up the sniffing
$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/../');
// Return a Code Sniffing configuration using
// all sniffers needed for PSR-2
// and additionally:
// - Remove leading slashes in use clauses.
// - PHP single-line arrays should not have trailing comma.
// - Single-line whitespace before closing semicolon are prohibited.
// - Remove unused use statements in the PHP source code
// - Ensure Concatenation to have at least one whitespace around
// - Remove trailing whitespace at the end of blank lines.
return PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules([
'@PSR2' => true,
'no_leading_import_slash' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_unused_imports' => true,
'concat_space' => ['spacing' => 'one'],
'no_whitespace_in_blank_line' => true,
'ordered_imports' => true,
'single_quote' => true,
'no_empty_statement' => true,
'no_extra_consecutive_blank_lines' => true,
'phpdoc_no_package' => true,
'phpdoc_scalar' => true,
'no_blank_lines_after_phpdoc' => true,
'array_syntax' => ['syntax' => 'short'],
'whitespace_after_comma_in_array' => true,
'function_typehint_space' => true,
'hash_to_slash_comment' => true,
'no_alias_functions' => true,
'lowercase_cast' => true,
'no_leading_namespace_whitespace' => true,
'native_function_casing' => true,
'self_accessor' => true,
'no_short_bool_cast' => true,
'no_unneeded_control_parentheses' => true
])
->setFinder($finder);

View File

@@ -0,0 +1,34 @@
<!--
Functional test suites setup
Functional tests should extend from \TYPO3\TestingFramework\Core\Tests\FunctionalTestCase,
take a look at this class for further documentation on how to run the suite.
TYPO3 CMS functional test suite also needs phpunit bootstrap code, the
file is located next to this .xml as FunctionalTestsBootstrap.php
-->
<phpunit
backupGlobals="true"
bootstrap="../.Build/vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
verbose="false"
beStrictAboutTestsThatDoNotTestAnything="false"
>
<testsuites>
<testsuite name="Core tests">
<directory>../Tests/Functional/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>../Classes/</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,34 @@
<!--
Functional test suites setup
Functional tests should extend from \TYPO3\TestingFramework\Core\Tests\FunctionalTestCase,
take a look at this class for further documentation on how to run the suite.
TYPO3 CMS functional test suite also needs phpunit bootstrap code, the
file is located next to this .xml as FunctionalTestsBootstrap.php
-->
<phpunit
backupGlobals="true"
bootstrap="../../../../../vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
verbose="false"
beStrictAboutTestsThatDoNotTestAnything="false"
>
<testsuites>
<testsuite name="Core tests">
<directory>../../Tests/Functional/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>../../Classes/</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,27 @@
<phpunit
backupGlobals="true"
bootstrap="../../../../../../vendor/typo3/testing-framework/Resources/Core/Build/UnitTestsBootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
verbose="false"
convertDeprecationsToExceptions="false"
beStrictAboutTestsThatDoNotTestAnything="false"
>
<testsuites>
<testsuite name="Core tests">
<directory>../../Tests/Unit/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>../../Classes/</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,28 @@
<phpunit
backupGlobals="true"
bootstrap="../.Build/vendor/typo3/testing-framework/Resources/Core/Build/UnitTestsBootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
verbose="false"
convertDeprecationsToExceptions="false"
beStrictAboutTestsThatDoNotTestAnything="false"
>
<testsuites>
<testsuite name="tests">
<directory>../Tests/Unit/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>../Classes/</directory>
</whitelist>
</filter>
</phpunit>

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;
}
}

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