64d566df28bd7fe3588c3d0e477b9f3c20d30121
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Configuration / FlexForm / FlexFormTools.php
1 <?php
2 namespace TYPO3\CMS\Core\Configuration\FlexForm;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19
20 /**
21 * Contains functions for manipulating flex form data
22 *
23 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
24 */
25 class FlexFormTools {
26
27 /**
28 * If set, the charset of data XML is converted to system charset.
29 *
30 * @var bool
31 */
32 public $convertCharset = FALSE;
33
34 /**
35 * If set, section indexes are re-numbered before processing
36 *
37 * @var bool
38 */
39 public $reNumberIndexesOfSectionData = FALSE;
40
41 /**
42 * Contains data structure when traversing flexform
43 *
44 * @var array
45 */
46 public $traverseFlexFormXMLData_DS = array();
47
48 /**
49 * Contains data array when traversing flexform
50 *
51 * @var array
52 */
53 public $traverseFlexFormXMLData_Data = array();
54
55 /**
56 * Options for array2xml() for flexform.
57 * This will map the weird keys from the internal array to tags that could potentially be checked with a DTD/schema
58 *
59 * @var array
60 */
61 public $flexArray2Xml_options = array(
62 'parentTagMap' => array(
63 'data' => 'sheet',
64 'sheet' => 'language',
65 'language' => 'field',
66 'el' => 'field',
67 'field' => 'value',
68 'field:el' => 'el',
69 'el:_IS_NUM' => 'section',
70 'section' => 'itemType'
71 ),
72 'disableTypeAttrib' => 2
73 );
74
75 /**
76 * Reference to object called
77 *
78 * @var object
79 */
80 public $callBackObj = NULL;
81
82 /**
83 * Used for accumulation of clean XML
84 *
85 * @var array
86 */
87 public $cleanFlexFormXML = array();
88
89 /**
90 * Handler for Flex Forms
91 *
92 * @param string $table The table name of the record
93 * @param string $field The field name of the flexform field to work on
94 * @param array $row The record data array
95 * @param object $callBackObj Object (passed by reference) in which the call back function is located
96 * @param string $callBackMethod_value Method name of call back function in object for values
97 * @return bool|string If TRUE, error happened (error string returned)
98 */
99 public function traverseFlexFormXMLData($table, $field, $row, $callBackObj, $callBackMethod_value) {
100 if (!is_array($GLOBALS['TCA'][$table]) || !is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
101 return 'TCA table/field was not defined.';
102 }
103 $this->callBackObj = $callBackObj;
104 // Get Data Structure:
105 $dataStructArray = BackendUtility::getFlexFormDS($GLOBALS['TCA'][$table]['columns'][$field]['config'], $row, $table, $field);
106 // If data structure was ok, proceed:
107 if (is_array($dataStructArray)) {
108 // Get flexform XML data:
109 $xmlData = $row[$field];
110 // Convert charset:
111 if ($this->convertCharset) {
112 $xmlHeaderAttributes = GeneralUtility::xmlGetHeaderAttribs($xmlData);
113 $storeInCharset = strtolower($xmlHeaderAttributes['encoding']);
114 if ($storeInCharset) {
115 $currentCharset = $GLOBALS['LANG']->charSet;
116 $xmlData = $GLOBALS['LANG']->csConvObj->conv($xmlData, $storeInCharset, $currentCharset, 1);
117 }
118 }
119 $editData = GeneralUtility::xml2array($xmlData);
120 if (!is_array($editData)) {
121 return 'Parsing error: ' . $editData;
122 }
123 // Language settings:
124 $langChildren = $dataStructArray['meta']['langChildren'] ? 1 : 0;
125 $langDisabled = $dataStructArray['meta']['langDisable'] ? 1 : 0;
126 // Empty or invalid <meta>
127 if (!is_array($editData['meta'])) {
128 $editData['meta'] = array();
129 }
130 $editData['meta']['currentLangId'] = array();
131 $languages = $this->getAvailableLanguages();
132 foreach ($languages as $lInfo) {
133 $editData['meta']['currentLangId'][] = $lInfo['ISOcode'];
134 }
135 if (!count($editData['meta']['currentLangId'])) {
136 $editData['meta']['currentLangId'] = array('DEF');
137 }
138 $editData['meta']['currentLangId'] = array_unique($editData['meta']['currentLangId']);
139 if ($langChildren || $langDisabled) {
140 $lKeys = array('DEF');
141 } else {
142 $lKeys = $editData['meta']['currentLangId'];
143 }
144 // Tabs sheets
145 if (is_array($dataStructArray['sheets'])) {
146 $sKeys = array_keys($dataStructArray['sheets']);
147 } else {
148 $sKeys = array('sDEF');
149 }
150 // Traverse languages:
151 foreach ($lKeys as $lKey) {
152 foreach ($sKeys as $sheet) {
153 $sheetCfg = $dataStructArray['sheets'][$sheet];
154 list($dataStruct, $sheet) = GeneralUtility::resolveSheetDefInDS($dataStructArray, $sheet);
155 // Render sheet:
156 if (is_array($dataStruct['ROOT']) && is_array($dataStruct['ROOT']['el'])) {
157 // Separate language key
158 $lang = 'l' . $lKey;
159 $PA['vKeys'] = $langChildren && !$langDisabled ? $editData['meta']['currentLangId'] : array('DEF');
160 $PA['lKey'] = $lang;
161 $PA['callBackMethod_value'] = $callBackMethod_value;
162 $PA['table'] = $table;
163 $PA['field'] = $field;
164 $PA['uid'] = $row['uid'];
165 $this->traverseFlexFormXMLData_DS = &$dataStruct;
166 $this->traverseFlexFormXMLData_Data = &$editData;
167 // Render flexform:
168 $this->traverseFlexFormXMLData_recurse($dataStruct['ROOT']['el'], $editData['data'][$sheet][$lang], $PA, 'data/' . $sheet . '/' . $lang);
169 } else {
170 return 'Data Structure ERROR: No ROOT element found for sheet "' . $sheet . '".';
171 }
172 }
173 }
174 } else {
175 return 'Data Structure ERROR: ' . $dataStructArray;
176 }
177 }
178
179 /**
180 * Recursively traversing flexform data according to data structure and element data
181 *
182 * @param array $dataStruct (Part of) data structure array that applies to the sub section of the flexform data we are processing
183 * @param array $editData (Part of) edit data array, reflecting current part of data structure
184 * @param array $PA Additional parameters passed.
185 * @param string $path Telling the "path" to the element in the flexform XML
186 * @return array
187 */
188 public function traverseFlexFormXMLData_recurse($dataStruct, $editData, &$PA, $path = '') {
189 if (is_array($dataStruct)) {
190 foreach ($dataStruct as $key => $value) {
191 // The value of each entry must be an array.
192 if (is_array($value)) {
193 if ($value['type'] == 'array') {
194 // Array (Section) traversal
195 if ($value['section']) {
196 $cc = 0;
197 if (is_array($editData[$key]['el'])) {
198 if ($this->reNumberIndexesOfSectionData) {
199 $temp = array();
200 $c3 = 0;
201 foreach ($editData[$key]['el'] as $v3) {
202 $temp[++$c3] = $v3;
203 }
204 $editData[$key]['el'] = $temp;
205 }
206 foreach ($editData[$key]['el'] as $k3 => $v3) {
207 if (is_array($v3)) {
208 $cc = $k3;
209 $theType = key($v3);
210 $theDat = $v3[$theType];
211 $newSectionEl = $value['el'][$theType];
212 if (is_array($newSectionEl)) {
213 $this->traverseFlexFormXMLData_recurse(array($theType => $newSectionEl), array($theType => $theDat), $PA, $path . '/' . $key . '/el/' . $cc);
214 }
215 }
216 }
217 }
218 } else {
219 // Array traversal:
220 $this->traverseFlexFormXMLData_recurse($value['el'], $editData[$key]['el'], $PA, $path . '/' . $key . '/el');
221 }
222 } elseif (is_array($value['TCEforms']['config'])) {
223 // Processing a field value:
224 foreach ($PA['vKeys'] as $vKey) {
225 $vKey = 'v' . $vKey;
226 // Call back:
227 if ($PA['callBackMethod_value']) {
228 $this->callBackObj->{$PA['callBackMethod_value']}($value, $editData[$key][$vKey], $PA, $path . '/' . $key . '/' . $vKey, $this);
229 }
230 }
231 }
232 }
233 }
234 }
235 }
236
237 /**
238 * Returns an array of available languages to use for FlexForm operations
239 *
240 * @return array
241 */
242 public function getAvailableLanguages() {
243 $isL = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('static_info_tables');
244 // Find all language records in the system
245 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
246 'language_isocode,static_lang_isocode,title,uid',
247 'sys_language',
248 'pid=0' . BackendUtility::deleteClause('sys_language'),
249 '',
250 'title'
251 );
252 // Traverse them
253 $output = array();
254 $output[0] = array(
255 'uid' => 0,
256 'title' => 'Default language',
257 'ISOcode' => 'DEF'
258 );
259 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
260 $output[$row['uid']] = $row;
261 if (!empty($row['language_isocode'])) {
262 $output[$row['uid']]['ISOcode'] = $row['language_isocode'];
263 } elseif ($isL && $row['static_lang_isocode']) {
264 \TYPO3\CMS\Core\Utility\GeneralUtility::deprecationLog('Usage of the field "static_lang_isocode" is discouraged, and will stop working with CMS 8. Use the built-in language field "language_isocode" in your sys_language records.');
265 $rr = BackendUtility::getRecord('static_languages', $row['static_lang_isocode'], 'lg_iso_2');
266 if ($rr['lg_iso_2']) {
267 $output[$row['uid']]['ISOcode'] = $rr['lg_iso_2'];
268 }
269 }
270 if (!$output[$row['uid']]['ISOcode']) {
271 unset($output[$row['uid']]);
272 }
273 }
274 $GLOBALS['TYPO3_DB']->sql_free_result($res);
275 return $output;
276 }
277
278 /***********************************
279 *
280 * Processing functions
281 *
282 ***********************************/
283 /**
284 * Cleaning up FlexForm XML to hold only the values it may according to its Data Structure. Also the order of tags will follow that of the data structure.
285 * BE CAREFUL: DO not clean records in workspaces unless IN the workspace! The Data Structure might resolve falsely on a workspace record when cleaned from Live workspace.
286 *
287 * @param string $table Table name
288 * @param string $field Field name of the flex form field in which the XML is found that should be cleaned.
289 * @param array $row The record
290 * @return string Clean XML from FlexForm field
291 */
292 public function cleanFlexFormXML($table, $field, $row) {
293 // New structure:
294 $this->cleanFlexFormXML = array();
295 // Create and call iterator object:
296 $flexObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools::class);
297 $flexObj->reNumberIndexesOfSectionData = TRUE;
298 $flexObj->traverseFlexFormXMLData($table, $field, $row, $this, 'cleanFlexFormXML_callBackFunction');
299 return $this->flexArray2Xml($this->cleanFlexFormXML, TRUE);
300 }
301
302 /**
303 * Call back function for \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools class
304 * Basically just setting the value in a new array (thus cleaning because only values that are valid are visited!)
305 *
306 * @param array $dsArr Data structure for the current value
307 * @param mixed $data Current value
308 * @param array $PA Additional configuration used in calling function
309 * @param string $path Path of value in DS structure
310 * @param object $pObj Object reference to caller
311 * @return void
312 */
313 public function cleanFlexFormXML_callBackFunction($dsArr, $data, $PA, $path, $pObj) {
314 // Just setting value in our own result array, basically replicating the structure:
315 $pObj->setArrayValueByPath($path, $this->cleanFlexFormXML, $data);
316 // Looking if an "extension" called ".vDEFbase" is found and if so, accept that too:
317 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['flexFormXMLincludeDiffBase']) {
318 $vDEFbase = $pObj->getArrayValueByPath($path . '.vDEFbase', $pObj->traverseFlexFormXMLData_Data);
319 if (isset($vDEFbase)) {
320 $pObj->setArrayValueByPath($path . '.vDEFbase', $this->cleanFlexFormXML, $vDEFbase);
321 }
322 }
323 }
324
325 /***********************************
326 *
327 * Multi purpose functions
328 *
329 ***********************************/
330 /**
331 * Get a value from a multi-dimensional array by giving a path "../../.." pointing to the element
332 *
333 * @param string $pathArray The path pointing to the value field, eg. test/2/title to access $array['test'][2]['title']
334 * @param array $array Array to get value from. Passed by reference so the value returned can be used to change the value in the array!
335 * @return mixed Value returned
336 */
337 public function &getArrayValueByPath($pathArray, &$array) {
338 if (!is_array($pathArray)) {
339 $pathArray = explode('/', $pathArray);
340 }
341 if (is_array($array)) {
342 if (count($pathArray)) {
343 $key = array_shift($pathArray);
344 if (isset($array[$key])) {
345 if (!count($pathArray)) {
346 return $array[$key];
347 } else {
348 return $this->getArrayValueByPath($pathArray, $array[$key]);
349 }
350 } else {
351 return NULL;
352 }
353 }
354 }
355 }
356
357 /**
358 * Set a value in a multi-dimensional array by giving a path "../../.." pointing to the element
359 *
360 * @param string $pathArray The path pointing to the value field, eg. test/2/title to access $array['test'][2]['title']
361 * @param array $array Array to set value in. Passed by reference so the value returned can be used to change the value in the array!
362 * @param mixed $value Value to set
363 * @return mixed Value returned
364 */
365 public function setArrayValueByPath($pathArray, &$array, $value) {
366 if (isset($value)) {
367 if (!is_array($pathArray)) {
368 $pathArray = explode('/', $pathArray);
369 }
370 if (is_array($array)) {
371 if (count($pathArray)) {
372 $key = array_shift($pathArray);
373 if (!count($pathArray)) {
374 $array[$key] = $value;
375 return TRUE;
376 } else {
377 if (!isset($array[$key])) {
378 $array[$key] = array();
379 }
380 return $this->setArrayValueByPath($pathArray, $array[$key], $value);
381 }
382 }
383 }
384 }
385 }
386
387 /**
388 * Convert FlexForm data array to XML
389 *
390 * @param array $array Array to output in <T3FlexForms> XML
391 * @param bool $addPrologue If set, the XML prologue is returned as well.
392 * @return string XML content.
393 */
394 public function flexArray2Xml($array, $addPrologue = FALSE) {
395 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['flexformForceCDATA']) {
396 $this->flexArray2Xml_options['useCDATA'] = 1;
397 }
398 $options = $GLOBALS['TYPO3_CONF_VARS']['BE']['niceFlexFormXMLtags'] ? $this->flexArray2Xml_options : array();
399 $spaceInd = $GLOBALS['TYPO3_CONF_VARS']['BE']['compactFlexFormXML'] ? -1 : 4;
400 $output = GeneralUtility::array2xml($array, '', 0, 'T3FlexForms', $spaceInd, $options);
401 if ($addPrologue) {
402 $output = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . $output;
403 }
404 return $output;
405 }
406
407 }