Commit 09d9c87b authored by Oliver Bartsch's avatar Oliver Bartsch
Browse files

[!!!][FEATURE] Automatically register linktypes via service configuration

Linkvalidator linktypes are now automatically tagged and registered, based on
the implemented `LinktypeInterface`, using the autoconfiguration
feature from the DI container.

The previous registration via
`$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks']`
has been removed.

Additionally, to be able to use autoconfiguration, the identifier
of a linktype has to be provided by the service directly using the
now required :php:`getIdentifier()` method.

Resolves: #96935
Releases: main
Change-Id: Ie7e53b2b9fb73394fa39d5cb84597d5e48326e29
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/73568

Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
parent 3334a963
.. include:: ../../Includes.txt
=============================================================================
Breaking: #96935 - Register linkvalidator linktypes via service configuration
=============================================================================
See :issue:`96935`
Description
===========
Linkvalidator `linktypes` are now registered via service configuration, also see
:doc:`feature changelog <Feature-96935-NewRegistrationForLinkvalidatorLinktype>`.
Therefore the registration via
:php:`$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks']`
has been removed.
Additionally, to be able to use autoconfiguration, the `linktype` identifier
has to be provided by the service directly using the :php:`getIdentifier()`
method, which is now required by the :php:`LinktypeInterface`.
In case a custom `linktype` extends
:php:`\TYPO3\CMS\Linkvalidator\Linktype\AbstractLinktype`,
only the class property `$identifier` has to be set, e.g.
:php:`protected string $identifier = 'my_linktype';`.
Impact
======
Registration of custom `linktypes` via
:php:`$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks']`
is not evaluated anymore.
The :php:`LinktypeInterface` is extended for
:php:`public function getIdentifier(): string`.
Affected Installations
======================
All TYPO3 installations using the old registration.
All TYPO3 installations with custom `linktypes`, not implementing
:php:`public function getIdentifier()`.
Migration
=========
Remove :php:`$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks']`
from your :file:`ext_localconf.php` file.
If :yaml:`autoconfigure` is not enabled in your :file:`Configuration/Services.(yaml|php)`,
add the tag :yaml:`linkvalidator.linktype` manually to your `linktype` service.
.. code-block:: yaml
Vendor\Extension\Linktype\MyCustomLinktype:
tags:
- name: linkvalidator.linktype
Additionally, make sure to either implement
:php:`public function getIdentifier(): string` or, in case your `linktype` extends
:php:`AbstractLinktype`, to set the `$identifier` class property.
.. index:: Backend, LocalConfiguration, PHP-API, FullyScanned, ext:linkvalidator
.. include:: ../../Includes.txt
=============================================================
Feature: #96935 - New registration for linkvalidator linktype
=============================================================
See :issue:`96935`
Description
===========
The system extension `linkvalidator` uses so called `linktypes` for
checking different types of links, e.g. internal or external links.
All `linktypes` have to implement the :php:`LinktypeInterface`.
This fact is now used to automatically registere the `linktypes`, based
on the interface, if :yaml:`autoconfigure` is enabled in :file:`Services.yaml`.
Alternatively, one can manually tag a custom `linktype` with the
:yaml:`linkvalidator.linktype` tag (See section "Migration" in the
:doc:`breaking changelog <Breaking-96935-RegisterLinkvalidatorLinktypesViaServiceConfiguration>`).
Due to the autoconfiguration, the identifier has to be provided by the
class directly, using the now required :php:`getIdentifier()` method.
When extending :php:`\TYPO3\CMS\Linkvalidator\Linktype\AbstractLinktype`
it's sufficient to set the `$identifier` class property.
Impact
======
`linktypes` are now automatically registered through the service configuration,
based on the implemented interface.
.. index:: Backend, LocalConfiguration, PHP-API, ext:linkvalidator
......@@ -614,4 +614,9 @@ return [
'Breaking-96899-DisplayWarningMessagesHookRemoved.rst',
],
],
'$GLOBALS[\'TYPO3_CONF_VARS\'][\'EXTCONF\'][\'linkvalidator\'][\'checkLinks\']' => [
'restFiles' => [
'Breaking-96935-RegisterLinkvalidatorLinktypesViaServiceConfiguration.rst',
],
],
];
......@@ -34,7 +34,7 @@ use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Linkvalidator\LinkAnalyzer;
use TYPO3\CMS\Linkvalidator\Linktype\LinktypeInterface;
use TYPO3\CMS\Linkvalidator\Linktype\LinktypeRegistry;
use TYPO3\CMS\Linkvalidator\Repository\BrokenLinkRepository;
use TYPO3\CMS\Linkvalidator\Repository\PagesRepository;
......@@ -78,11 +78,6 @@ class LinkValidatorController
'timestamp' => 0,
];
/**
* @var LinktypeInterface[]
*/
protected array $hookObjectsArr = [];
protected int $id;
protected array $searchFields = [];
......@@ -96,6 +91,7 @@ class LinkValidatorController
protected readonly BrokenLinkRepository $brokenLinkRepository,
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
protected readonly LinkAnalyzer $linkAnalyzer,
protected readonly LinktypeRegistry $linktypeRegistry,
) {
}
......@@ -115,14 +111,6 @@ class LinkValidatorController
$view->getDocHeaderComponent()->setMetaInformation($this->pageRecord);
}
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] ?? [] as $linkType => $className) {
$hookObject = GeneralUtility::makeInstance($className);
if (!$hookObject instanceof LinktypeInterface) {
continue;
}
$this->hookObjectsArr[$linkType] = $hookObject;
}
$this->validateSettings();
$this->initializeLinkAnalyzer();
......@@ -235,7 +223,7 @@ class LinkValidatorController
$set = $this->request->getParsedBody()[$prefix . '_SET'] ?? [];
$submittedValues = $this->request->getParsedBody()[$prefix . '_values'] ?? [];
foreach (array_keys($this->hookObjectsArr) as $linkType) {
foreach ($this->linktypeRegistry->getIdentifiers() as $linkType) {
// Compile list of all available types. Used for checking with button "Check Links".
unset($this->checkOpt[$prefix][$linkType]);
$mainLinkType = $prefix . '_' . $linkType;
......@@ -382,7 +370,7 @@ class LinkValidatorController
$fieldLabel = $row['field'];
$table = $row['table_name'];
$languageService = $this->getLanguageService();
$hookObj = $this->hookObjectsArr[$row['link_type'] ?? ''];
$hookObj = $this->linktypeRegistry->getLinktype($row['link_type'] ?? '');
// Try to resolve the field label from TCA
if ($GLOBALS['TCA'][$table]['columns'][$row['field']]['label'] ?? false) {
......@@ -400,9 +388,9 @@ class LinkValidatorController
'label' => sprintf($languageService->getLL('list.field'), $fieldLabel),
'path' => BackendUtility::getRecordPath($row['record_pid'], $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 0),
'linkTitle' => $row['link_title'],
'linkTarget' => $hookObj->getBrokenUrl($row),
'linkTarget' => $hookObj?->getBrokenUrl($row),
'linkStatus' => (bool)($row['url_response']['valid'] ?? false),
'linkMessage' => $hookObj->getErrorMessage($row['url_response']['errorParams']),
'linkMessage' => $hookObj?->getErrorMessage($row['url_response']['errorParams']),
'lastCheck' => sprintf(
$languageService->getLL('list.msg.lastRun'),
date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $row['last_check']),
......@@ -445,8 +433,7 @@ class LinkValidatorController
'optionsByType' => [],
];
$linkTypes = GeneralUtility::trimExplode(',', $this->modTS['linktypes'] ?? '', true);
$availableLinkTypes = array_keys($this->hookObjectsArr);
foreach ($availableLinkTypes as $type) {
foreach ($this->linktypeRegistry->getIdentifiers() as $type) {
if (!in_array($type, $linkTypes, true)) {
continue;
}
......
......@@ -28,7 +28,7 @@ use TYPO3\CMS\Core\Html\HtmlParser;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Linkvalidator\Event\BeforeRecordIsAnalyzedEvent;
use TYPO3\CMS\Linkvalidator\Linktype\LinktypeInterface;
use TYPO3\CMS\Linkvalidator\Linktype\LinktypeRegistry;
use TYPO3\CMS\Linkvalidator\Repository\BrokenLinkRepository;
/**
......@@ -61,13 +61,6 @@ class LinkAnalyzer
*/
protected array $brokenLinkCounts = [];
/**
* Array for hooks for own checks
*
* @var LinktypeInterface[]
*/
protected array $hookObjectsArr = [];
/**
* The currently active TSconfig. Will be passed to the init function.
*
......@@ -75,18 +68,12 @@ class LinkAnalyzer
*/
protected $tsConfig = [];
protected EventDispatcherInterface $eventDispatcher;
protected BrokenLinkRepository $brokenLinkRepository;
protected SoftReferenceParserFactory $softReferenceParserFactory;
public function __construct(
EventDispatcherInterface $eventDispatcher,
BrokenLinkRepository $brokenLinkRepository,
SoftReferenceParserFactory $softReferenceParserFactory
protected readonly EventDispatcherInterface $eventDispatcher,
protected readonly BrokenLinkRepository $brokenLinkRepository,
protected readonly SoftReferenceParserFactory $softReferenceParserFactory,
protected readonly LinktypeRegistry $linktypeRegistry,
) {
$this->eventDispatcher = $eventDispatcher;
$this->brokenLinkRepository = $brokenLinkRepository;
$this->softReferenceParserFactory = $softReferenceParserFactory;
$this->getLanguageService()->includeLLFile('EXT:linkvalidator/Resources/Private/Language/Module/locallang.xlf');
}
......@@ -103,16 +90,11 @@ class LinkAnalyzer
$this->pids = $pidList;
$this->tsConfig = $tsConfig;
// Hook to handle own checks
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] ?? [] as $key => $className) {
$hookObject = GeneralUtility::makeInstance($className);
if (!$hookObject instanceof LinktypeInterface) {
continue;
foreach ($this->linktypeRegistry->getLinktypes() as $identifier => $linktype) {
if (is_array($tsConfig['linktypesConfig.'][$identifier . '.'] ?? false)) {
// setAdditionalConfig might use global configuration, so still call it, even if options are empty
$linktype->setAdditionalConfig($tsConfig['linktypesConfig.'][$identifier . '.']);
}
$this->hookObjectsArr[$key] = $hookObject;
$options = $tsConfig['linktypesConfig.'][$key . '.'] ?? [];
// setAdditionalConfig might use global configuration, so still call it, even if options are empty
$this->hookObjectsArr[$key]->setAdditionalConfig($options);
}
}
......@@ -196,7 +178,7 @@ class LinkAnalyzer
*/
protected function checkLinks(array $links, array $linkTypes)
{
foreach ($this->hookObjectsArr as $key => $hookObj) {
foreach ($this->linktypeRegistry->getLinktypes() as $key => $hookObj) {
if (!is_array($links[$key] ?? false) || (!in_array($key, $linkTypes, true))) {
continue;
}
......@@ -376,7 +358,7 @@ class LinkAnalyzer
continue;
}
foreach ($this->hookObjectsArr as $keyArr => $hookObj) {
foreach ($this->linktypeRegistry->getLinktypes() as $keyArr => $hookObj) {
$type = $hookObj->fetchType($reference, $type, $keyArr);
// Store the type that was found
// This prevents overriding by internal validator
......@@ -439,7 +421,7 @@ class LinkAnalyzer
if (empty($currentR)) {
continue;
}
foreach ($this->hookObjectsArr as $keyArr => $hookObj) {
foreach ($this->linktypeRegistry->getLinktypes() as $keyArr => $hookObj) {
$type = $hookObj->fetchType($currentR, $type, $keyArr);
// Store the type that was found
// This prevents overriding by internal validator
......
......@@ -29,6 +29,13 @@ abstract class AbstractLinktype implements LinktypeInterface
*/
protected $errorParams = [];
protected string $identifier = '';
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* Function to override config of Linktype. Should be used only
* if necessary. Add additional configuration to TSconfig.
......
......@@ -93,6 +93,8 @@ class ExternalLinktype extends AbstractLinktype
*/
protected $errorParams = [];
protected string $identifier = 'external';
public function __construct(
protected readonly RequestFactory $requestFactory,
) {
......
......@@ -25,6 +25,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class FileLinktype extends AbstractLinktype
{
protected string $identifier = 'file';
/**
* Type fetching method, based on the type that softRefParserObj returns
*
......
......@@ -57,6 +57,8 @@ class InternalLinktype extends AbstractLinktype
*/
protected $responseContent = true;
protected string $identifier = 'db';
/**
* Checks a given URL + /path/filename.ext for validity
*
......
......@@ -20,6 +20,11 @@ namespace TYPO3\CMS\Linkvalidator\Linktype;
*/
interface LinktypeInterface
{
/**
* Returns the unique identifier of the linktype
*/
public function getIdentifier(): string;
/**
* Checks a given link for validity
*
......
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Linkvalidator\Linktype;
/**
* Registry for linktypes. The registry receives all services, tagged with "linkvalidator.linktype".
* The tagging of linktype is automatically done based on the implemented LinktypeInterface.
*
* @internal
*/
class LinktypeRegistry
{
private array $linktypes = [];
public function __construct(iterable $linktypes)
{
foreach ($linktypes as $linktype) {
if (!($linktype instanceof LinktypeInterface)) {
continue;
}
$identifier = $linktype->getIdentifier();
if ($identifier === '') {
throw new \InvalidArgumentException('Identifier for linktype ' . get_class($linktype) . ' is empty.', 1644932383);
}
if (isset($this->linktypes[$identifier])) {
throw new \InvalidArgumentException('Linktype identifier ' . $identifier . ' is already registered.', 1644932384);
}
$this->linktypes[$identifier] = $linktype;
}
}
public function getLinktype(string $identifier): ?LinktypeInterface
{
return $this->linktypes[$identifier] ?? null;
}
/**
* Get all registered linktypes
*
* @return LinktypeInterface[]
*/
public function getLinktypes(): array
{
return $this->linktypes;
}
/**
* Get the identifiers of all registered linktypes
*
* @return string[]
*/
public function getIdentifiers(): array
{
return array_keys($this->linktypes);
}
}
......@@ -29,6 +29,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MailUtility;
use TYPO3\CMS\Fluid\View\TemplatePaths;
use TYPO3\CMS\Linkvalidator\Event\ModifyValidatorTaskEmailEvent;
use TYPO3\CMS\Linkvalidator\Linktype\LinktypeRegistry;
use TYPO3\CMS\Linkvalidator\Result\LinkAnalyzerResult;
use TYPO3\CMS\Scheduler\Task\AbstractTask;
......@@ -392,9 +393,9 @@ class ValidatorTask extends AbstractTask
{
$linkTypes = [];
$typesTmp = GeneralUtility::trimExplode(',', $this->modTSconfig['linktypes'], true);
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] ?? [] as $type => $value) {
if (in_array($type, $typesTmp, true)) {
$linkTypes[] = (string)$type;
foreach (GeneralUtility::makeInstance(LinktypeRegistry::class)->getIdentifiers() as $identifier) {
if (in_array($identifier, $typesTmp, true)) {
$linkTypes[] = $identifier;
}
}
return $linkTypes;
......
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Linkvalidator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use TYPO3\CMS\Linkvalidator\Linktype\LinktypeInterface;
return static function (ContainerConfigurator $container, ContainerBuilder $containerBuilder) {
$containerBuilder->registerForAutoconfiguration(LinktypeInterface::class)->addTag('linkvalidator.linktype');
};
......@@ -36,3 +36,9 @@ services:
- name: event.listener
identifier: 'rte-check-link-to-file'
method: 'checkFileLink'
# Linktype registry
TYPO3\CMS\Linkvalidator\Linktype\LinktypeRegistry:
public: true
arguments:
- !tagged_iterator linkvalidator.linktype
<?php
declare(strict_types=1);
/*
* 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!
*/
namespace TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype;
use TYPO3\CMS\Linkvalidator\Linktype\LinktypeInterface;
use TYPO3\CMS\Linkvalidator\Linktype\LinktypeRegistry;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
class LinktypeRegistryTest extends UnitTestCase
{
/**
* @test
*/
public function registrationRequiresInterface(): void
{
$linktypes = [
new class() {
},
$this->getLinkType('valid-identifier'),
];
$linkTypeRegistry = new LinktypeRegistry($linktypes);
self::assertNotNull($linkTypeRegistry->getLinktype('valid-identifier'));
self::assertCount(1, $linkTypeRegistry->getLinktypes());
self::assertEquals(['valid-identifier'], $linkTypeRegistry->getIdentifiers());
}
/**
* @test
*/
public function registrationThrowsExceptionOnEmptyIdentifier(): void
{
$linktypes = [
$this->getLinkType(),
];
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(1644932383);
new LinktypeRegistry($linktypes);
}
/**
* @test
*/
public function registrationThrowsExceptionOnDuplicateIdentifier(): void
{
$linktypes = [
$this->getLinkType('duplicate'),
$this->getLinkType('duplicate'),
];
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(1644932384);
new LinktypeRegistry($linktypes);
}
protected function getLinkType(string $identifier = ''): LinktypeInterface
{
return new class($identifier) implements LinktypeInterface {
private string $identifier;
public function __construct(string $identifier)
{
$this->identifier = $identifier;
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function checkLink($url, $softRefEntry, $reference)
{
return true;
}
public function setAdditionalConfig(array $config): void
{
}
public function fetchType($value, $type, $key)
{
return '';
}
public function getErrorParams()
{
return [];
}
public function getBrokenUrl($row)
{
return '';
}
public function getErrorMessage($errorParams)
{
return '';
}
};
}
}
......@@ -2,9 +2,6 @@
declare(strict_types=1);
use TYPO3\CMS\Linkvalidator\Linktype\ExternalLinktype;
use TYPO3\CMS\Linkvalidator\Linktype\FileLinktype;