[BUGFIX] Install Tool: Handle PCRE errors in row updater
[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 Psr\Log\LoggerInterface;
19 use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
20 use TYPO3\CMS\Core\LinkHandling\Exception\UnknownUrnException;
21 use TYPO3\CMS\Core\LinkHandling\LinkService;
22 use TYPO3\CMS\Core\Log\LogManager;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
25
26 /**
27 * Move '<link ...' syntax to '<a href' in rte fields
28 */
29 class RteLinkSyntaxUpdater implements RowUpdaterInterface
30 {
31 /**
32 * Table list with field list that may have links them
33 *
34 * @var array
35 */
36 protected $tableFieldListToConsider = [];
37
38 /**
39 * @var array Table names that should be ignored.
40 */
41 protected $blackListedTables = [
42 'sys_log',
43 'sys_history',
44 'sys_template',
45 ];
46
47 /**
48 * Regular expressions to match the <link ...>content</link> inside
49 * @var array
50 */
51 protected $regularExpressions = [
52 'default' => '#
53 (?\'tag\'<link\\s++(?\'typolink\'[^>]+)>)
54 (?\'content\'(?:[^<]++|<(?!/link>))*+)
55 </link>
56 #xumsi',
57 'flex' => '#
58 (?\'tag\'&lt;link\\s++(?\'typolink\'(?:[^&]++|&(?!gt;))++)&gt;)
59 (?\'content\'(?:[^&]++|&(?!lt;/link&gt;))*+)
60 &lt;/link&gt;
61 #xumsi'
62 ];
63
64 /**
65 * @var LoggerInterface
66 */
67 protected $logger = null;
68
69 /**
70 * Get title
71 *
72 * @return string Title
73 */
74 public function getTitle(): string
75 {
76 return 'Scan for old "<link>" syntax in richtext and text fields and update to "<a href>"';
77 }
78
79 /**
80 * Return true if a table may have RTE fields
81 *
82 * @param string $tableName Table name to check
83 * @return bool True if this table potentially has RTE fields
84 */
85 public function hasPotentialUpdateForTable(string $tableName): bool
86 {
87 if (!is_array($GLOBALS['TCA'][$tableName])) {
88 throw new \RuntimeException(
89 'Globals TCA of ' . $tableName . ' must be an array',
90 1484173035
91 );
92 }
93 $result = false;
94 if (in_array($tableName, $this->blackListedTables, true)) {
95 return $result;
96 }
97 $tcaOfTable = $GLOBALS['TCA'][$tableName];
98 if (!is_array($tcaOfTable['columns'])) {
99 return $result;
100 }
101 foreach ($tcaOfTable['columns'] as $fieldName => $fieldConfiguration) {
102 if (isset($fieldConfiguration['config']['type'])
103 && in_array($fieldConfiguration['config']['type'], ['input', 'text', 'flex'], true)
104 ) {
105 $result = true;
106 if (!is_array($this->tableFieldListToConsider[$tableName])) {
107 $this->tableFieldListToConsider[$tableName] = [];
108 }
109 $this->tableFieldListToConsider[$tableName][] = $fieldName;
110 }
111 }
112 return $result;
113 }
114
115 /**
116 * Update "<link" tags in RTE fields
117 *
118 * @param string $tableName Table name
119 * @param array $row Given row data
120 * @return array Modified row data
121 */
122 public function updateTableRow(string $tableName, array $row): array
123 {
124 if (!is_array($this->tableFieldListToConsider)) {
125 throw new \RuntimeException(
126 'Parent should not call me with a table name I do not consider relevant for update',
127 1484173650
128 );
129 }
130 $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
131 $fieldsToScan = $this->tableFieldListToConsider[$tableName];
132 foreach ($fieldsToScan as $fieldName) {
133 $row[$fieldName] = $this->transformLinkTagsIfFound(
134 $tableName,
135 $fieldName,
136 $row,
137 $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'] === 'flex'
138 );
139 }
140 return $row;
141 }
142
143 /**
144 * Finds all <link> tags and calls the typolink codec service and the link service (twice) to get a string
145 * representation of the href part, and then builds an anchor tag.
146 *
147 * @param string $tableName
148 * @param string $fieldName
149 * @param array $row
150 * @param bool $isFlexformField If true the content is htmlspecialchar()'d and must be treated as such
151 * @return mixed the modified content
152 */
153 protected function transformLinkTagsIfFound(string $tableName, string $fieldName, array $row, bool $isFlexformField)
154 {
155 $content = $row[$fieldName];
156 if (is_string($content)
157 && !empty($content)
158 && (stripos($content, '<link') !== false || stripos($content, '&lt;link') !== false)
159 ) {
160 $result = preg_replace_callback(
161 $this->regularExpressions[$isFlexformField ? 'flex' : 'default'],
162 function ($matches) use ($isFlexformField) {
163 $typoLink = $isFlexformField ? htmlspecialchars_decode($matches['typolink']) : $matches['typolink'];
164 $typoLinkParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($typoLink);
165 $anchorTagAttributes = [
166 'target' => $typoLinkParts['target'],
167 'class' => $typoLinkParts['class'],
168 'title' => $typoLinkParts['title'],
169 ];
170
171 $link = $typoLinkParts['url'];
172 if (!empty($typoLinkParts['additionalParams'])) {
173 $link .= (strpos($link, '?') === false ? '?' : '&') . ltrim($typoLinkParts['additionalParams'], '&');
174 }
175
176 try {
177 $linkService = GeneralUtility::makeInstance(LinkService::class);
178 // Ensure the old syntax is converted to the new t3:// syntax, if necessary
179 $linkParts = $linkService->resolve($link);
180 $anchorTagAttributes['href'] = $linkService->asString($linkParts);
181 $newLink = '<a ' . GeneralUtility::implodeAttributes($anchorTagAttributes, true) . '>' .
182 ($isFlexformField ? htmlspecialchars_decode($matches['content']) : $matches['content']) .
183 '</a>';
184 if ($isFlexformField) {
185 $newLink = htmlspecialchars($newLink);
186 }
187 } catch (UnknownLinkHandlerException $e) {
188 $newLink = $matches[0];
189 } catch (UnknownUrnException $e) {
190 $newLink = $matches[0];
191 }
192
193 return $newLink;
194 },
195 $content
196 );
197 if ($result !== null) {
198 $content = $result;
199 } else {
200 $this->logger->error('Converting links failed due to PCRE error', [
201 'table' => $tableName,
202 'field' => $fieldName,
203 'uid' => $row['uid'] ?? null,
204 'errorCode' => preg_last_error()
205 ]);
206 }
207 }
208 return $content;
209 }
210 }