Added overlay handling for foreign table translations
[TYPO3CMS/Extensions/overlays.git] / class.tx_overlays.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2008 Francois Suter (Cobweb) <typo3@cobweb.ch>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 *
24 * $Id$
25 ***************************************************************/
26 /**
27 * [CLASS/FUNCTION INDEX of SCRIPT]
28 *
29 * Hint: use extdeveval to insert/update function index above.
30 */
31
32
33 /**
34 * Main library of the 'overlays' extension.
35 * It aims to improve on the performance of the original overlaying mechanism provided by t3lib_page
36 * and to provide a more useful API for developers
37 *
38 * @author Francois Suter (Cobweb) <typo3@cobweb.ch>
39 * @package TYPO3
40 * @subpackage tx_overlays
41 */
42 class tx_overlays {
43
44 /**
45 * This method is designed to get all the records from a given table, properly overlaid with versions and translations
46 * Its parameters are the same as t3lib_db::exec_SELECTquery()
47 * A small difference is that it will take only a single table
48 * The big difference is that it returns an array of properly overlaid records and not a result pointer
49 *
50 * @param string $selectFields: List of fields to select from the table. This is what comes right after "SELECT ...". Required value.
51 * @param string $fromTable: Table from which to select. This is what comes right after "FROM ...". Required value.
52 * @param string $whereClause: Optional additional WHERE clauses put in the end of the query. NOTICE: You must escape values in this argument with $this->fullQuoteStr() yourself! DO NOT PUT IN GROUP BY, ORDER BY or LIMIT!
53 * @param string $groupBy: Optional GROUP BY field(s), if none, supply blank string.
54 * @param string $orderBy: Optional ORDER BY field(s), if none, supply blank string.
55 * @param string $limit: Optional LIMIT value ([begin,]max), if none, supply blank string.
56 * @return array Fully overlaid recordset
57 */
58 public function getAllRecordsForTable($selectFields, $fromTable, $whereClause = '', $groupBy = '', $orderBy = '', $limit = '') {
59 // SQL WHERE clause is the base clause passed to the function, plus language condition, plus enable fields condition
60 $where = $whereClause;
61 $condition = self::getLanguageCondition($fromTable);
62 if (!empty($condition)) {
63 if (!empty($where)) $where .= ' AND ';
64 $where .= $condition;
65 }
66 $condition = self::getEnableFieldsCondition($fromTable);
67 if (!empty($condition)) {
68 if (!empty($where)) $where .= ' AND ';
69 $where .= $condition;
70 }
71
72 // If the language is not default, prepare for overlays
73 if ($GLOBALS['TSFE']->sys_language_content > 0) {
74 // Make sure the list of selected fields includes "uid", "pid" and language fields so that language overlays can be gotten properly
75 // If these do not exist in the queried table, the recordset is returned as is, without overlay
76 try {
77 $selectFields = self::selectOverlayFields($fromTable, $selectFields);
78 $doOverlays = true;
79 }
80 catch (Exception $e) {
81 $doOverlays = false;
82 }
83 }
84 else {
85 $doOverlays = false;
86 }
87
88 // Execute the query itself
89 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($selectFields, $fromTable, $where, $groupBy, $orderBy, $limit);
90 // Assemble a raw recordset
91 $records = array();
92 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
93 $records[] = $row;
94 }
95
96 // If we have both a uid and a pid field, we can proceed with overlaying the records
97 if ($doOverlays) {
98 $records = self::overlayRecordSet($fromTable, $records, $GLOBALS['TSFE']->sys_language_content, $GLOBALS['TSFE']->sys_language_contentOL);
99 }
100 return $records;
101 }
102
103 /**
104 * This method gets the SQL condition to apply for fetching the proper language
105 * depending on the localization settings in the TCA
106 *
107 * @param string $table: name of the table to assemble the condition for
108 * @return string SQL to add to the WHERE clause (without "AND")
109 */
110 public function getLanguageCondition($table) {
111 $languageCondition = '';
112
113 // First check if there's actually a TCA for the given table
114 if (isset($GLOBALS['TCA'][$table]['ctrl'])) {
115 $tableCtrlTCA = $GLOBALS['TCA'][$table]['ctrl'];
116
117 // Assemble language condition only if a language field is defined
118 if (!empty($tableCtrlTCA['languageField'])) {
119 if (isset($GLOBALS['TSFE']->sys_language_contentOL) && isset($tableCtrlTCA['transOrigPointerField'])) {
120 $languageCondition = $table.'.'.$tableCtrlTCA['languageField'].' IN (0,-1)'; // Default language and "all" language
121
122 // If current language is not default, select elements that exist only for current language
123 // That means elements that exist for current language but have no parent element
124 if ($GLOBALS['TSFE']->sys_language_content > 0) {
125 $languageCondition .= ' OR ('.$table.'.'.$tableCtrlTCA['languageField']." = '".$GLOBALS['TSFE']->sys_language_content."' AND ".$table.'.'.$tableCtrlTCA['transOrigPointerField']." = '0')";
126 }
127 }
128 else {
129 $languageCondition = $table.'.'.$tableCtrlTCA['languageField']." = '".$GLOBALS['TSFE']->sys_language_content."'";
130 }
131 }
132 }
133 // TODO: throw an exception if table has no language mechanism?
134 return $languageCondition;
135 }
136
137 /**
138 * This method returns the condition on enable fields for the given table
139 * Basically it calls on the method provided by t3lib_page, but without the " AND " in front
140 *
141 * @param string $table: name of the table to build the condition for
142 * @param boolean $showHidden: If set, then you want NOT to filter out hidden records. Otherwise hidden record are filtered based on the current preview settings.
143 * @return string SQL to add to the WHERE clause (without "AND")
144 */
145 public function getEnableFieldsCondition($table, $showHidden = 0) {
146 $enableCondition = '';
147 // First check if table has a TCA ctrl section, otherwise t3lib_page::enableFields() will die() (stupid thing!)
148 if (isset($GLOBALS['TCA'][$table]['ctrl'])) {
149 $enableCondition = $GLOBALS['TSFE']->sys_page->enableFields($table, $show_hidden ? $show_hidden : ($table == 'pages' ? $GLOBALS['TSFE']->showHiddenPage : $GLOBALS['TSFE']->showHiddenRecords));
150 // If an enable clause was returned, strip the first ' AND '
151 if (!empty($enableCondition)) {
152 $enableCondition = substr($enableCondition, strlen(' AND '));
153 }
154 }
155 // TODO: throw an exception if the given table has no TCA? (t3lib_page::enableFields() used a die)
156 return $enableCondition;
157 }
158
159 /**
160 * This method makes sure that all the fields necessary for proper overlaying are included
161 * in the list of selected fields and exist in the table being queried
162 * If not, it throws an exception
163 *
164 * @param string $table: Table from which to select. This is what comes right after "FROM ...". Required value.
165 * @param string $selectFields: List of fields to select from the table. This is what comes right after "SELECT ...". Required value.
166 * @return string Possibly modified list of fields to select
167 */
168 public function selectOverlayFields($table, $selectFields) {
169 $select = $selectFields;
170
171 // Check if the table indeed has a TCA
172 if (isset($GLOBALS['TCA'][$table]['ctrl'])) {
173
174 // If the table uses a foreign table for translations, there are no fields to add
175 // Return original select fields directly
176 if ($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']) {
177 return $selectFields;
178 }
179 else {
180 $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
181
182 // In order to be properly overlaid, a table has to have a uid, a pid and languageField
183 $hasUidField = strpos($selectFields, 'uid');
184 $hasPidField = strpos($selectFields, 'pid');
185 $hasLanguageField = strpos($selectFields, $languageField);
186 if ($hasUidField === false || $hasPidField === false || $hasLanguageField === false) {
187 $availableFields = $GLOBALS['TYPO3_DB']->admin_get_fields($table);
188 if (isset($availableFields['uid'])) {
189 if ($selectFields != '*') $select .= ', '.$table.'.uid';
190 $hasUidField = true;
191 }
192 if (isset($availableFields['pid'])) {
193 if ($selectFields != '*') $select .= ', '.$table.'.pid';
194 $hasPidField = true;
195 }
196 if (isset($availableFields[$languageField])) {
197 if ($selectFields != '*') $select .= ', '.$table.'.'.$languageField;
198 $hasLanguageField = true;
199 }
200 }
201 // If one of the fields is still missing after that, throw an exception
202 if ($hasUidField === false || $hasPidField === false || $hasLanguageField === false) {
203 throw new Exception('Not all overlay fields available.');
204 }
205 // Else return the modified list of fields to select
206 else {
207 return $select;
208 }
209 }
210 }
211 // The table has no TCA, throw an exception
212 else {
213 throw new Exception('No TCA for table, cannot add overlay fields.');
214 }
215 }
216
217 /**
218 * Creates language-overlay for records in general (where translation is found in records from the same table)
219 * This is originally copied from t3lib_page::getRecordOverlay()
220 *
221 * @param string $table: Table name
222 * @param array $recordset: Full recordset to overlay. Must containt uid, pid and $table]['ctrl']['languageField']
223 * @param integer $sys_language_contentPointer to the sys_language uid for content on the site.
224 * @param string $OLmodeOverlay mode. If "hideNonTranslated" then records without translation will not be returned un-translated but unset (and return value is false)
225 * @return array Returns the full overlaid recordset. If $OLmode is "hideNonTranslated" then some records may be missing if no translation was found.
226 */
227 public function overlayRecordSet($table, $recordset, $sys_language_content, $OLmode = '') {
228
229 // Test with the first row if uid and pid fields are present
230 if (!empty($recordset[0]['uid']) && !empty($recordset[0]['pid'])) {
231
232 // Test if the table has a TCA definition
233 if (isset($GLOBALS['TCA'][$table])) {
234 $tableCtrl = $GLOBALS['TCA'][$table]['ctrl'];
235
236 // Test if the TCA definition includes translation information for the same table
237 if (isset($tableCtrl['languageField']) && isset($tableCtrl['transOrigPointerField'])) {
238
239 // Test with the first row if languageField is present
240 if (isset($recordset[0][$tableCtrl['languageField']])) {
241 // Filter out records that are not in the default or [ALL] language, should there be any
242 $filteredRecordset = array();
243 foreach ($recordset as $row) {
244 if ($row[$tableCtrl['languageField']] <= 0) {
245 $filteredRecordset[] = $row;
246 }
247 }
248
249 // Will try to overlay a record only if the sys_language_content value is larger than zero,
250 // that is, it is not default or [ALL] language
251 if ($sys_language_content > 0) {
252 // Assemble a list of uid's for getting the overlays,
253 // but only from the filtered recordset
254 $uidList = array();
255 foreach ($filteredRecordset as $row) {
256 $uidList[] = $row['uid'];
257 }
258
259 // Get all overlay records
260 $overlays = self::getLocalOverlayRecords($table, $uidList, $sys_language_content);
261
262 // Now loop on the filtered recordset and try to overlay each record
263 $overlaidRecordset = array();
264 foreach ($filteredRecordset as $row) {
265 // An overlay exists, apply it
266 if (isset($overlays[$row['uid']][$row['pid']])) {
267 $overlaidRecordset[] = self::overlaySingleRecord($table, $row, $overlays[$row['uid']][$row['pid']]);
268 }
269 // No overlay exists
270 else {
271 // Take original record, only if non-translated are not hidden, or if language is [All]
272 if ($OLmode != 'hideNonTranslated' || $row[$tableCtrl['languageField']] == -1) {
273 $overlaidRecordset[] = $row;
274 }
275 }
276 }
277 // Return the overlaid recordset
278 return $overlaidRecordset;
279 }
280 else {
281 // When default language is displayed, we never want to return a record carrying another language!
282 // Return the filtered recordset
283 return $filteredRecordset;
284 }
285 }
286 // Provided recordset does not contain languageField field, return recordset unchanged
287 else {
288 return $recordset;
289 }
290 }
291 // Test if the TCA definition includes translation information for a foreign table
292 elseif (isset($tableCtrl['transForeignTable'])) {
293 // The foreign table has a TCA structure. We can proceed.
294 if (isset($GLOBALS['TCA'][$tableCtrl['transForeignTable']])) {
295 $foreignCtrl = $GLOBALS['TCA'][$tableCtrl['transForeignTable']]['ctrl'];
296 // Check that the foreign table is indeed the appropriate translation table
297 // and also check that the foreign table has all the necessary TCA definitions
298 if (!empty($foreignCtrl['transOrigPointerTable']) && $foreignCtrl['transOrigPointerTable'] == $table && !empty($foreignCtrl['transOrigPointerField']) && !empty($foreignCtrl['languageField'])) {
299 // Assemble a list of all uid's of records to translate
300 $uidList = array();
301 foreach ($recordset as $row) {
302 $uidList[] = $row['uid'];
303 }
304
305 // Get all overlay records
306 $overlays = $this->getForeignOverlayRecords($tableCtrl['transForeignTable'], $uidList, $sys_language_content);
307
308 // Now loop on the filtered recordset and try to overlay each record
309 $overlaidRecordset = array();
310 foreach ($recordset as $row) {
311 // An overlay exists, apply it
312 if (isset($overlays[$row['uid']])) {
313 $overlaidRecordset[] = self::overlaySingleRecord($table, $row, $overlays[$row['uid']][$row['pid']]);
314 }
315 // No overlay exists
316 else {
317 // Take original record, only if non-translated are not hidden
318 if ($OLmode != 'hideNonTranslated') {
319 $overlaidRecordset[] = $row;
320 }
321 }
322 }
323 // Return the overlaid recordset
324 return $overlaidRecordset;
325 }
326 }
327 // The foreign table has no TCA definition, it's impossible to perform overlays
328 // Return recordset as is
329 else {
330 return $recordset;
331 }
332 }
333 // No appropriate language fields defined in TCA, return recordset unchanged
334 else {
335 return $recordset;
336 }
337 }
338 // No TCA for table, return recordset unchanged
339 else {
340 return $recordset;
341 }
342 }
343 // Recordset did not contain uid or pid field, return recordset unchanged
344 else {
345 return $recordset;
346 }
347 }
348
349 /**
350 * This method is a wrapper around getLocalOverlayRecords() and getForeignOverlayRecords().
351 * It makes it possible to use the same call whether translations are in the same table or
352 * in a foreign table. This method dispatches accordingly.
353 *
354 * @param string $table: name of the table for which to fetch the records
355 * @param array $uids: array of all uid's of the original records for which to fetch the translation
356 * @param integer $sys_language_content: uid of the system language to translate to
357 * @return array All overlay records arranged per original uid and per pid, so that they can be checked (this is related to workspaces)
358 */
359 public function getOverlayRecords($table, $uids, $sys_language_content) {
360 if (isset($GLOBALS['TCA'][$table]['ctrl']['transForeignTable'])) {
361 return self::getForeignOverlayRecords($GLOBALS['TCA'][$table]['ctrl']['transForeignTable'], $uids, $sys_language_content);
362 }
363 else {
364 return self::getLocalOverlayRecords($table, $uids, $sys_language_content);
365 }
366 }
367
368 /**
369 * This method is used to retrieve all the records for overlaying other records
370 * when those records are stored in the same table as the originals
371 *
372 * @param string $table: name of the table for which to fetch the records
373 * @param array $uids: array of all uid's of the original records for which to fetch the translation
374 * @param integer $sys_language_content: uid of the system language to translate to
375 * @return array All overlay records arranged per original uid and per pid, so that they can be checked (this is related to workspaces)
376 */
377 public function getLocalOverlayRecords($table, $uids, $sys_language_content) {
378 $tableCtrl = $GLOBALS['TCA'][$table]['ctrl'];
379 // Select overlays for all records
380 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
381 '*',
382 $table,
383 $tableCtrl['languageField'].' = '.intval($sys_language_content).
384 ' AND '.$tableCtrl['transOrigPointerField'].' IN ('.implode(', ', $uids).')'.
385 ' AND '.self::getEnableFieldsCondition($table)
386 );
387 // Arrange overlay records according to transOrigPointerField, so that it's easy to relate them to the originals
388 // This structure is actually a 2-dimensional array, with the pid as the second key
389 // Because of versioning, there may be several overlays for a given original and matching the pid too
390 // ensures that we are refering to the correct overlay
391 $overlays = array();
392 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
393 if (!isset($overlays[$row[$tableCtrl['transOrigPointerField']]])) $overlays[$row[$tableCtrl['transOrigPointerField']]] = array();
394 $overlays[$row[$tableCtrl['transOrigPointerField']]][$row['pid']] = $row;
395 }
396 return $overlays;
397 }
398
399 /**
400 * This method is used to retrieve all the records for overlaying other records
401 * when those records are stored in a different table than the originals
402 *
403 * @param string $table: name of the table for which to fetch the records
404 * @param array $uids: array of all uid's of the original records for which to fetch the translation
405 * @param integer $sys_language_content: uid of the system language to translate to
406 * @return array All overlay records arranged per original uid and per pid, so that they can be checked (this is related to workspaces)
407 */
408 public function getForeignOverlayRecords($table, $uids, $sys_language_content) {
409 $tableCtrl = $GLOBALS['TCA'][$table]['ctrl'];
410 // Select overlays for all records
411 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
412 '*',
413 $table,
414 $tableCtrl['languageField'].' = '.intval($sys_language_content).
415 ' AND '.$tableCtrl['transOrigPointerField'].' IN ('.implode(', ', $uids).')'.
416 ' AND '.self::getEnableFieldsCondition($table)
417 );
418 // Arrange overlay records according to transOrigPointerField, so that it's easy to relate them to the originals
419 $overlays = array();
420 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
421 $overlays[$row[$tableCtrl['transOrigPointerField']]] = $row;
422 }
423 return $overlays;
424 }
425
426 /**
427 * This method takes a record and its overlay and performs the overlay according to active translation rules
428 * This piece of code is extracted from t3lib_page::getRecordOverlay()
429 *
430 * @param string $table: name of the table for which the operation is taking place
431 * @param array $record: record to overlay
432 * @param array $overlay: overlay of the record
433 * @return array Overlaid record
434 */
435 public function overlaySingleRecord($table, $record, $overlay) {
436 $overlaidRecord = $record;
437 $overlaidRecord['_LOCALIZED_UID'] = $overlay['uid'];
438 foreach($record as $key => $value) {
439 if ($key != 'uid' && $key != 'pid' && isset($overlay[$key])) {
440 if ($GLOBALS['TSFE']->TCAcachedExtras[$table]['l10n_mode'][$key] != 'exclude'
441 && ($GLOBALS['TSFE']->TCAcachedExtras[$table]['l10n_mode'][$key] != 'mergeIfNotBlank' || strcmp(trim($overlay[$key]), ''))) {
442 $overlaidRecord[$key] = $overlay[$key];
443 }
444 }
445 }
446 return $overlaidRecord;
447 }
448 }
449
450
451
452 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/overlays/class.tx_overlays.php']) {
453 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/overlays/class.tx_overlays.php']);
454 }
455
456 ?>