[TASK] Add update wizard to migrate <link> tags to <a> tags
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / RowUpdater / RteLinkSyntaxUpdater.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Install\Updates\RowUpdater;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
19 use TYPO3\CMS\Core\LinkHandling\Exception\UnknownUrnException;
20 use TYPO3\CMS\Core\LinkHandling\LinkService;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
23
24 /**
25 * Move '<link ...' syntax to '<a href' in rte fields
26 */
27 class RteLinkSyntaxUpdater implements RowUpdaterInterface
28 {
29 /**
30 * Table list with field list that may have links them
31 *
32 * @var array
33 */
34 protected $tableFieldListToConsider = [];
35
36 /**
37 * @var array Table names that should be ignored.
38 */
39 protected $blackListedTables = [
40 'sys_log',
41 'sys_history',
42 ];
43
44 /**
45 * Regular expressions to match the <link ...>content</link> inside
46 * @var array
47 */
48 protected $regularExpressions = [
49 'default' => '#(?\'tag\'<link\\s+(?\'typolink\'[^>]+)>)(?\'content\'(?:(?!</link>).)*)</link>#msi',
50 'flex' => '#(?\'tag\'&lt;link\\s+(?\'typolink\'.+?)&gt;)(?\'content\'(?:(?!&lt;/link&gt;).)*)&lt;/link&gt;#msi'
51 ];
52
53 /**
54 * Get title
55 *
56 * @return string Title
57 */
58 public function getTitle(): string
59 {
60 return 'Scan for old "<link>" syntax in richtext and text fields and update to "<a href>"';
61 }
62
63 /**
64 * Return true if a table may have RTE fields
65 *
66 * @param string $tableName Table name to check
67 * @return bool True if this table potentially has RTE fields
68 */
69 public function hasPotentialUpdateForTable(string $tableName): bool
70 {
71 if (!is_array($GLOBALS['TCA'][$tableName])) {
72 throw new \RuntimeException(
73 'Globals TCA of ' . $tableName . ' must be an array',
74 1484173035
75 );
76 }
77 $result = false;
78 if (in_array($tableName, $this->blackListedTables, true)) {
79 return $result;
80 }
81 $tcaOfTable = $GLOBALS['TCA'][$tableName];
82 if (!is_array($tcaOfTable['columns'])) {
83 return $result;
84 }
85 foreach ($tcaOfTable['columns'] as $fieldName => $fieldConfiguration) {
86 if (isset($fieldConfiguration['config']['type'])
87 && in_array($fieldConfiguration['config']['type'], ['input', 'text', 'flex'], true)
88 ) {
89 $result = true;
90 if (!is_array($this->tableFieldListToConsider[$tableName])) {
91 $this->tableFieldListToConsider[$tableName] = [];
92 }
93 $this->tableFieldListToConsider[$tableName][] = $fieldName;
94 }
95 }
96 return $result;
97 }
98
99 /**
100 * Update "<link" tags in RTE fields
101 *
102 * @param string $tableName Table name
103 * @param array $row Given row data
104 * @return array Modified row data
105 */
106 public function updateTableRow(string $tableName, array $row): array
107 {
108 if (!is_array($this->tableFieldListToConsider)) {
109 throw new \RuntimeException(
110 'Parent should not call me with a table name I do not consider relevant for update',
111 1484173650
112 );
113 }
114 $fieldsToScan = $this->tableFieldListToConsider[$tableName];
115 foreach ($fieldsToScan as $fieldName) {
116 $row[$fieldName] = $this->transformLinkTagsIfFound(
117 $row[$fieldName],
118 $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'] === 'flex'
119 );
120 }
121 return $row;
122 }
123
124 /**
125 * Finds all <link> tags and calls the typolink codec service and the link service (twice) to get a string
126 * representation of the href part, and then builds an anchor tag.
127 *
128 * @param mixed $content The content to process
129 * @param bool $isFlexformField If true the content is htmlspecialchar()'d and must be treated as such
130 * @return mixed the modified content
131 */
132 protected function transformLinkTagsIfFound($content, $isFlexformField)
133 {
134 if (is_string($content)
135 && !empty($content)
136 && (stripos($content, '<link') !== false || stripos($content, '&lt;link') !== false)
137 ) {
138 $content = preg_replace_callback(
139 $this->regularExpressions[$isFlexformField ? 'flex' : 'default'],
140 function ($matches) use ($isFlexformField) {
141 $typoLink = $isFlexformField ? htmlspecialchars_decode($matches['typolink']) : $matches['typolink'];
142 $typoLinkParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($typoLink);
143 $anchorTagAttributes = [
144 'target' => $typoLinkParts['target'],
145 'class' => $typoLinkParts['class'],
146 'title' => $typoLinkParts['title'],
147 ];
148
149 $link = $typoLinkParts['url'];
150 if (!empty($typoLinkParts['additionalParams'])) {
151 $link .= (strpos($link, '?') === false ? '?' : '&') . ltrim($typoLinkParts['additionalParams'], '&');
152 }
153
154 try {
155 $linkService = GeneralUtility::makeInstance(LinkService::class);
156 // Ensure the old syntax is converted to the new t3:// syntax, if necessary
157 $linkParts = $linkService->resolve($link);
158 $anchorTagAttributes['href'] = $linkService->asString($linkParts);
159 $newLink = '<a ' . GeneralUtility::implodeAttributes($anchorTagAttributes, true) . '>' .
160 ($isFlexformField ? htmlspecialchars_decode($matches['content']) : $matches['content']) .
161 '</a>';
162 if ($isFlexformField) {
163 $newLink = htmlspecialchars($newLink);
164 }
165 } catch (UnknownLinkHandlerException $e) {
166 $newLink = $matches[0];
167 } catch (UnknownUrnException $e) {
168 $newLink = $matches[0];
169 }
170
171 return $newLink;
172 },
173 $content
174 );
175 }
176 return $content;
177 }
178 }