[BUGFIX] Wrong whitespace handling in RTE-enabled fields upgrade wizard
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / RteFileLinksUpdateWizard.php
1 <?php
2 namespace TYPO3\CMS\Install\Updates;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2013 Francois Suter <francois@typo3.org>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * This copyright notice MUST APPEAR in all copies of the script!
25 ***************************************************************/
26
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28
29 /**
30 * Upgrade wizard that rewrites all file links to FAL references.
31 *
32 * The content string and the reference index (sys_refindex) are updated accordingly.
33 */
34 class RteFileLinksUpdateWizard extends AbstractUpdate {
35
36 /**
37 * Title of the update wizard
38 * @var string
39 */
40 protected $title = 'Migrate all file links of RTE-enabled fields to FAL';
41
42 /**
43 * @var string Path the to fileadmin directory
44 */
45 protected $fileAdminDir;
46
47 /**
48 * The default storage
49 * @var \TYPO3\CMS\Core\Resource\ResourceStorage
50 */
51 protected $storage;
52
53 /**
54 * @var \TYPO3\CMS\Core\Html\RteHtmlParser
55 */
56 protected $rteHtmlParser;
57
58 /**
59 * Count of converted links
60 * @var integer
61 */
62 protected $convertedLinkCounter = 0;
63
64 /**
65 * Is DBAL installed or not (if not, we can use transactions)
66 * @var boolean
67 */
68 protected $isDbalInstalled = FALSE;
69
70 /**
71 * Array to store file conversion errors
72 * @var array
73 */
74 protected $errors = array();
75
76 /**
77 * List of update queries
78 * @var array
79 */
80 protected $queries = array();
81
82 /**
83 * Initialize some objects
84 *
85 * @return void
86 */
87 public function init() {
88 $this->rteHtmlParser = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Html\\RteHtmlParser');
89 /** @var $storageRepository \TYPO3\CMS\Core\Resource\StorageRepository */
90 $storageRepository = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Resource\\StorageRepository');
91 $storages = $storageRepository->findAll();
92 $this->storage = $storages[0];
93 $this->fileAdminDir = $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'];
94 // Check if DBAL is installed or not
95 $this->isDbalInstalled = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('dbal');
96 }
97
98 /**
99 * Checks if an update is needed
100 *
101 * @param string $description The description for the update
102 * @return boolean TRUE if an update is needed, FALSE otherwise
103 */
104 public function checkForUpdate(&$description) {
105 $description = 'This update wizard goes through all file links in all rich-text fields and changes them to FAL references.';
106 $description .= 'If the process times out, please run it again.';
107 // Issue warning about sys_refindex needing to be up to date
108 /** @var \TYPO3\CMS\Core\Messaging\FlashMessage $message */
109 $message = GeneralUtility::makeInstance(
110 'TYPO3\\CMS\\Core\\Messaging\\FlashMessage',
111 'This script bases itself on the references contained in the general reference index (sys_refindex). It is strongly advised to update it before running this wizard.',
112 'Updating the reference index',
113 \TYPO3\CMS\Core\Messaging\FlashMessage::WARNING
114 );
115 $description .= $message->render();
116
117 // Confirm activation only if some old-style links are found
118 $oldRecords = $this->findOldLinks();
119 if (count($oldRecords) > 0) {
120 $description .= '<br />There are currently <strong>' . count($oldRecords) . '</strong> links to update.<br />';
121 return TRUE;
122 }
123
124 // No update needed, disable the wizard
125 return FALSE;
126 }
127
128 /**
129 * Performs the database update.
130 *
131 * @param array $dbQueries queries done in this update
132 * @param mixed $customMessages custom messages
133 * @return boolean TRUE on success, FALSE on error
134 */
135 public function performUpdate(array &$dbQueries, &$customMessages) {
136 $this->init();
137
138 // Make sure we have a storage
139 if (!$this->storage) {
140 $customMessages = 'No file resource storage found';
141 return FALSE;
142 }
143
144 // Get the references and migrate them
145 $records = $this->findOldLinks();
146 foreach ($records as $singleRecord) {
147 $this->migrateRecord($singleRecord);
148 }
149 $dbQueries = $this->queries;
150
151 if (count($this->errors) > 0) {
152 foreach ($this->errors as $errorMessage) {
153 $message = GeneralUtility::makeInstance(
154 'TYPO3\\CMS\\Core\\Messaging\\FlashMessage',
155 $errorMessage,
156 '',
157 \TYPO3\CMS\Core\Messaging\FlashMessage::WARNING
158 );
159 /** @var \TYPO3\CMS\Core\Messaging\FlashMessage $message */
160 $customMessages .= '<br />' . $message->render();
161 }
162 if ($this->convertedLinkCounter == 0) {
163 // no links converted only missing files: UPDATE was not successful
164 return FALSE;
165 }
166 }
167
168 if ($this->convertedLinkCounter > 0) {
169 $customMessages = $this->convertedLinkCounter . ' links converted.<br />' . $customMessages;
170 } else {
171 $customMessages .= 'No file links found';
172 }
173 return TRUE;
174 }
175
176 /**
177 * Processes each record and updates the database
178 *
179 * @param array $reference Reference to a record from sys_refindex
180 * @return void
181 */
182 protected function migrateRecord(array $reference) {
183 // Get the current record based on the sys_refindex information
184 $record = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
185 'uid, ' . $reference['field'],
186 $reference['tablename'],
187 'uid = ' . $reference['recuid']
188 );
189 if ($record !== NULL) {
190 $this->convertFileLinks($reference, $record);
191 } else {
192 // Original record could not be found (happens if sys_refindex is not up to date), issue error
193 $this->errors[] = 'Original record not found for reference to element ' . $reference['recuid'] . ' of table ' . $reference['tablename'] . ' in field ' . $reference['field'] . '. Not migrated.';
194 }
195 }
196
197 /**
198 * The actual transformation of the links
199 * pretty similar to TS_links_rte in RteHtmlParser
200 *
201 * @param array $reference sys_refindex information
202 * @param array $record Original record pointed to by the sys_refindex reference
203 * @return void
204 */
205 protected function convertFileLinks(array $reference, array $record) {
206 // First of all, try to get the referenced file. Continue only if found.
207 try {
208 $fileObject = $this->fetchReferencedFile($reference['ref_string'], $reference);
209 } catch (\InvalidArgumentException $exception) {
210 $fileObject = NULL;
211 $this->errors[] = $reference['ref_string'] . ' could not be replaced. File does not exist.';
212 }
213 if ($fileObject instanceof \TYPO3\CMS\Core\Resource\AbstractFile) {
214 // Next, match the reference path in the content to be sure it's present inside a <link> tag
215 $content = $record[$reference['field']];
216 $regularExpression = '$<(link ' . str_replace(' ', '%20', $reference['ref_string']) . ').*>$';
217 $matches = array();
218 $result = preg_match($regularExpression, $content, $matches);
219 if ($result) {
220 // Replace the file path with the file reference
221 $modifiedContent = str_replace(
222 $matches[1],
223 'link file:' . $fileObject->getUid(),
224 $record[$reference['field']]
225 );
226 // Save the changes and stop looping
227 $this->saveChanges($modifiedContent, $reference, $fileObject);
228 $this->convertedLinkCounter++;
229 } else {
230 $this->errors[] = $reference['ref_string'] . ' not found in referenced element (uid: ' . $reference['recuid'] . ' of table ' . $reference['tablename'] . ' in field ' . $reference['field'] . '). Reference index was probably out of date.';
231 }
232 }
233 }
234
235 /**
236 * Tries to fetch the file object corresponding to the given path.
237 *
238 * @param string $path Path to a file (starting with "fileadmin/")
239 * @param array $reference Corresponding sys_refindex entry
240 * @return null|\TYPO3\CMS\Core\Resource\FileInterface
241 */
242 protected function fetchReferencedFile($path, array $reference) {
243 $fileObject = NULL;
244 if (@file_exists(PATH_site . '/' . $path)) {
245 try {
246 $fileObject = $this->storage->getFile(
247 '/' . str_replace(
248 $this->fileAdminDir,
249 '',
250 $path
251 )
252 );
253 } catch (\TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException $notFoundException) {
254 // This should really not happen, since we are testing existence of the file just before
255 $this->errors[] = $path . ' not found (referenced in element ' . $reference['recuid'] . ' of table ' . $reference['tablename'] . ' in field ' . $reference['field'] . ')';
256 }
257 } else {
258 // Nothing to be done if file not found
259 $this->errors[] = $path . ' not found (referenced in element ' . $reference['recuid'] . ' of table ' . $reference['tablename'] . ' in field ' . $reference['field'] . ')';
260 }
261 return $fileObject;
262 }
263
264 /**
265 * Saves the modified content to the database and updates the sys_refindex accordingly.
266 *
267 * @param string $modifiedText Original content with the file links replaced
268 * @param array $reference sys_refindex record
269 * @param \TYPO3\CMS\Core\Resource\AbstractFile $file
270 * @return void
271 */
272 protected function saveChanges($modifiedText, array $reference, $file) {
273
274 // If DBAL is not installed, we can start a transaction before saving
275 // This ensures that a possible time out doesn't break the database integrity
276 // by occurring between the two needed DB writes.
277 if (!$this->isDbalInstalled) {
278 $GLOBALS['TYPO3_DB']->sql_query('START TRANSACTION');
279 }
280
281 // Save the changed field
282 $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
283 $reference['tablename'],
284 'uid = ' . $reference['recuid'],
285 array(
286 $reference['field'] => $modifiedText
287 )
288 );
289 $this->queries[] = htmlspecialchars(str_replace(LF, ' ', $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery));
290
291 // Finally, update the sys_refindex table as well
292 $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
293 'sys_refindex',
294 'hash = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($reference['hash'], 'sys_refindex'),
295 array(
296 'ref_table' => 'sys_file',
297 'ref_uid' => $file->getUid(),
298 'ref_string' => ''
299 )
300 );
301 $this->queries[] = str_replace(LF, ' ', $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery);
302
303 // Confirm the transaction
304 if (!$this->isDbalInstalled) {
305 $GLOBALS['TYPO3_DB']->sql_query('COMMIT');
306 }
307 }
308
309 /**
310 * Use sys_refindex to find all links to "old" files in typolink tags.
311 *
312 * This will find any RTE-enabled field.
313 *
314 * @return array Entries from sys_refindex
315 */
316 protected function findOldLinks() {
317 $records = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
318 'hash, tablename, recuid, field, ref_table, ref_uid, ref_string',
319 'sys_refindex',
320 'softref_key = \'typolink_tag\' AND ref_table = \'_FILE\' '
321 );
322 return $records;
323 }
324
325 }