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