diff --git a/.ddev/commands/host/initialize b/.ddev/commands/host/initialize
index 5b4c965b..6b8ca8d3 100755
--- a/.ddev/commands/host/initialize
+++ b/.ddev/commands/host/initialize
@@ -18,9 +18,9 @@ mkdir -p config/sites/main
cp .project/config/typo3/config.yaml config/sites/main/
mkdir -p .build/public
cp .project/config/typo3/.htaccess .build/public/
-mkdir -p .build/public/typo3conf
-cp .project/config/typo3/LocalConfiguration.php .build/public/typo3conf
-cp .project/config/typo3/AdditionalConfiguration.php .build/public/typo3conf
+mkdir -p config/system
+cp .project/config/typo3/settings.php config/system/
+cp .project/config/typo3/additional.php config/system/
echo "Importing database"
ddev import-db --src=.project/data/db.sql.gz
diff --git a/.ddev/config.yaml b/.ddev/config.yaml
index 30d6b874..96b633e7 100644
--- a/.ddev/config.yaml
+++ b/.ddev/config.yaml
@@ -1,7 +1,7 @@
name: in2studyfinder
type: php
docroot: .build/public
-php_version: "8.0"
+php_version: "8.1"
webserver_type: apache-fpm
router_http_port: "80"
router_https_port: "443"
diff --git a/.gitignore b/.gitignore
index 2000f375..18e6101f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,4 @@ var/
!.ddev/config.yaml
!.ddev/docker-compose.typo3.yaml
+!.project/config
diff --git a/.phpcs.xml b/.phpcs.xml
index f7c6456c..531ed0ec 100644
--- a/.phpcs.xml
+++ b/.phpcs.xml
@@ -10,7 +10,7 @@
Classes
-
+
diff --git a/.phpmd.xml b/.phpmd.xml
index f7135217..a5ae14ba 100644
--- a/.phpmd.xml
+++ b/.phpmd.xml
@@ -19,7 +19,7 @@
-
+
diff --git a/.project/config/typo3/.htaccess b/.project/config/typo3/.htaccess
index 8d8bd60f..d39e2af8 100644
--- a/.project/config/typo3/.htaccess
+++ b/.project/config/typo3/.htaccess
@@ -33,13 +33,13 @@
# *) Set $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] = 9 together with the TypoScript properties
# config.compressJs and config.compressCss for GZIP compression of Frontend JS and CSS files.
-#
-# AddType "text/javascript" .gzip
+#
+# AddType "text/javascript" .gz
#
-#
-# AddType "text/css" .gzip
+#
+# AddType "text/css" .gz
#
-#AddEncoding gzip .gzip
+#AddEncoding x-gzip .gz
# Force compression for mangled `Accept-Encoding` request headers
@@ -111,7 +111,7 @@
# This affects Frontend and Backend and increases performance.
- ExpiresActive on
+ ExpiresActive On
ExpiresDefault "access plus 1 month"
ExpiresByType text/css "access plus 1 year"
@@ -259,7 +259,7 @@ AddDefaultCharset utf-8
# Send the CORS header for images when browsers request it.
-
+
SetEnvIf Origin ":" IS_CORS
Header set Access-Control-Allow-Origin "*" env=IS_CORS
@@ -279,8 +279,6 @@ AddDefaultCharset utf-8
### Begin: Rewriting and Access ###
-# You need rewriting, if you use a URL-Rewriting extension (RealURL, CoolUri).
-
# Enable URL rewriting
@@ -305,7 +303,7 @@ AddDefaultCharset utf-8
# IMPORTANT: This rule has to be the very first RewriteCond in order to work!
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
- RewriteRule ^(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gzip)$ %{ENV:CWD}$1.$3 [L]
+ RewriteRule ^(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gz)$ %{ENV:CWD}$1.$3 [L]
# Access block for folders
RewriteRule _(?:recycler|temp)_/ - [F]
@@ -377,7 +375,7 @@ Options -MultiViews
# Force IE to render pages in the highest available mode
Header set X-UA-Compatible "IE=edge"
-
+
Header unset X-UA-Compatible
diff --git a/.project/config/typo3/AdditionalConfiguration.php b/.project/config/typo3/additional.php
similarity index 100%
rename from .project/config/typo3/AdditionalConfiguration.php
rename to .project/config/typo3/additional.php
diff --git a/.project/config/typo3/LocalConfiguration.php b/.project/config/typo3/settings.php
similarity index 98%
rename from .project/config/typo3/LocalConfiguration.php
rename to .project/config/typo3/settings.php
index eb58abd9..5f97a7ec 100644
--- a/.project/config/typo3/LocalConfiguration.php
+++ b/.project/config/typo3/settings.php
@@ -1,7 +1,7 @@
[
- 'debug' => false,
+ 'debug' => true,
'explicitADmode' => 'explicitAllow',
'installToolPassword' => '$argon2i$v=19$m=65536,t=16,p=1$Y0Zkc2tFVUFzaWs3S1JUMA$VUYXEG49usrpbDP9cKfC0GrhRAtGICi5B7HGJ4LVYtg',
'passwordHashing' => [
@@ -53,7 +53,7 @@
],
],
'FE' => [
- 'debug' => false,
+ 'debug' => true,
'disableNoCacheParameter' => true,
'passwordHashing' => [
'className' => 'TYPO3\\CMS\\Core\\Crypto\\PasswordHashing\\Argon2iPasswordHash',
diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php
index 08ad7344..af124fc0 100644
--- a/Classes/Controller/BackendController.php
+++ b/Classes/Controller/BackendController.php
@@ -7,33 +7,41 @@
use In2code\In2studyfinder\Domain\Repository\StudyCourseRepository;
use In2code\In2studyfinder\Domain\Service\CourseService;
use In2code\In2studyfinder\Service\ExportService;
+use Psr\Http\Message\ResponseInterface;
+use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Core\Database\ConnectionPool;
-use TYPO3\CMS\Core\Messaging\AbstractMessage;
+use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* @SuppressWarnings(PHPMD.LongVariable)
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class BackendController extends AbstractController
{
protected CourseService $courseService;
-
- /**
- * @var StudyCourseRepository
- */
- protected $studyCourseRepository;
-
- public function __construct(StudyCourseRepository $studyCourseRepository, CourseService $courseService)
- {
+ protected StudyCourseRepository $studyCourseRepository;
+ protected ModuleTemplateFactory $moduleTemplateFactory;
+ protected PageRenderer $pageRenderer;
+
+ public function __construct(
+ StudyCourseRepository $studyCourseRepository,
+ CourseService $courseService,
+ ModuleTemplateFactory $moduleTemplateFactory,
+ PageRenderer $pageRenderer
+ ) {
$this->studyCourseRepository = $studyCourseRepository;
$this->courseService = $courseService;
+ $this->moduleTemplateFactory = $moduleTemplateFactory;
+ $this->pageRenderer = $pageRenderer;
}
/**
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException
*/
- public function listAction(): void
+ public function listAction(): ResponseInterface
{
$this->validateSettings();
@@ -57,7 +65,7 @@ public function listAction(): void
$this->addFlashMessage(
LocalizationUtility::translate('messages.noCourses.body', 'in2studyfinder'),
LocalizationUtility::translate('messages.noCourses.title', 'in2studyfinder'),
- AbstractMessage::WARNING
+ ContextualFeedbackSeverity::WARNING
);
} else {
$propertyArray =
@@ -82,6 +90,13 @@ public function listAction(): void
'itemsPerPage' => $itemsPerPage
]
);
+
+ $moduleTemplate = $this->moduleTemplateFactory->create($this->request);
+ $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/In2studyfinder/Backend/Backend');
+ $this->pageRenderer->addCssFile('EXT:in2studyfinder/Resources/Public/Css/backend.css');
+ $moduleTemplate->setContent($this->view->render());
+
+ return $this->htmlResponse($moduleTemplate->renderContent());
}
/**
@@ -98,7 +113,7 @@ public function exportAction(
$this->addFlashMessage(
LocalizationUtility::translate('messages.notAllRequiredFieldsSet.body', 'in2studyfinder'),
LocalizationUtility::translate('messages.notAllRequiredFieldsSet.title', 'in2studyfinder'),
- AbstractMessage::ERROR
+ ContextualFeedbackSeverity::ERROR
);
$this->forward('list');
@@ -124,7 +139,7 @@ protected function getSysLanguages(): array
->from('sys_language')
->where($queryBuilder->expr()->eq('hidden', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)))
->orderBy('sorting')
- ->execute()->fetchAll();
+ ->executeQuery()->fetchAllAssociative();
foreach ($languageRecords as $languageRecord) {
$sysLanguages[(int)$languageRecord['uid']] =
@@ -153,7 +168,7 @@ protected function getPossibleExportDataProvider(): array
$this->addFlashMessage(
'export provider class "' . $providerClass . '" was not found',
'export provider class not found',
- AbstractMessage::ERROR
+ ContextualFeedbackSeverity::ERROR
);
} else {
$possibleDataProvider[$providerName] = $providerClass;
@@ -170,7 +185,7 @@ protected function validateSettings(): void
$this->addFlashMessage(
LocalizationUtility::translate('messages.noStoragePid.body', 'in2studyfinder'),
LocalizationUtility::translate('messages.noStoragePid.title', 'in2studyfinder'),
- AbstractMessage::ERROR
+ ContextualFeedbackSeverity::ERROR
);
}
@@ -178,7 +193,7 @@ protected function validateSettings(): void
$this->addFlashMessage(
LocalizationUtility::translate('messages.noSettingsPid.body', 'in2studyfinder'),
LocalizationUtility::translate('messages.noSettingsPid.title', 'in2studyfinder'),
- AbstractMessage::ERROR
+ ContextualFeedbackSeverity::ERROR
);
}
}
diff --git a/Classes/Controller/StudyCourseController.php b/Classes/Controller/StudyCourseController.php
index e22865d4..9385e0b6 100644
--- a/Classes/Controller/StudyCourseController.php
+++ b/Classes/Controller/StudyCourseController.php
@@ -14,6 +14,7 @@
use In2code\In2studyfinder\Utility\FlexFormUtility;
use In2code\In2studyfinder\Utility\FrontendUtility;
use In2code\In2studyfinder\Utility\RecordUtility;
+use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
@@ -39,40 +40,12 @@ public function __construct(
}
/**
- * Strip empty options from incoming (selected) filters
- *
- * @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException
+ * @param array $pluginInformation contains additional plugin information from ajax / fetch requests
*/
- public function initializeFilterAction(): void
+ public function filterAction(array $searchOptions = [], array $pluginInformation = []): ResponseInterface
{
$this->filterService->initialize();
- if ($this->request->hasArgument('searchOptions')) {
- $searchOptions = array_filter((array)$this->request->getArgument('searchOptions'));
- $this->request->setArgument('searchOptions', $searchOptions);
-
- if (ConfigurationUtility::isPersistentFilterEnabled()) {
- FrontendUtility::getTyposcriptFrontendController()
- ->fe_user
- ->setAndSaveSessionData('tx_in2studycourse_filter', $searchOptions);
- }
- } else {
- if (ConfigurationUtility::isPersistentFilterEnabled()) {
- $this->request->setArgument(
- 'searchOptions',
- FrontendUtility::getTyposcriptFrontendController()
- ->fe_user
- ->getSessionData('tx_in2studycourse_filter')
- );
- }
- }
- }
-
- /**
- * @param array $pluginInformation contains additional plugin information from ajax / fetch requests
- */
- public function filterAction(array $searchOptions = [], array $pluginInformation = []): void
- {
if (!empty($pluginInformation)) {
// if the current call is an ajax / fetch request
$currentPluginRecord =
@@ -90,8 +63,15 @@ public function filterAction(array $searchOptions = [], array $pluginInformation
$currentPluginRecord = $this->configurationManager->getContentObject()->data;
}
+ $this->filterService->setSettings($this->settings);
+ $searchOptions = $this->filterService->sanitizeSearch($searchOptions);
+
+ if (ConfigurationUtility::isPersistentFilterEnabled()) {
+ $searchOptions = $this->filterService->loadOrSetPersistedFilter($searchOptions);
+ }
+
$studyCourses = $this->courseService->findBySearchOptions(
- $this->filterService->setSettings($this->settings)->prepareSearchOptions($searchOptions),
+ $this->filterService->resolveFilterPropertyPath($searchOptions),
$currentPluginRecord
);
@@ -106,12 +86,14 @@ public function filterAction(array $searchOptions = [], array $pluginInformation
'data' => $currentPluginRecord
]
);
+
+ return $this->htmlResponse();
}
/**
* fastSearchAction
*/
- public function fastSearchAction(): void
+ public function fastSearchAction(): ResponseInterface
{
$currentPluginRecord = $this->configurationManager->getContentObject()->data;
$studyCourses =
@@ -126,6 +108,8 @@ public function fastSearchAction(): void
'data' => $currentPluginRecord
]
);
+
+ return $this->htmlResponse();
}
/**
@@ -156,7 +140,7 @@ public function initializeDetailAction(): void
*
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException
*/
- public function detailAction(StudyCourse $studyCourse = null): void
+ public function detailAction(StudyCourse $studyCourse = null): ResponseInterface
{
if ($studyCourse) {
$this->courseService->setPageTitleAndMetadata($studyCourse);
@@ -166,5 +150,7 @@ public function detailAction(StudyCourse $studyCourse = null): void
} else {
$this->redirect('filterAction', null, null, null, $this->settings['flexform']['studyCourseListPage']);
}
+
+ return $this->htmlResponse();
}
}
diff --git a/Classes/Domain/Repository/StudyCourseRepository.php b/Classes/Domain/Repository/StudyCourseRepository.php
index a3f4e48d..aed0510d 100644
--- a/Classes/Domain/Repository/StudyCourseRepository.php
+++ b/Classes/Domain/Repository/StudyCourseRepository.php
@@ -41,18 +41,14 @@ public function findAllFilteredByOptions($options): QueryResultInterface
foreach ($options as $name => $array) {
if ($array[0] === 'true') {
$constraints[] = $query->logicalOr(
- [
- $query->logicalNot($query->equals($name, '')),
- $query->greaterThan($name, 0),
- ]
+ $query->logicalNot($query->equals($name, '')),
+ $query->greaterThan($name, 0),
);
} elseif ($array[0] === 'false') {
$constraints[] = $query->logicalOr(
- [
- $query->equals($name, 0),
- $query->equals($name, ''),
- $query->equals($name, null),
- ]
+ $query->equals($name, 0),
+ $query->equals($name, ''),
+ $query->equals($name, null),
);
} else {
$constraints[] = $query->in($name . '.uid', $array);
@@ -60,7 +56,7 @@ public function findAllFilteredByOptions($options): QueryResultInterface
}
if (!empty($constraints)) {
- $query->matching($query->logicalAnd($constraints));
+ $query->matching($query->logicalAnd(...$constraints));
}
return $query->execute();
@@ -116,7 +112,7 @@ public function findByUidsAndLanguage(array $uids, int $sysLanguageUid): QueryRe
$constraints[] = $query->equals('sysLanguageUid', $sysLanguageUid);
}
- $query->matching($query->logicalAnd($constraints));
+ $query->matching($query->logicalAnd(...$constraints));
return $query->execute();
}
diff --git a/Classes/Event/ManipulateCsvPropertyBeforeExportEvent.php b/Classes/Event/ManipulateCsvPropertyBeforeExportEvent.php
new file mode 100644
index 00000000..bc30e341
--- /dev/null
+++ b/Classes/Event/ManipulateCsvPropertyBeforeExportEvent.php
@@ -0,0 +1,25 @@
+property = $property;
+ }
+
+ public function getProperty(): mixed
+ {
+ return $this->property;
+ }
+
+ public function setProperty(mixed $property): void
+ {
+ $this->property = $property;
+ }
+}
diff --git a/Classes/Export/AbstractExport.php b/Classes/Export/AbstractExport.php
index 5cbb6dda..d9388105 100644
--- a/Classes/Export/AbstractExport.php
+++ b/Classes/Export/AbstractExport.php
@@ -5,23 +5,18 @@
namespace In2code\In2studyfinder\Export;
use In2code\In2studyfinder\Export\Configuration\ExportConfiguration;
+use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
use TYPO3\CMS\Fluid\View\StandaloneView;
class AbstractExport implements ExportInterface
{
protected string $fileExtension = '';
-
- /**
- * @var Dispatcher
- */
- protected Dispatcher $signalSlotDispatcher;
+ protected EventDispatcherInterface $eventDispatcher;
public function __construct()
{
- // @todo replace signal with psr 14 event
- $this->signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
+ $this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
}
public function export(ExportConfiguration $exportConfiguration): string
diff --git a/Classes/Export/ExportTypes/CsvExport.php b/Classes/Export/ExportTypes/CsvExport.php
index 78361692..78d3017d 100644
--- a/Classes/Export/ExportTypes/CsvExport.php
+++ b/Classes/Export/ExportTypes/CsvExport.php
@@ -4,6 +4,7 @@
namespace In2code\In2studyfinder\Export\ExportTypes;
+use In2code\In2studyfinder\Event\ManipulateCsvPropertyBeforeExportEvent;
use In2code\In2studyfinder\Export\AbstractExport;
use In2code\In2studyfinder\Export\Configuration\ExportConfiguration;
use In2code\In2studyfinder\Export\ExportInterface;
@@ -17,19 +18,16 @@ class CsvExport extends AbstractExport implements ExportInterface
/**
* @throws Exception
- * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
- * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
*/
public function export(ExportConfiguration $exportConfiguration): string
{
$recordRows = [];
foreach ($exportConfiguration->getRecordsToExport() as $row => $record) {
- foreach (array_values($record) as $property) {
- $this->signalSlotDispatcher->dispatch(
- self::class,
- 'manipulatePropertyBeforeExport',
- [&$property]
- );
+ $recordRows[$row] = '';
+ foreach ($record as $property) {
+ $property = $this->eventDispatcher->dispatch(
+ new ManipulateCsvPropertyBeforeExportEvent($property)
+ )->getProperty();
$recordRows[$row] .= '"' . $property . '";';
}
diff --git a/Classes/Service/FilterService.php b/Classes/Service/FilterService.php
index 486c6efa..e88f8a47 100644
--- a/Classes/Service/FilterService.php
+++ b/Classes/Service/FilterService.php
@@ -6,6 +6,7 @@
use In2code\In2studyfinder\Domain\Model\StudyCourseInterface;
use In2code\In2studyfinder\Utility\ExtensionUtility;
+use In2code\In2studyfinder\Utility\FrontendUtility;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Utility\ClassNamingUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -47,27 +48,39 @@ public function getFilter(): array
}
/**
- * removes not allowed keys empty values from searchOptions and updates the filter keys to the actual property path
+ * removes not allowed keys and empty values from searchOptions
*/
- public function prepareSearchOptions(array $searchOptions): array
+ public function sanitizeSearch(array $searchOptions): array
{
// merge plugin restrictions to search options
$searchOptions = array_merge($searchOptions, $this->getPluginFilterRestrictions());
$this->disableFilterFrontendRenderingByPluginRestrictions();
$filter = $this->getFilter();
- // 1. remove not allowed keys
- foreach ($searchOptions as $filterName => $filterValues) {
+ // remove not allowed keys
+ foreach (array_keys($searchOptions) as $filterName) {
if (!array_key_exists($filterName, $filter)) {
unset($searchOptions[$filterName]);
}
}
- // 2. remove empty values
- $searchOptions = array_map('array_filter', $searchOptions);
- $searchOptions = array_filter($searchOptions);
+ // remove empty values
+ foreach ($searchOptions as $optionName => $optionValue) {
+ if (empty($optionValue)) {
+ unset($searchOptions[$optionName]);
+ }
+ }
+
+ return $searchOptions;
+ }
+
+ /**
+ * updates the filter keys to the actual property path
+ */
+ public function resolveFilterPropertyPath($searchOptions): array
+ {
+ $filter = $this->getFilter();
- // 3. set filter propertyPath as filter array key
foreach ($searchOptions as $filterName => $filterValues) {
$searchOptions[$filter[$filterName]['propertyPath']] = $filterValues;
if ($filter[$filterName]['propertyPath'] !== $filterName) {
@@ -78,6 +91,24 @@ public function prepareSearchOptions(array $searchOptions): array
return $searchOptions;
}
+ public function loadOrSetPersistedFilter(array $searchOptions): array
+ {
+ if (!empty($searchOptions)) {
+ FrontendUtility::getTyposcriptFrontendController()
+ ->fe_user
+ ->setAndSaveSessionData('tx_in2studycourse_filter', array_filter($searchOptions));
+ } else {
+ $sessionData = FrontendUtility::getTyposcriptFrontendController()
+ ->fe_user
+ ->getSessionData('tx_in2studycourse_filter');
+ if (!empty($sessionData)) {
+ return (array)$sessionData;
+ }
+ }
+
+ return $searchOptions;
+ }
+
public function setSettings(array $settings): FilterService
{
$this->settings = $settings;
@@ -175,11 +206,7 @@ protected function buildObjectFilter(string $filterName, array $filterConfigurat
$defaultQuerySettings->setStoragePageIds([$this->settings['settingsPid']]);
$defaultQuerySettings->setLanguageOverlayMode(true);
- // In TYPO3 11 repositories still need the Object Manager for initialization
- // this will change with TYPO3 12
- $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
- $repository = $objectManager->get($repositoryClassName);
-
+ $repository = GeneralUtility::makeInstance($repositoryClassName);
$repository->setDefaultQuerySettings($defaultQuerySettings);
$this->filter[$filterName]['repository'] = $repositoryClassName;
@@ -215,7 +242,7 @@ protected function buildFilter(): void
'propertyPath' => $filterProperties['propertyPath'],
'frontendLabel' => $this->buildFrontendLabel($filterProperties),
'disabledInFrontend' => $this->isFilterInFrontendVisible($filterProperties),
- 'singleSelect' => $filterProperties['singleSelect']
+ 'singleSelect' => $filterProperties['singleSelect'] ?? ''
];
switch ($filterProperties['type']) {
diff --git a/Classes/Service/SlugService.php b/Classes/Service/SlugService.php
index 07af0306..f3163db9 100644
--- a/Classes/Service/SlugService.php
+++ b/Classes/Service/SlugService.php
@@ -12,10 +12,7 @@
class SlugService
{
- protected ?QueryBuilder $queryBuilder = null;
-
protected array $fieldConfig = [];
-
protected ?SlugHelper $slugHelper = null;
/**
@@ -31,10 +28,6 @@ public function __construct()
'path_segment',
$this->fieldConfig
);
-
- /** @var QueryBuilder $queryBuilder */
- $this->queryBuilder =
- GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(StudyCourse::TABLE);
}
/**
@@ -42,34 +35,36 @@ public function __construct()
*/
public function performUpdates(): array
{
- $this->queryBuilder->getRestrictions()->removeAll();
+ $queryBuilder =
+ GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(StudyCourse::TABLE);
+
+ $queryBuilder->getRestrictions()->removeAll();
$databaseQueries = [];
- $statement = $this->queryBuilder->select('*')
- ->from(StudyCourse::TABLE)
+ $records = $queryBuilder->select('*')->from(StudyCourse::TABLE)
->where(
- $this->queryBuilder->expr()->orX(
- $this->queryBuilder->expr()->eq(
+ $queryBuilder->expr()->or(
+ $queryBuilder->expr()->eq(
'url_segment',
- $this->queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
+ $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
),
- $this->queryBuilder->expr()->isNull('url_segment')
+ $queryBuilder->expr()->isNull('url_segment')
)
- )
- ->execute();
- while ($record = $statement->fetch()) {
+ )->executeQuery()->fetchAllAssociative();
+
+ foreach ($records as $record) {
if ((string)$record['title'] !== '') {
$slug = $this->slugHelper->generate($record, (int)$record['pid']);
- $this->queryBuilder->update(StudyCourse::TABLE)
+ $queryBuilder->update(StudyCourse::TABLE)
->where(
- $this->queryBuilder->expr()->eq(
+ $queryBuilder->expr()->eq(
'uid',
- $this->queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
+ $queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
)
)
->set('url_segment', $slug);
- $databaseQueries[] = $this->queryBuilder->getSQL();
- $this->queryBuilder->execute();
+ $databaseQueries[] = $queryBuilder->getSQL();
+ $queryBuilder->executeStatement();
}
}
@@ -81,20 +76,23 @@ public function performUpdates(): array
*/
public function isSlugUpdateRequired(): bool
{
- $this->queryBuilder->getRestrictions()->removeAll();
+ $queryBuilder =
+ GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(StudyCourse::TABLE);
+
+ $queryBuilder->getRestrictions()->removeAll();
- $count = $this->queryBuilder->count('uid')
+ $count = $queryBuilder->count('uid')
->from(StudyCourse::TABLE)
->where(
- $this->queryBuilder->expr()->orX(
- $this->queryBuilder->expr()->eq(
+ $queryBuilder->expr()->orX(
+ $queryBuilder->expr()->eq(
'url_segment',
- $this->queryBuilder->createNamedParameter('')
+ $queryBuilder->createNamedParameter('')
),
- $this->queryBuilder->expr()->isNull('url_segment')
+ $queryBuilder->expr()->isNull('url_segment')
)
)
- ->execute()->fetchColumn(0);
+ ->executeQuery()->fetchOne();
return $count > 0;
}
diff --git a/Classes/Slug/UrlSegmentPostModifier.php b/Classes/Slug/UrlSegmentPostModifier.php
index 5f493d5e..a44d06cd 100644
--- a/Classes/Slug/UrlSegmentPostModifier.php
+++ b/Classes/Slug/UrlSegmentPostModifier.php
@@ -8,17 +8,20 @@
use In2code\In2studyfinder\Domain\Model\Graduation;
use In2code\In2studyfinder\Domain\Model\StudyCourse;
use LogicException;
+use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
- * Class UrlSegmentPostModifier
+ * @SuppressWarnings(PHPMD.Superglobals)
*/
class UrlSegmentPostModifier
{
protected array $configuration = [];
+ protected int $courseId = -1;
+ protected int $academicDegree = -1;
/**
* @noinspection PhpUnusedParameterInspection
@@ -28,17 +31,17 @@ class UrlSegmentPostModifier
public function extendWithGraduation(array $configuration, SlugHelper $slugHelper): string
{
$this->configuration = $configuration;
- $graduationTitle = '';
- if ($this->isNewRecord()) {
- if (!empty($this->configuration['record']['academic_degree'])) {
- $graduationTitle =
- $this->getGraduationTitle((int)$this->configuration['record']['academic_degree']);
- }
- } else {
- $graduationTitle = $this->getGraduationTitle();
+ if (!$this->isUpgradeWizard() && !$this->isNewRecord()) {
+ $this->courseId = $this->getStudyCourseRecordIdentifier();
+ }
+
+ if (!empty($this->configuration['record']['academic_degree'])) {
+ $this->academicDegree = (int)$this->configuration['record']['academic_degree'];
}
+ $graduationTitle = $this->getGraduationTitle();
+
if (!empty($graduationTitle)) {
$slug = $configuration['slug'] . '-' . $graduationTitle;
} else {
@@ -48,18 +51,15 @@ public function extendWithGraduation(array $configuration, SlugHelper $slugHelpe
return $slug;
}
- /**
- * @throws \Doctrine\DBAL\DBALException
- */
- protected function getGraduationTitle(int $academicDegreeUid = -1): string
+ protected function getGraduationTitle(): string
{
$queryBuilder =
GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(StudyCourse::TABLE);
$queryBuilder->select(Graduation::TABLE . '.title');
- if ($academicDegreeUid > 0) {
- $queryBuilder
+ if ($this->academicDegree > 0) {
+ return (string)$queryBuilder
->from(Graduation::TABLE)
->leftJoin(
Graduation::TABLE,
@@ -70,8 +70,14 @@ protected function getGraduationTitle(int $academicDegreeUid = -1): string
AcademicDegree::TABLE . '.graduation'
)
)
- ->where($queryBuilder->expr()->eq(AcademicDegree::TABLE . '.uid', $academicDegreeUid));
- } else {
+ ->where(
+ $queryBuilder->expr()->eq(AcademicDegree::TABLE . '.uid',
+ $this->academicDegree
+ )
+ )->executeQuery()->fetchOne();
+ }
+
+ if ($this->courseId > 0) {
$queryBuilder->from(StudyCourse::TABLE)
->leftJoin(
StudyCourse::TABLE,
@@ -92,45 +98,45 @@ protected function getGraduationTitle(int $academicDegreeUid = -1): string
)
)
->where(
- $queryBuilder->expr()->eq(StudyCourse::TABLE . '.uid', $this->getStudyCourseRecordIdentifier())
+ $queryBuilder->expr()->eq(StudyCourse::TABLE . '.uid', $this->courseId)
);
+
+ return (string)$queryBuilder->executeQuery()->fetchOne();
}
- return (string)$queryBuilder->execute()->fetchColumn();
+ return '';
}
- protected function isNewRecord(): bool
+ private function isUpgradeWizard(): bool
{
- if ($this->isRecalculateSlug()) {
- $recordUid = GeneralUtility::_GP('recordId');
-
- if (!MathUtility::canBeInterpretedAsInteger($recordUid)) {
- return true;
- }
- } else {
- $data = GeneralUtility::_GP('data');
- if (is_array($data) && key($data) === StudyCourse::TABLE) {
- return true;
- }
- }
+ return !is_null(GeneralUtility::_GP('install')) &&
+ array_key_exists('action', GeneralUtility::_GP('install')) &&
+ GeneralUtility::_GP('install')['action'] === 'upgradeWizardsExecute';
+ }
- return false;
+ protected function isNewRecord(): bool
+ {
+ return $this->isRecalculateSlug() &&
+ !MathUtility::canBeInterpretedAsInteger($this->getRequest()->getParsedBody()['recordId']);
}
protected function getStudyCourseRecordIdentifier(): int
{
- if (!empty($this->configuration['record']['uid'])) {
- $identifier = $this->configuration['record']['uid'];
- } elseif ((int)GeneralUtility::_GP('recordId') > 0) {
- $identifier = (int)GeneralUtility::_GP('recordId');
- } else {
+ $identifier = $this->getRequest()->getParsedBody()['recordId'];
+ if (!MathUtility::canBeInterpretedAsInteger($identifier)) {
throw new LogicException('No record identifier given', 1585056768);
}
- return $identifier;
+
+ return (int)$identifier;
}
protected function isRecalculateSlug(): bool
{
- return GeneralUtility::_GP('route') === '/ajax/record/slug/suggest';
+ return $this->getRequest()->getAttribute('route')->getPath() === '/ajax/record/slug/suggest';
+ }
+
+ private function getRequest(): ServerRequestInterface
+ {
+ return $GLOBALS['TYPO3_REQUEST'];
}
}
diff --git a/Classes/Utility/PageUtility.php b/Classes/Utility/PageUtility.php
index 0ffec7e6..1df45f91 100644
--- a/Classes/Utility/PageUtility.php
+++ b/Classes/Utility/PageUtility.php
@@ -40,13 +40,13 @@ public function getTreeList(int $uid, int $depth, int $begin = 0, string $permCl
if ($permClause !== '') {
$queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($permClause));
}
- $statement = $queryBuilder->execute();
- while ($row = $statement->fetchAssociative()) {
+
+ foreach ($queryBuilder->executeQuery()->fetchAllAssociative() as $page) {
if ($begin <= 0) {
- $theList .= ',' . $row['uid'];
+ $theList .= ',' . $page['uid'];
}
if ($depth > 1) {
- $theSubList = self::getTreeList($row['uid'], $depth - 1, $begin - 1, $permClause);
+ $theSubList = self::getTreeList((int)$page['uid'], $depth - 1, $begin - 1, $permClause);
if (!empty($theList) && !empty($theSubList) && ($theSubList[0] !== ',')) {
$theList .= ',';
}
diff --git a/Classes/Utility/RecordUtility.php b/Classes/Utility/RecordUtility.php
index 0fe26474..eb71a9c0 100644
--- a/Classes/Utility/RecordUtility.php
+++ b/Classes/Utility/RecordUtility.php
@@ -51,7 +51,7 @@ public static function getRecordWithTranslations(int $uid): array
'l18n_parent',
$queryBuilder->createNamedParameter((int)$records[0]['uid'], \PDO::PARAM_INT)
)
- )->execute()->fetchAll();
+ )->executeQuery()->fetchAllAssociative();
foreach ($translatedRecords as $translatedRecord) {
if (!array_key_exists((int)$translatedRecord['sys_language_uid'], $records)) {
@@ -139,7 +139,7 @@ public static function getRecord(
$queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
}
- $row = $queryBuilder->execute()->fetch();
+ $row = $queryBuilder->executeQuery()->fetchAssociative();
if ($row) {
return $row;
}
diff --git a/Classes/ViewHelpers/Form/AbstractCheckboxViewHelper.php b/Classes/ViewHelpers/Form/AbstractCheckboxViewHelper.php
deleted file mode 100755
index 11898e57..00000000
--- a/Classes/ViewHelpers/Form/AbstractCheckboxViewHelper.php
+++ /dev/null
@@ -1,51 +0,0 @@
-registerArgument(
- 'possibleFilters',
- 'array',
- 'Array which holds "propertyType" => array("uid", ...)" for each available filter option',
- true,
- []
- );
- $this->registerArgument(
- 'searchedOptions',
- 'array',
- 'Array of the previously selected filter options',
- true,
- []
- );
- }
-
- protected function setDisabledIfNotAvailable(): void
- {
- [$propertyName, $objectId] = explode('_', $this->arguments['id']);
-
- if (is_array($this->arguments['possibleFilters']) && !empty($this->arguments['possibleFilters'])) {
- if (
- !isset($this->arguments['possibleFilters'][$propertyName])
- || !in_array($objectId, $this->arguments['possibleFilters'][$propertyName])
- ) {
- $this->tag->addAttribute('disabled', 'disabled');
- }
- }
- }
-
- protected function setSelectedIfPreviouslySelected(): void
- {
- [$propertyName, $objectId] = explode('_', $this->arguments['id']);
- if (isset($this->arguments['searchedOptions'][$propertyName])) {
- if (in_array($objectId, $this->arguments['searchedOptions'][$propertyName])) {
- $this->tag->addAttribute('checked', true);
- }
- }
- }
-}
diff --git a/Classes/ViewHelpers/Form/AbstractSelectViewHelper.php b/Classes/ViewHelpers/Form/AbstractSelectViewHelper.php
new file mode 100644
index 00000000..9de13a84
--- /dev/null
+++ b/Classes/ViewHelpers/Form/AbstractSelectViewHelper.php
@@ -0,0 +1,275 @@
+registerUniversalTagAttributes();
+ $this->registerTagAttribute('size', 'string', 'Size of input field');
+ $this->registerTagAttribute('disabled', 'string', 'Specifies that the input element should be disabled when the page loads');
+ $this->registerArgument('options', 'array', 'Associative array with internal IDs as key, and the values are displayed in the select box. Can be combined with or replaced by child f:form.select.* nodes.');
+ $this->registerArgument('optionsAfterContent', 'boolean', 'If true, places auto-generated option tags after those rendered in the tag content. If false, automatic options come first.', false, false);
+ $this->registerArgument('optionValueField', 'string', 'If specified, will call the appropriate getter on each object to determine the value.');
+ $this->registerArgument('optionLabelField', 'string', 'If specified, will call the appropriate getter on each object to determine the label.');
+ $this->registerArgument('sortByOptionLabel', 'boolean', 'If true, List will be sorted by label.', false, false);
+ $this->registerArgument('selectAllByDefault', 'boolean', 'If specified options are selected if none was set before.', false, false);
+ $this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
+ $this->registerArgument('prependOptionLabel', 'string', 'If specified, will provide an option at first position with the specified label.');
+ $this->registerArgument('prependOptionValue', 'string', 'If specified, will provide an option at first position with the specified value.');
+ $this->registerArgument('multiple', 'boolean', 'If set multiple options may be selected.', false, false);
+ $this->registerArgument('required', 'boolean', 'If set no empty value is allowed.', false, false);
+ }
+
+ public function render(): string
+ {
+ if ($this->arguments['required']) {
+ $this->tag->addAttribute('required', 'required');
+ }
+ $name = $this->getName();
+ if ($this->arguments['multiple']) {
+ $this->tag->addAttribute('multiple', 'multiple');
+ $name .= '[]';
+ }
+ $this->tag->addAttribute('name', $name);
+ $options = $this->getOptions();
+
+ $viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
+
+ $this->addAdditionalIdentityPropertiesIfNeeded();
+ $this->setErrorClassAttribute();
+ $content = '';
+
+ // register field name for token generation.
+ $this->registerFieldNameForFormTokenGeneration($name);
+ // in case it is a multi-select, we need to register the field name
+ // as often as there are elements in the box
+ if ($this->arguments['multiple']) {
+ $content .= $this->renderHiddenFieldForEmptyValue();
+ // Register the field name additional times as required by the total number of
+ // options. Since we already registered it once above, we start the counter at 1
+ // instead of 0.
+ $optionsCount = count($options);
+ for ($i = 1; $i < $optionsCount; $i++) {
+ $this->registerFieldNameForFormTokenGeneration($name);
+ }
+ // save the parent field name so that any child f:form.select.option
+ // tag will know to call registerFieldNameForFormTokenGeneration
+ // this is the reason why "self::class" is used instead of static::class (no LSB)
+ $viewHelperVariableContainer->addOrUpdate(
+ self::class,
+ 'registerFieldNameForFormTokenGeneration',
+ $name
+ );
+ }
+
+ $viewHelperVariableContainer->addOrUpdate(self::class, 'selectedValue', $this->getSelectedValue());
+ $prependContent = $this->renderPrependOptionTag();
+ $tagContent = $this->renderOptionTags($options);
+ $childContent = $this->renderChildren();
+ $viewHelperVariableContainer->remove(self::class, 'selectedValue');
+ $viewHelperVariableContainer->remove(self::class, 'registerFieldNameForFormTokenGeneration');
+ if (isset($this->arguments['optionsAfterContent']) && $this->arguments['optionsAfterContent']) {
+ $tagContent = $childContent . $tagContent;
+ } else {
+ $tagContent .= $childContent;
+ }
+ $tagContent = $prependContent . $tagContent;
+
+ $this->tag->forceClosingTag(true);
+ $this->tag->setContent($tagContent);
+ $content .= $this->tag->render();
+ return $content;
+ }
+
+ /**
+ * Render prepended option tag
+ */
+ protected function renderPrependOptionTag(): string
+ {
+ $output = '';
+ if ($this->hasArgument('prependOptionLabel')) {
+ $value = $this->hasArgument('prependOptionValue') ? $this->arguments['prependOptionValue'] : '';
+ $label = $this->arguments['prependOptionLabel'];
+ $output .= $this->renderOptionTag((string)$value, (string)$label, false) . LF;
+ }
+ return $output;
+ }
+
+ /**
+ * Render the option tags.
+ */
+ protected function renderOptionTags(array $options): string
+ {
+ $output = '';
+ foreach ($options as $value => $label) {
+ $isSelected = $this->isSelected($value);
+ $output .= $this->renderOptionTag((string)$value, (string)$label, $isSelected) . LF;
+ }
+ return $output;
+ }
+
+ /**
+ * Render the option tags.
+ *
+ * @return array An associative array of options, key will be the value of the option tag
+ *
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
+ protected function getOptions(): array
+ {
+ if (!is_array($this->arguments['options']) && !$this->arguments['options'] instanceof \Traversable) {
+ return [];
+ }
+ $options = [];
+ $optionsArgument = $this->arguments['options'];
+ foreach ($optionsArgument as $key => $value) {
+ if (is_object($value) || is_array($value)) {
+ if ($this->hasArgument('optionValueField')) {
+ $key = ObjectAccess::getPropertyPath($value, $this->arguments['optionValueField']);
+ if (is_object($key)) {
+ if (method_exists($key, '__toString')) {
+ $key = (string)$key;
+ } else {
+ throw new Exception('Identifying value for object of class "' . get_debug_type($value) . '" was an object.', 1247827428);
+ }
+ }
+ } elseif ($this->persistenceManager->getIdentifierByObject($value) !== null) {
+ // @todo use $this->persistenceManager->isNewObject() once it is implemented
+ $key = $this->persistenceManager->getIdentifierByObject($value);
+ } elseif (is_object($value) && method_exists($value, '__toString')) {
+ $key = (string)$value;
+ } elseif (is_object($value)) {
+ throw new Exception('No identifying value for object of class "' . get_class($value) . '" found.', 1247826696);
+ }
+ if ($this->hasArgument('optionLabelField')) {
+ $value = ObjectAccess::getPropertyPath($value, $this->arguments['optionLabelField']);
+ if (is_object($value)) {
+ if (method_exists($value, '__toString')) {
+ $value = (string)$value;
+ } else {
+ throw new Exception('Label value for object of class "' . get_class($value) . '" was an object without a __toString() method.', 1247827553);
+ }
+ }
+ } elseif (is_object($value) && method_exists($value, '__toString')) {
+ $value = (string)$value;
+ } elseif ($this->persistenceManager->getIdentifierByObject($value) !== null) {
+ // @todo use $this->persistenceManager->isNewObject() once it is implemented
+ $value = $this->persistenceManager->getIdentifierByObject($value);
+ }
+ }
+ $options[$key] = $value;
+ }
+ if ($this->arguments['sortByOptionLabel']) {
+ asort($options, SORT_LOCALE_STRING);
+ }
+ return $options;
+ }
+
+ /**
+ * Render the option tags.
+ *
+ * @param mixed $value Value to check for
+ * @return bool True if the value should be marked as selected.
+ */
+ protected function isSelected($value): bool
+ {
+ $selectedValue = $this->getSelectedValue();
+ if ($value === $selectedValue || (string)$value === $selectedValue) {
+ return true;
+ }
+ if ($this->hasArgument('multiple')) {
+ if ($selectedValue === null && $this->arguments['selectAllByDefault'] === true) {
+ return true;
+ }
+ if (is_array($selectedValue) && in_array($value, $selectedValue)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Retrieves the selected value(s)
+ *
+ * @return mixed value string or an array of strings
+ */
+ protected function getSelectedValue()
+ {
+ $this->setRespectSubmittedDataValue(true);
+ $value = $this->getValueAttribute();
+ if (!is_array($value) && !$value instanceof \Traversable) {
+ return $this->getOptionValueScalar($value);
+ }
+ $selectedValues = [];
+ foreach ($value as $selectedValueElement) {
+ $selectedValues[] = $this->getOptionValueScalar($selectedValueElement);
+ }
+ return $selectedValues;
+ }
+
+ /**
+ * Get the option value for an object
+ *
+ * @param mixed $valueElement
+ * @return string @todo: Does not always return string ...
+ */
+ protected function getOptionValueScalar($valueElement)
+ {
+ if (is_object($valueElement)) {
+ if ($this->hasArgument('optionValueField')) {
+ return ObjectAccess::getPropertyPath($valueElement, $this->arguments['optionValueField']);
+ }
+ // @todo use $this->persistenceManager->isNewObject() once it is implemented
+ if ($this->persistenceManager->getIdentifierByObject($valueElement) !== null) {
+ return $this->persistenceManager->getIdentifierByObject($valueElement);
+ }
+ return (string)$valueElement;
+ }
+ return $valueElement;
+ }
+
+ /**
+ * Render one option tag
+ *
+ * @param string $value value attribute of the option tag (will be escaped)
+ * @param string $label content of the option tag (will be escaped)
+ * @param bool $isSelected specifies whether to add selected attribute
+ * @return string the rendered option tag
+ */
+ protected function renderOptionTag(string $value, string $label, bool $isSelected): string
+ {
+ $output = '