[BUGFIX] Could not parse where clause
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / TtContentUploadsUpdateWizard.php
1 <?php
2 namespace TYPO3\CMS\Install\Updates;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2011-2013 Steffen Ritter <steffen.ritter@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 /**
28 * Upgrade wizard which goes through all files referenced in the tt_content.media filed
29 * and creates sys_file records as well as sys_file_reference records for the individual usages.
30 *
31 * @author Steffen Ritter <steffen.ritter@typo3.org>
32 */
33 class TtContentUploadsUpdateWizard extends AbstractUpdate {
34
35 const FOLDER_ContentUploads = '_migrated/content_uploads';
36
37 /**
38 * @var string
39 */
40 protected $title = 'Migrate file relations of tt_content "uploads"';
41
42 /**
43 * @var string
44 */
45 protected $targetDirectory;
46
47 /**
48 * @var \TYPO3\CMS\Core\Resource\ResourceFactory
49 */
50 protected $fileFactory;
51
52 /**
53 * @var \TYPO3\CMS\Core\Resource\Index\FileIndexRepository
54 */
55 protected $fileIndexRepository;
56
57 /**
58 * @var \TYPO3\CMS\Core\Resource\ResourceStorage
59 */
60 protected $storage;
61
62 /**
63 * Initialize all required repository and factory objects.
64 *
65 * @throws \RuntimeException
66 */
67 protected function init() {
68 $fileadminDirectory = rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/') . '/';
69 /** @var $storageRepository \TYPO3\CMS\Core\Resource\StorageRepository */
70 $storageRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Resource\\StorageRepository');
71 $storages = $storageRepository->findAll();
72 foreach ($storages as $storage) {
73 $storageRecord = $storage->getStorageRecord();
74 $configuration = $storage->getConfiguration();
75 $isLocalDriver = $storageRecord['driver'] === 'Local';
76 $isOnFileadmin = !empty($configuration['basePath']) && \TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($configuration['basePath'], $fileadminDirectory);
77 if ($isLocalDriver && $isOnFileadmin) {
78 $this->storage = $storage;
79 break;
80 }
81 }
82 if (!isset($this->storage)) {
83 throw new \RuntimeException('Local default storage could not be initialized - might be due to missing sys_file* tables.');
84 }
85 $this->fileFactory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Resource\\ResourceFactory');
86 $this->fileIndexRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Resource\\Index\\FileIndexRepository');
87 $this->targetDirectory = PATH_site . $fileadminDirectory . self::FOLDER_ContentUploads . '/';
88 }
89
90 /**
91 * Checks if an update is needed
92 *
93 * @param string &$description The description for the update
94 * @return boolean TRUE if an update is needed, FALSE otherwise
95 */
96 public function checkForUpdate(&$description) {
97 $updateNeeded = FALSE;
98 // Fetch records where the field media does not contain a plain integer value
99 // * check whether media field is not empty
100 // * then check whether media field does not contain a reference count (= not integer)
101 $mapping = $this->getTableColumnMapping();
102 $sql = $GLOBALS['TYPO3_DB']->SELECTquery(
103 'COUNT(' . $mapping['mapFieldNames']['uid'] . ')',
104 $mapping['mapTableName'],
105 '1=1'
106 );
107 $whereClause = $this->getDbalCompliantUpdateWhereClause();
108 $sql = str_replace('WHERE 1=1', $whereClause, $sql);
109 $resultSet = $GLOBALS['TYPO3_DB']->sql_query($sql);
110 $notMigratedRowsCount = 0;
111 if ($resultSet !== FALSE) {
112 list($notMigratedRowsCount) = $GLOBALS['TYPO3_DB']->sql_fetch_row($resultSet);
113 $notMigratedRowsCount = (int)$notMigratedRowsCount;
114 $GLOBALS['TYPO3_DB']->sql_free_result($resultSet);
115 }
116 if ($notMigratedRowsCount > 0) {
117 $description = 'There are Content Elements of type "upload" which are referencing files that are not using ' . ' the File Abstraction Layer. This wizard will move the files to fileadmin/' . self::FOLDER_ContentUploads . ' and index them.';
118 $updateNeeded = TRUE;
119 }
120 return $updateNeeded;
121 }
122
123 /**
124 * Performs the database update.
125 *
126 * @param array &$dbQueries Queries done in this update
127 * @param mixed &$customMessages Custom messages
128 * @return boolean TRUE on success, FALSE on error
129 */
130 public function performUpdate(array &$dbQueries, &$customMessages) {
131 $this->init();
132 $records = $this->getRecordsFromTable('tt_content');
133 $this->checkPrerequisites();
134 foreach ($records as $singleRecord) {
135 $this->migrateRecord($singleRecord);
136 }
137 return TRUE;
138 }
139
140 /**
141 * Ensures a new folder "fileadmin/content_upload/" is available.
142 *
143 * @return void
144 */
145 protected function checkPrerequisites() {
146 if (!$this->storage->hasFolder(self::FOLDER_ContentUploads)) {
147 $this->storage->createFolder(self::FOLDER_ContentUploads, $this->storage->getRootLevelFolder());
148 }
149 }
150
151 /**
152 * Processes the actual transformation from CSV to sys_file_references
153 *
154 * @param array $record
155 * @return void
156 */
157 protected function migrateRecord(array $record) {
158 $collections = array();
159 if (trim($record['select_key'])) {
160 $GLOBALS['TYPO3_DB']->exec_INSERTquery('sys_file_collection', array(
161 'pid' => $record['pid'],
162 'title' => $record['select_key'],
163 'storage' => $this->storage->getUid(),
164 'folder' => ltrim('fileadmin/', $record['select_key'])
165 ));
166 $collections[] = $GLOBALS['TYPO3_DB']->sql_insert_id();
167 }
168 $files = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $record['media'], TRUE);
169 $descriptions = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode('
170 ', $record['imagecaption']);
171 $titleText = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode('
172 ', $record['titleText']);
173 $i = 0;
174 foreach ($files as $file) {
175 if (file_exists(PATH_site . 'uploads/media/' . $file)) {
176 \TYPO3\CMS\Core\Utility\GeneralUtility::upload_copy_move(PATH_site . 'uploads/media/' . $file, $this->targetDirectory . $file);
177 $fileObject = $this->storage->getFile(self::FOLDER_ContentUploads . '/' . $file);
178 $this->fileIndexRepository->add($fileObject);
179 $dataArray = array(
180 'uid_local' => $fileObject->getUid(),
181 'tablenames' => 'tt_content',
182 'uid_foreign' => $record['uid'],
183 // the sys_file_reference record should always placed on the same page
184 // as the record to link to, see issue #46497
185 'pid' => $record['pid'],
186 'fieldname' => 'media',
187 'sorting_foreign' => $i
188 );
189 if (isset($descriptions[$i])) {
190 $dataArray['description'] = $descriptions[$i];
191 }
192 if (isset($titleText[$i])) {
193 $dataArray['alternative'] = $titleText[$i];
194 }
195 $GLOBALS['TYPO3_DB']->exec_INSERTquery('sys_file_reference', $dataArray);
196 unlink(PATH_site . 'uploads/media/' . $file);
197 }
198 $i++;
199 }
200 $this->cleanRecord($record, $i, $collections);
201 }
202
203 /**
204 * Removes the old fields from the database-record
205 *
206 * @param array $record
207 * @param integer $fileCount
208 * @param array $collectionUids
209 * @return void
210 */
211 protected function cleanRecord(array $record, $fileCount, array $collectionUids) {
212 $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tt_content', 'uid = ' . $record['uid'], array(
213 'media' => $fileCount,
214 'imagecaption' => '',
215 'titleText' => '',
216 'altText' => '',
217 'select_key' => '',
218 'file_collections' => implode(',', $collectionUids)
219 ));
220 }
221
222 /**
223 * Retrieve every record which needs to be processed
224 *
225 * @return array
226 */
227 protected function getRecordsFromTable() {
228 $mapping = $this->getTableColumnMapping();
229 $reverseFieldMapping = array_flip($mapping['mapFieldNames']);
230
231 $fields = array();
232 foreach (array('uid', 'pid', 'select_key', 'media', 'imagecaption', 'titleText') as $columnName) {
233 $fields[] = $mapping['mapFieldNames'][$columnName];
234 }
235 $fields = implode(',', $fields);
236
237 $sql = $GLOBALS['TYPO3_DB']->SELECTquery(
238 $fields,
239 $mapping['mapTableName'],
240 '1=1'
241 );
242 $whereClause = $this->getDbalCompliantUpdateWhereClause();
243 $sql = str_replace('WHERE 1=1', $whereClause, $sql);
244 $resultSet = $GLOBALS['TYPO3_DB']->sql_query($sql);
245 $records = array();
246 if (!$GLOBALS['TYPO3_DB']->sql_error()) {
247 while (($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($resultSet)) !== FALSE) {
248 // Mapping back column names to native TYPO3 names
249 $record = array();
250 foreach ($reverseFieldMapping as $columnName => $finalColumnName) {
251 $record[$finalColumnName] = $row[$columnName];
252 }
253 $records[] = $record;
254 }
255 $GLOBALS['TYPO3_DB']->sql_free_result($resultSet);
256 }
257 return $records;
258 }
259
260 /**
261 * Returns a DBAL-compliant where clause to be used for the update where clause.
262 * We have DBAL-related code here because the SQL parser is not able to properly
263 * parse this complex condition but we know that it is compatible with the DBMS
264 * we support in TYPO3 Core.
265 *
266 * @return string
267 */
268 protected function getDbalCompliantUpdateWhereClause() {
269 $mapping = $this->getTableColumnMapping();
270 $this->quoteIdentifiers($mapping);
271
272 $where = sprintf(
273 'WHERE %s <> \'\' AND CAST(CAST(%s AS DECIMAL) AS CHAR) <> %s OR (%s = \'uploads\' AND %s != \'\')',
274 $mapping['mapFieldNames']['media'],
275 $mapping['mapFieldNames']['media'],
276 $mapping['mapFieldNames']['media'],
277 $mapping['mapFieldNames']['CType'],
278 $mapping['mapFieldNames']['select_key']
279 );
280
281 return $where;
282 }
283
284 /**
285 * Returns the table and column mapping.
286 *
287 * @return array
288 */
289 protected function getTableColumnMapping() {
290 $mapping = array(
291 'mapTableName' => 'tt_content',
292 'mapFieldNames' => array(
293 'uid' => 'uid',
294 'pid' => 'pid',
295 'media' => 'media',
296 'imagecaption' => 'imagecaption',
297 'titleText' => 'titleText',
298 'CType' => 'CType',
299 'select_key' => 'select_key',
300 )
301 );
302
303 if ($GLOBALS['TYPO3_DB'] instanceof \TYPO3\CMS\Dbal\Database\DatabaseConnection) {
304 if (!empty($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['dbal']['mapping']['tt_content'])) {
305 $mapping = array_merge_recursive($mapping, $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['dbal']['mapping']['tt_content']);
306 }
307 }
308
309 return $mapping;
310 }
311
312 /**
313 * Quotes identifiers for DBAL-compliant query.
314 *
315 * @param array &$mapping
316 * @return void
317 */
318 protected function quoteIdentifiers(array &$mapping) {
319 if ($GLOBALS['TYPO3_DB'] instanceof \TYPO3\CMS\Dbal\Database\DatabaseConnection) {
320 if (!$GLOBALS['TYPO3_DB']->runningNative() && !$GLOBALS['TYPO3_DB']->runningADOdbDriver('mysql')) {
321 $mapping['mapTableName'] = '"' . $mapping['mapTableName'] . '"';
322 foreach ($mapping['mapFieldNames'] as $key => &$value) {
323 $value = '"' . $value . '"';
324 }
325 }
326 }
327 }
328
329 }