[CLEANUP] Replace count with empty in EXT:backend
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormEngine.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form;
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\Form\Utility\FormEngineUtility;
18 use TYPO3\CMS\Backend\Template\DocumentTemplate;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Backend\Utility\IconUtility;
21 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
22 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
23 use TYPO3\CMS\Core\Html\HtmlParser;
24 use TYPO3\CMS\Core\Http\AjaxRequestHandler;
25 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Core\Utility\MathUtility;
28 use TYPO3\CMS\Lang\LanguageService;
29 use TYPO3\CMS\Backend\Form\Container\FullRecordContainer;
30 use TYPO3\CMS\Backend\Form\Container\SoloFieldContainer;
31 use TYPO3\CMS\Backend\Form\Container\InlineRecordContainer;
32 use TYPO3\CMS\Backend\Form\Container\ListOfFieldsContainer;
33 use TYPO3\CMS\Core\Utility\ArrayUtility;
34
35 /**
36 * This is form engine - Class for creating the backend editing forms.
37 */
38 class FormEngine {
39
40 /**
41 * @var bool
42 */
43 public $disableWizards = FALSE;
44
45 /**
46 * List of additional preview languages that should be shown to the user. Initialized early.
47 *
48 * array(
49 * $languageUid => array(
50 * 'uid' => $languageUid,
51 * 'ISOcode' => $isoCodeOfLanguage
52 * )
53 * )
54 *
55 * @var array
56 */
57 protected $additionalPreviewLanguages = array();
58
59 /**
60 * @var string
61 */
62 protected $extJSCODE = '';
63
64 /**
65 * @var array HTML of additional hidden fields rendered by sub containers
66 */
67 protected $hiddenFieldAccum = array();
68
69 /**
70 * @var string
71 */
72 public $TBE_EDITOR_fieldChanged_func = '';
73
74 /**
75 * @var bool
76 */
77 public $loadMD5_JS = TRUE;
78
79 /**
80 * Alternative return URL path (default is \TYPO3\CMS\Core\Utility\GeneralUtility::linkThisScript())
81 *
82 * @var string
83 */
84 public $returnUrl = '';
85
86 /**
87 * Can be set to point to a field name in the form which will be set to '1' when the form
88 * is submitted with a *save* button. This way the recipient script can determine that
89 * the form was submitted for save and not "close" for example.
90 *
91 * @var string
92 */
93 public $doSaveFieldName = '';
94
95 /**
96 * If this evaluates to TRUE, the forms are rendering only localization relevant fields of the records.
97 *
98 * @var string
99 */
100 public $localizationMode = '';
101
102 /**
103 * When enabled all elements are rendered non-editable
104 *
105 * @var bool
106 */
107 protected $renderReadonly = FALSE;
108
109 /**
110 * @var InlineStackProcessor
111 */
112 protected $inlineStackProcessor;
113
114 /**
115 * @var array Data array from IRRE pushed to frontend as json array
116 */
117 protected $inlineData = array();
118
119 /**
120 * Set by readPerms() (caching)
121 *
122 * @var string
123 */
124 public $perms_clause = '';
125
126 /**
127 * Set by readPerms() (caching-flag)
128 *
129 * @var bool
130 */
131 public $perms_clause_set = FALSE;
132
133 /**
134 * Total wrapping for the table rows
135 *
136 * @var string
137 * @todo: This is overwritten in __construct
138 */
139 public $totalWrap = '<hr />|<hr />';
140
141 /**
142 * This array of fields will be set as hidden-fields instead of rendered normally!
143 * This is used by EditDocumentController to force some field values if set as "overrideVals" in _GP
144 *
145 * @var array
146 */
147 public $hiddenFieldListArr = array();
148
149 // Internal, registers for user defined functions etc.
150 /**
151 * Additional HTML code, printed before the form
152 *
153 * @var array
154 */
155 public $additionalCode_pre = array();
156
157 /**
158 * Additional JavaScript printed after the form
159 *
160 * @var array
161 */
162 protected $additionalJS_post = array();
163
164 /**
165 * Additional JavaScript executed on submit; If you set "OK" variable it will raise an error
166 * about RTEs not being loaded and offer to block further submission.
167 *
168 * @var array
169 */
170 public $additionalJS_submit = array();
171
172 /**
173 * Array containing hook class instances called once for a form
174 *
175 * @var array
176 */
177 public $hookObjectsMainFields = array();
178
179 /**
180 * Rows getting inserted into the headers (when called from the EditDocumentController)
181 *
182 * @var array
183 */
184 public $extraFormHeaders = array();
185
186 /**
187 * Form template, relative to typo3 directory
188 *
189 * @var string
190 */
191 public $templateFile = '';
192
193 /**
194 * @var string The table that is handled
195 */
196 protected $table = '';
197
198 /**
199 * @var array Database row data
200 */
201 protected $databaseRow = array();
202
203 /**
204 * @var NodeFactory Factory taking care of creating appropriate sub container and elements
205 */
206 protected $nodeFactory;
207
208 /**
209 * Array with requireJS modules, use module name as key, the value could be callback code.
210 * Use NULL as value if no callback is used.
211 *
212 * @var array
213 */
214 protected $requireJsModules = array(
215 'TYPO3/CMS/Backend/FormEngineValidation' => NULL
216 );
217
218 /**
219 * Constructor function, setting internal variables, loading the styles used.
220 *
221 */
222 public function __construct() {
223 $this->inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
224 $this->initializeAdditionalPreviewLanguages();
225 // Prepare user defined objects (if any) for hooks which extend this function:
226 $this->hookObjectsMainFields = array();
227 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms.php']['getMainFieldsClass'])) {
228 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms.php']['getMainFieldsClass'] as $classRef) {
229 $this->hookObjectsMainFields[] = GeneralUtility::getUserObj($classRef);
230 }
231 }
232 $this->templateFile = 'sysext/backend/Resources/Private/Templates/FormEngine.html';
233 $template = GeneralUtility::getUrl(PATH_typo3 . $this->templateFile);
234 // Wrapping all table rows for a particular record being edited:
235 $this->totalWrap = HtmlParser::getSubpart($template, '###TOTALWRAP###');
236 $this->nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
237 }
238
239 /**
240 * Set render read only flag
241 *
242 * @param bool $value
243 */
244 public function setRenderReadonly($value) {
245 $this->renderReadonly = (bool)$value;
246 }
247
248 /*******************************************************
249 *
250 * Rendering the forms, fields etc
251 *
252 *******************************************************/
253
254 /**
255 * Based on the $table and $row of content, this displays the complete TCEform for the record.
256 * The input-$row is required to be preprocessed if necessary by eg.
257 * the \TYPO3\CMS\Backend\Form\DataPreprocessor class. For instance the RTE content
258 * should be transformed through this class first.
259 *
260 * @param string $table The table name
261 * @param array $databaseRow The record from the table for which to render a field.
262 * @return string HTML output
263 */
264 public function getMainFields($table, array $databaseRow) {
265 $this->table = $table;
266 $this->databaseRow = $databaseRow;
267
268 // Hook: getMainFields_preProcess
269 foreach ($this->hookObjectsMainFields as $hookObj) {
270 if (method_exists($hookObj, 'getMainFields_preProcess')) {
271 $hookObj->getMainFields_preProcess($table, $databaseRow, $this);
272 }
273 }
274
275 $options = $this->getConfigurationOptionsForChildElements();
276 $options['renderType'] = 'fullRecordContainer';
277 $resultArray = $this->nodeFactory->create($options)->render();
278
279 $content = $resultArray['html'];
280 $this->mergeResult($resultArray);
281
282 // Hook: getMainFields_postProcess
283 foreach ($this->hookObjectsMainFields as $hookObj) {
284 if (method_exists($hookObj, 'getMainFields_postProcess')) {
285 $hookObj->getMainFields_postProcess($table, $databaseRow, $this);
286 }
287 }
288
289 return $content;
290 }
291
292 /**
293 * Will return the TCEform element for just a single field from a record.
294 * The field must be listed in the currently displayed fields (as found in [types][showitem]) for the record.
295 * This also means that the $table/$row supplied must be complete so the list of fields to show can be found correctly
296 * This method is used by "full screen RTE". Difference to getListedFields() is basically that no wrapper html is rendered around the element.
297 *
298 * @param string $table The table name
299 * @param array $databaseRow The record from the table for which to render a field.
300 * @param string $theFieldToReturn The field name to return the TCEform element for.
301 * @return string HTML output
302 */
303 public function getSoloField($table, $databaseRow, $theFieldToReturn) {
304 $this->table = $table;
305 $this->databaseRow = $databaseRow;
306
307 $options = $this->getConfigurationOptionsForChildElements();
308 $options['singleFieldToRender'] = $theFieldToReturn;
309 $options['renderType'] = 'soloFieldContainer';
310 $resultArray = $this->nodeFactory->create($options)->render();
311 $html = $resultArray['html'];
312
313 $this->additionalJS_post = $resultArray['additionalJavaScriptPost'];
314 $this->additionalJS_submit = $resultArray['additionalJavaScriptSubmit'];
315 $this->extJSCODE = $resultArray['extJSCODE'];
316 $this->inlineData = $resultArray['inlineData'];
317 $this->hiddenFieldAccum = $resultArray['additionalHiddenFields'];
318 $this->additionalCode_pre = $resultArray['additionalHeadTags'];
319
320 return $html;
321 }
322
323 /**
324 * Will return the TCEform elements for a pre-defined list of fields.
325 * Notice that this will STILL use the configuration found in the list [types][showitem] for those fields which are found there.
326 * So ideally the list of fields given as argument to this function should also be in the current [types][showitem] list of the record.
327 * Used for displaying forms for the frontend edit icons for instance.
328 *
329 * @todo: The list module calls this method multiple times on the same class instance if single fields
330 * @todo: of multiple records are edited. This is why the properties are accumulated here.
331 *
332 * @param string $table The table name
333 * @param array $databaseRow The record array.
334 * @param string $list Commalist of fields from the table. These will be shown in the specified order in a form.
335 * @return string TCEform elements in a string.
336 */
337 public function getListedFields($table, $databaseRow, $list) {
338 $this->table = $table;
339 $this->databaseRow = $databaseRow;
340
341 $options = $this->getConfigurationOptionsForChildElements();
342 $options['fieldListToRender'] = $list;
343 $options['renderType'] = 'listOfFieldsContainer';
344 $resultArray = $this->nodeFactory->create($options)->render();
345 $html = $resultArray['html'];
346 $this->mergeResult($resultArray);
347
348 return $html;
349 }
350
351
352 /**
353 * Merge existing data with the given result array
354 *
355 * @param array $resultArray Array returned by child
356 * @return void
357 */
358 protected function mergeResult(array $resultArray) {
359 foreach ($resultArray['additionalJavaScriptPost'] as $element) {
360 $this->additionalJS_post[] = $element;
361 }
362 foreach ($resultArray['additionalJavaScriptSubmit'] as $element) {
363 $this->additionalJS_submit[] = $element;
364 }
365 if (!empty($resultArray['requireJsModules'])) {
366 foreach ($resultArray['requireJsModules'] as $module) {
367 $moduleName = NULL;
368 $callback = NULL;
369 if (is_string($module)) {
370 // if $module is a string, no callback
371 $moduleName = $module;
372 $callback = NULL;
373 } elseif (is_array($module)) {
374 // if $module is an array, callback is possible
375 foreach ($module as $key => $value) {
376 $moduleName = $key;
377 $callback = $value;
378 break;
379 }
380 }
381 if ($moduleName !== NULL) {
382 if (!empty($this->requireJsModules[$moduleName]) && $callback !== NULL) {
383 $existingValue = $this->requireJsModules[$moduleName];
384 if (!is_array($existingValue)) {
385 $existingValue = array($existingValue);
386 }
387 $existingValue[] = $callback;
388 $this->requireJsModules[$moduleName] = $existingValue;
389 } else {
390 $this->requireJsModules[$moduleName] = $callback;
391 }
392 }
393 }
394 }
395 $this->extJSCODE = $this->extJSCODE . LF . $resultArray['extJSCODE'];
396 $this->inlineData = $resultArray['inlineData'];
397 foreach ($resultArray['additionalHiddenFields'] as $element) {
398 $this->hiddenFieldAccum[] = $element;
399 }
400 foreach ($resultArray['additionalHeadTags'] as $element) {
401 $this->additionalCode_pre[] = $element;
402 }
403
404 if (!empty($resultArray['inlineData'])) {
405 $resultArrayInlineData = $this->inlineData;
406 $resultInlineData = $resultArray['inlineData'];
407 ArrayUtility::mergeRecursiveWithOverrule($resultArrayInlineData, $resultInlineData);
408 $this->inlineData = $resultArrayInlineData;
409 }
410 }
411
412 /**
413 * Returns an array of global form settings to be given to child elements.
414 *
415 * @return array
416 */
417 protected function getConfigurationOptionsForChildElements() {
418 return array(
419 'renderReadonly' => $this->renderReadonly,
420 'disabledWizards' => $this->disableWizards,
421 'returnUrl' => $this->thisReturnUrl(),
422 'table' => $this->table,
423 'databaseRow' => $this->databaseRow,
424 'recordTypeValue' => '',
425 'additionalPreviewLanguages' => $this->additionalPreviewLanguages,
426 'localizationMode' => $this->localizationMode, // @todo: find out the details, Warning, this overlaps with inline behaviour localizationMode
427 'elementBaseName' => '',
428 'tabAndInlineStack' => array(),
429 'inlineFirstPid' => $this->getInlineFirstPid(),
430 'inlineExpandCollapseStateArray' => $this->getInlineExpandCollapseStateArrayForTableUid($this->table, $this->databaseRow['uid']),
431 'inlineData' => $this->inlineData,
432 'inlineStructure' => $this->inlineStackProcessor->getStructure(),
433 'overruleTypesArray' => array(),
434 'hiddenFieldListArray' => $this->hiddenFieldListArr,
435 'isAjaxContext' => FALSE,
436 'flexFormFieldIdentifierPrefix' => 'ID',
437 'nodeFactory' => $this->nodeFactory,
438 );
439 }
440
441 /**
442 * General processor for AJAX requests concerning IRRE.
443 *
444 * @param array $_ Additional parameters (not used here)
445 * @param AjaxRequestHandler $ajaxObj The AjaxRequestHandler object of this request
446 * @throws \RuntimeException
447 * @return void
448 */
449 public function processInlineAjaxRequest($_, AjaxRequestHandler $ajaxObj) {
450 $ajaxArguments = GeneralUtility::_GP('ajax');
451 $ajaxIdParts = explode('::', $GLOBALS['ajaxID'], 2);
452 if (isset($ajaxArguments) && is_array($ajaxArguments) && !empty($ajaxArguments)) {
453 $ajaxMethod = $ajaxIdParts[1];
454 $ajaxObj->setContentFormat('jsonbody');
455 // Construct runtime environment for Inline Relational Record Editing
456 $this->setUpRuntimeEnvironmentForAjaxRequests();
457 // @todo: ajaxArguments[2] is "returnUrl" in the first 3 calls - still needed?
458 switch ($ajaxMethod) {
459 case 'synchronizeLocalizeRecords':
460 $domObjectId = $ajaxArguments[0];
461 $type = $ajaxArguments[1];
462 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
463 $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
464 $this->inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
465 $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId);
466 $ajaxObj->setContent($this->renderInlineSynchronizeLocalizeRecords($type, $inlineFirstPid));
467 break;
468 case 'createNewRecord':
469 $domObjectId = $ajaxArguments[0];
470 $createAfterUid = 0;
471 if (isset($ajaxArguments[1])) {
472 $createAfterUid = $ajaxArguments[1];
473 }
474 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
475 $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
476 $this->inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
477 $ajaxObj->setContent($this->renderInlineNewChildRecord($domObjectId, $createAfterUid));
478 break;
479 case 'getRecordDetails':
480 $domObjectId = $ajaxArguments[0];
481 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
482 $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
483 $this->inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
484 $ajaxObj->setContent($this->renderInlineChildRecord($domObjectId));
485 break;
486 case 'setExpandedCollapsedState':
487 $domObjectId = $ajaxArguments[0];
488 // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
489 $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId, FALSE);
490 $expand = $ajaxArguments[1];
491 $collapse = $ajaxArguments[2];
492 $this->setInlineExpandedCollapsedState($expand, $collapse);
493 break;
494 default:
495 throw new \RuntimeException('Not a valid ajax identifier', 1428227862);
496 }
497 }
498 }
499
500 /**
501 * Handle AJAX calls to dynamically load the form fields of a given inline record.
502 *
503 * @param string $domObjectId The calling object in hierarchy, that requested a new record.
504 * @return array An array to be used for JSON
505 */
506 protected function renderInlineChildRecord($domObjectId) {
507 // The current table - for this table we should add/import records
508 $current = $this->inlineStackProcessor->getUnstableStructure();
509 // The parent table - this table embeds the current table
510 $parent = $this->inlineStackProcessor->getStructureLevel(-1);
511 $config = $parent['config'];
512
513 if (empty($config['foreign_table']) || !is_array($GLOBALS['TCA'][$config['foreign_table']])) {
514 return $this->getErrorMessageForAJAX('Wrong configuration in table ' . $parent['table']);
515 }
516
517 $config = FormEngineUtility::mergeInlineConfiguration($config);
518
519 // Set flag in config so that only the fields are rendered
520 $config['renderFieldsOnly'] = TRUE;
521 $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll'];
522 $expandSingle = isset($config['appearance']['expandSingle']) && $config['appearance']['expandSingle'];
523
524 $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
525 $record = $inlineRelatedRecordResolver->getRecord($current['table'], $current['uid']);
526
527 $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId);
528 // The HTML-object-id's prefix of the dynamically created record
529 $objectPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $current['table'];
530 $objectId = $objectPrefix . '-' . $record['uid'];
531
532 $options = $this->getConfigurationOptionsForChildElements();
533 $options['databaseRow'] = array('uid' => $parent['uid']);
534 $options['inlineFirstPid'] = $inlineFirstPid;
535 $options['inlineRelatedRecordToRender'] = $record;
536 $options['inlineRelatedRecordConfig'] = $config;
537 $options['inlineStructure'] = $this->inlineStackProcessor->getStructure();
538 $options['isAjaxContext'] = TRUE;
539
540 $options['renderType'] = 'inlineRecordContainer';
541 $childArray = $this->nodeFactory->create($options)->render();
542
543 if ($childArray === FALSE) {
544 return $this->getErrorMessageForAJAX('Access denied');
545 }
546
547 $this->mergeResult($childArray);
548
549 $jsonArray = array(
550 'data' => $childArray['html'],
551 'scriptCall' => array(),
552 );
553 $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
554 if ($config['foreign_unique']) {
555 $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ');';
556 }
557
558 $jsonArray = $this->getInlineAjaxCommonScriptCalls($jsonArray, $config, $inlineFirstPid);
559
560 // Collapse all other records if requested:
561 if (!$collapseAll && $expandSingle) {
562 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ');';
563 }
564
565 return $jsonArray;
566 }
567
568 /**
569 * Handle AJAX calls to show a new inline-record of the given table.
570 *
571 * @param string $domObjectId The calling object in hierarchy, that requested a new record.
572 * @param string|int $foreignUid If set, the new record should be inserted after that one.
573 * @return array An array to be used for JSON
574 */
575 protected function renderInlineNewChildRecord($domObjectId, $foreignUid) {
576 // The current table - for this table we should add/import records
577 $current = $this->inlineStackProcessor->getUnstableStructure();
578 // The parent table - this table embeds the current table
579 $parent = $this->inlineStackProcessor->getStructureLevel(-1);
580 $config = $parent['config'];
581
582 if (empty($config['foreign_table']) || !is_array($GLOBALS['TCA'][$config['foreign_table']])) {
583 return $this->getErrorMessageForAJAX('Wrong configuration in table ' . $parent['table']);
584 }
585
586 $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
587
588 $config = FormEngineUtility::mergeInlineConfiguration($config);
589
590 $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll'];
591 $expandSingle = isset($config['appearance']['expandSingle']) && $config['appearance']['expandSingle'];
592
593 $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId);
594
595 // Dynamically create a new record using \TYPO3\CMS\Backend\Form\DataPreprocessor
596 if (!$foreignUid || !MathUtility::canBeInterpretedAsInteger($foreignUid) || $config['foreign_selector']) {
597 $record = $inlineRelatedRecordResolver->getNewRecord($inlineFirstPid, $current['table']);
598 // Set default values for new created records
599 if (isset($config['foreign_record_defaults']) && is_array($config['foreign_record_defaults'])) {
600 $foreignTableConfig = $GLOBALS['TCA'][$current['table']];
601 // The following system relevant fields can't be set by foreign_record_defaults
602 $notSettableFields = array(
603 'uid', 'pid', 't3ver_oid', 't3ver_id', 't3ver_label', 't3ver_wsid', 't3ver_state', 't3ver_stage',
604 't3ver_count', 't3ver_tstamp', 't3ver_move_id'
605 );
606 $configurationKeysForNotSettableFields = array(
607 'crdate', 'cruser_id', 'delete', 'origUid', 'transOrigDiffSourceField', 'transOrigPointerField',
608 'tstamp'
609 );
610 foreach ($configurationKeysForNotSettableFields as $configurationKey) {
611 if (isset($foreignTableConfig['ctrl'][$configurationKey])) {
612 $notSettableFields[] = $foreignTableConfig['ctrl'][$configurationKey];
613 }
614 }
615 foreach ($config['foreign_record_defaults'] as $fieldName => $defaultValue) {
616 if (isset($foreignTableConfig['columns'][$fieldName]) && !in_array($fieldName, $notSettableFields)) {
617 $record[$fieldName] = $defaultValue;
618 }
619 }
620 }
621 // Set language of new child record to the language of the parent record:
622 if ($parent['localizationMode'] === 'select') {
623 $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
624 $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
625 $childLanguageField = $GLOBALS['TCA'][$current['table']]['ctrl']['languageField'];
626 if ($parentRecord[$parentLanguageField] > 0) {
627 $record[$childLanguageField] = $parentRecord[$parentLanguageField];
628 }
629 }
630 } else {
631 // @todo: Check this: Else also hits if $foreignUid = 0?
632 $record = $inlineRelatedRecordResolver->getRecord($current['table'], $foreignUid);
633 }
634 // Now there is a foreign_selector, so there is a new record on the intermediate table, but
635 // this intermediate table holds a field, which is responsible for the foreign_selector, so
636 // we have to set this field to the uid we get - or if none, to a new uid
637 if ($config['foreign_selector'] && $foreignUid) {
638 $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $config['foreign_selector']);
639 // For a selector of type group/db, prepend the tablename (<tablename>_<uid>):
640 $record[$config['foreign_selector']] = $selConfig['type'] != 'groupdb' ? '' : $selConfig['table'] . '_';
641 $record[$config['foreign_selector']] .= $foreignUid;
642 if ($selConfig['table'] === 'sys_file') {
643 $fileRecord = $inlineRelatedRecordResolver->getRecord($selConfig['table'], $foreignUid);
644 if ($fileRecord !== FALSE && !$this->checkInlineFileTypeAccessForField($selConfig, $fileRecord)) {
645 return $this->getErrorMessageForAJAX('File extension ' . $fileRecord['extension'] . ' is not allowed here!');
646 }
647 }
648 }
649 // The HTML-object-id's prefix of the dynamically created record
650 $objectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
651 $objectPrefix = $objectName . '-' . $current['table'];
652 $objectId = $objectPrefix . '-' . $record['uid'];
653
654 $options = $this->getConfigurationOptionsForChildElements();
655 $options['databaseRow'] = array('uid' => $parent['uid']);
656 $options['inlineFirstPid'] = $inlineFirstPid;
657 $options['inlineRelatedRecordToRender'] = $record;
658 $options['inlineRelatedRecordConfig'] = $config;
659 $options['inlineStructure'] = $this->inlineStackProcessor->getStructure();
660 $options['isAjaxContext'] = TRUE;
661
662 $options['renderType'] = 'inlineRecordContainer';
663 $childArray = $this->nodeFactory->create($options)->render();
664
665 if ($childArray === FALSE) {
666 return $this->getErrorMessageForAJAX('Access denied');
667 }
668
669 $this->mergeResult($childArray);
670
671 $jsonArray = array(
672 'data' => $childArray['html'],
673 'scriptCall' => array(),
674 );
675
676 if (!$current['uid']) {
677 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
678 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ',null,' . GeneralUtility::quoteJSvalue($foreignUid) . ');';
679 } else {
680 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
681 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ',' . GeneralUtility::quoteJSvalue($current['uid']) . ',' . GeneralUtility::quoteJSvalue($foreignUid) . ');';
682 }
683
684 $jsonArray = $this->getInlineAjaxCommonScriptCalls($jsonArray, $config, $inlineFirstPid);
685
686 // Collapse all other records if requested:
687 if (!$collapseAll && $expandSingle) {
688 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ', ' . GeneralUtility::quoteJSvalue($objectPrefix) . ', ' . GeneralUtility::quoteJSvalue($record['uid']) . ');';
689 }
690 // Tell the browser to scroll to the newly created record
691
692 $jsonArray['scriptCall'][] = 'Element.scrollTo(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
693 // Fade out and fade in the new record in the browser view to catch the user's eye
694 $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
695
696 return $jsonArray;
697 }
698
699 /**
700 * Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent.
701 *
702 * @param string $type Defines the type 'localize' or 'synchronize' (string) or a single uid to be localized (int)
703 * @param int $inlineFirstPid Inline first pid
704 * @return array An array to be used for JSON
705 */
706 protected function renderInlineSynchronizeLocalizeRecords($type, $inlineFirstPid) {
707 $jsonArray = FALSE;
708 if (GeneralUtility::inList('localize,synchronize', $type) || MathUtility::canBeInterpretedAsInteger($type)) {
709 $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
710 // The parent level:
711 $parent = $this->inlineStackProcessor->getStructureLevel(-1);
712 $current = $this->inlineStackProcessor->getUnstableStructure();
713 $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
714
715 $cmd = array();
716 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = $parent['field'] . ',' . $type;
717 /** @var $tce \TYPO3\CMS\Core\DataHandling\DataHandler */
718 $tce = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
719 $tce->stripslashes_values = FALSE;
720 $tce->start(array(), $cmd);
721 $tce->process_cmdmap();
722
723 $oldItemList = $parentRecord[$parent['field']];
724 $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parent['field']];
725
726 $jsonArray = array(
727 'scriptCall' => array(),
728 );
729 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
730 $nameObjectForeignTable = $nameObject . '-' . $current['table'];
731 // Get the name of the field pointing to the original record:
732 $transOrigPointerField = $GLOBALS['TCA'][$current['table']]['ctrl']['transOrigPointerField'];
733 // Get the name of the field used as foreign selector (if any):
734 $foreignSelector = isset($parent['config']['foreign_selector']) && $parent['config']['foreign_selector'] ? $parent['config']['foreign_selector'] : FALSE;
735 // Convert lists to array with uids of child records:
736 $oldItems = FormEngineUtility::getInlineRelatedRecordsUidArray($oldItemList);
737 $newItems = FormEngineUtility::getInlineRelatedRecordsUidArray($newItemList);
738 // Determine the items that were localized or localized:
739 $removedItems = array_diff($oldItems, $newItems);
740 $localizedItems = array_diff($newItems, $oldItems);
741 // Set the items that should be removed in the forms view:
742 foreach ($removedItems as $item) {
743 $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $item) . ', {forceDirectRemoval: true});';
744 }
745 // Set the items that should be added in the forms view:
746 $html = '';
747 $resultArray = NULL;
748 // @todo: This should be another container ...
749 foreach ($localizedItems as $item) {
750 $row = $inlineRelatedRecordResolver->getRecord($current['table'], $item);
751 $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($row[$foreignSelector]) : 'null';
752
753 $options = $this->getConfigurationOptionsForChildElements();
754 $options['databaseRow'] = array('uid' => $parent['uid']);
755 $options['inlineFirstPid'] = $inlineFirstPid;
756 $options['inlineRelatedRecordToRender'] = $row;
757 $options['inlineRelatedRecordConfig'] = $parent['config'];
758 $options['inlineStructure'] = $this->inlineStackProcessor->getStructure();
759 $options['isAjaxContext'] = TRUE;
760
761 $options['renderType'] = 'inlineRecordContainer';
762 $childArray = $this->nodeFactory->create($options)->render();
763 $html .= $childArray['html'];
764 $childArray['html'] = '';
765
766 // @todo: Obsolete if a container and copied from AbstractContainer for now
767 if ($resultArray === NULL) {
768 $resultArray = $childArray;
769 } else {
770 if (!empty($childArray['extJSCODE'])) {
771 $resultArray['extJSCODE'] .= LF . $childArray['extJSCODE'];
772 }
773 foreach ($childArray['additionalJavaScriptPost'] as $value) {
774 $resultArray['additionalJavaScriptPost'][] = $value;
775 }
776 foreach ($childArray['additionalJavaScriptSubmit'] as $value) {
777 $resultArray['additionalJavaScriptSubmit'][] = $value;
778 }
779 if (!empty($childArray['inlineData'])) {
780 $resultArrayInlineData = $resultArray['inlineData'];
781 $childInlineData = $childArray['inlineData'];
782 ArrayUtility::mergeRecursiveWithOverrule($resultArrayInlineData, $childInlineData);
783 $resultArray['inlineData'] = $resultArrayInlineData;
784 }
785 }
786
787 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($item) . ', null, ' . $selectedValue . ');';
788 // Remove possible virtual records in the form which showed that a child records could be localized:
789 if (isset($row[$transOrigPointerField]) && $row[$transOrigPointerField]) {
790 $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $row[$transOrigPointerField] . '_div') . ');';
791 }
792 }
793 if (!empty($html)) {
794 $jsonArray['data'] = $html;
795 array_unshift($jsonArray['scriptCall'], 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records') . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', json.data);');
796 }
797
798 $this->mergeResult($resultArray);
799
800 $jsonArray = $this->getInlineAjaxCommonScriptCalls($jsonArray, $parent['config'], $inlineFirstPid);
801 }
802 return $jsonArray;
803 }
804
805 /**
806 * Save the expanded/collapsed state of a child record in the BE_USER->uc.
807 *
808 * @param string $expand Whether this record is expanded.
809 * @param string $collapse Whether this record is collapsed.
810 * @return void
811 */
812 protected function setInlineExpandedCollapsedState($expand, $collapse) {
813 $backendUser = $this->getBackendUserAuthentication();
814 // The current table - for this table we should add/import records
815 $currentTable = $this->inlineStackProcessor->getUnstableStructure();
816 $currentTable = $currentTable['table'];
817 // The top parent table - this table embeds the current table
818 $top = $this->inlineStackProcessor->getStructureLevel(0);
819 $topTable = $top['table'];
820 $topUid = $top['uid'];
821 $inlineView = $this->getInlineExpandCollapseStateArray();
822 // Only do some action if the top record and the current record were saved before
823 if (MathUtility::canBeInterpretedAsInteger($topUid)) {
824 $expandUids = GeneralUtility::trimExplode(',', $expand);
825 $collapseUids = GeneralUtility::trimExplode(',', $collapse);
826 // Set records to be expanded
827 foreach ($expandUids as $uid) {
828 $inlineView[$topTable][$topUid][$currentTable][] = $uid;
829 }
830 // Set records to be collapsed
831 foreach ($collapseUids as $uid) {
832 $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
833 }
834 // Save states back to database
835 if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
836 $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
837 $backendUser->uc['inlineView'] = serialize($inlineView);
838 $backendUser->writeUC();
839 }
840 }
841 }
842
843 /**
844 * Construct runtime environment for Inline Relational Record Editing.
845 * - creates an anonymous \TYPO3\CMS\Backend\Controller\EditDocumentController in $GLOBALS['SOBE']
846 * - sets $this to $GLOBALS['SOBE']->tceforms
847 *
848 * @return void
849 */
850 protected function setUpRuntimeEnvironmentForAjaxRequests() {
851 $this->getLanguageService()->includeLLFile('EXT:lang/locallang_alt_doc.xlf');
852 // Create a new anonymous object:
853 $GLOBALS['SOBE'] = new \stdClass();
854 $GLOBALS['SOBE']->MOD_MENU = array();
855 // Setting virtual document name
856 $GLOBALS['SOBE']->MCONF['name'] = 'xMOD_alt_doc.php';
857 // CLEANSE SETTINGS
858 $GLOBALS['SOBE']->MOD_SETTINGS = array();
859 // Create an instance of the document template object
860 // @todo: resolve clash getDocumentTemplate() / getControllerDocumenttemplate()
861 $GLOBALS['SOBE']->doc = GeneralUtility::makeInstance(DocumentTemplate::class);
862 $GLOBALS['SOBE']->doc->backPath = $GLOBALS['BACK_PATH'];
863 // Initialize FormEngine (rendering the forms)
864 // @todo: check if this is still needed, simplify
865 $GLOBALS['SOBE']->tceforms = $this;
866 }
867
868 /**
869 * Return expand / collapse state array for a given table / uid combination
870 *
871 * @param string $table Handled table
872 * @param int $uid Handled uid
873 * @return array
874 */
875 protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid) {
876 $inlineView = $this->getInlineExpandCollapseStateArray();
877 $result = array();
878 if (MathUtility::canBeInterpretedAsInteger($uid)) {
879 if (!empty($inlineView[$table][$uid])) {
880 $result = $inlineView[$table][$uid];
881 }
882 }
883 return $result;
884 }
885
886 /**
887 * Get expand / collapse state of inline items
888 *
889 * @return array
890 */
891 protected function getInlineExpandCollapseStateArray() {
892 $backendUser = $this->getBackendUserAuthentication();
893 $inlineView = unserialize($backendUser->uc['inlineView']);
894 if (!is_array($inlineView)) {
895 $inlineView = array();
896 }
897 return $inlineView;
898 }
899
900 /**
901 * The "entry" pid for inline records. Nested inline records can potentially hang around on different
902 * pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes place on the page structure.
903 *
904 * @return integer
905 */
906 protected function getInlineFirstPid() {
907 $table = $this->table;
908 $row = $this->databaseRow;
909 // If the parent is a page, use the uid(!) of the (new?) page as pid for the child records:
910 if ($table == 'pages') {
911 $liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']);
912 $pid = is_null($liveVersionId) ? $row['uid'] : $liveVersionId;
913 } elseif ($row['pid'] < 0) {
914 $prevRec = BackendUtility::getRecord($table, abs($row['pid']));
915 $pid = $prevRec['pid'];
916 } else {
917 $pid = $row['pid'];
918 }
919 return $pid;
920 }
921
922 /**
923 * Checks if a record selector may select a certain file type
924 *
925 * @param array $selectorConfiguration
926 * @param array $fileRecord
927 * @return bool
928 */
929 protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord) {
930 if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
931 $allowedFileExtensions = GeneralUtility::trimExplode(
932 ',',
933 $selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'],
934 TRUE
935 );
936 if (!in_array(strtolower($fileRecord['extension']), $allowedFileExtensions, TRUE)) {
937 return FALSE;
938 }
939 }
940 return TRUE;
941 }
942
943 /**
944 * Remove an element from an array.
945 *
946 * @param mixed $needle The element to be removed.
947 * @param array $haystack The array the element should be removed from.
948 * @param mixed $strict Search elements strictly.
949 * @return array The array $haystack without the $needle
950 */
951 protected function removeFromArray($needle, $haystack, $strict = NULL) {
952 $pos = array_search($needle, $haystack, $strict);
953 if ($pos !== FALSE) {
954 unset($haystack[$pos]);
955 }
956 return $haystack;
957 }
958
959 /**
960 * Generates an error message that transferred as JSON for AJAX calls
961 *
962 * @param string $message The error message to be shown
963 * @return array The error message in a JSON array
964 */
965 protected function getErrorMessageForAJAX($message) {
966 $jsonArray = array(
967 'data' => $message,
968 'scriptCall' => array(
969 'alert("' . $message . '");'
970 )
971 );
972 return $jsonArray;
973 }
974
975 /**
976 * Determines and sets several script calls to a JSON array, that would have been executed if processed in non-AJAX mode.
977 *
978 * @param array &$jsonArray Reference of the array to be used for JSON
979 * @param array $config The configuration of the IRRE field of the parent record
980 * @param int $inlineFirstPid Inline first pid
981 * @return array Modified array
982 * @todo: Basically, this methods shouldn't be there at all ...
983 */
984 protected function getInlineAjaxCommonScriptCalls($jsonArray, $config, $inlineFirstPid) {
985 // Add data that would have been added at the top of a regular FormEngine call:
986 if ($headTags = $this->getInlineHeadTags()) {
987 $jsonArray['headData'] = $headTags;
988 }
989 // Add the JavaScript data that would have been added at the bottom of a regular FormEngine call:
990 $jsonArray['scriptCall'][] = $this->JSbottom('editform', TRUE);
991 // If script.aculo.us Sortable is used, update the Observer to know the record:
992 if ($config['appearance']['useSortable']) {
993 $inlineObjectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
994 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
995 }
996 // If FormEngine has some JavaScript code to be executed, just do it
997 // @todo: this is done by JSBottom() already?!
998 if ($this->extJSCODE) {
999 $jsonArray['scriptCall'][] = $this->extJSCODE;
1000 }
1001
1002 // require js handling
1003 foreach ($this->requireJsModules as $moduleName => $callbacks) {
1004 if (!is_array($callbacks)) {
1005 $callbacks = array($callbacks);
1006 }
1007 foreach ($callbacks as $callback) {
1008 $inlineCodeKey = $moduleName;
1009 $javaScriptCode = 'require(["' . $moduleName . '"]';
1010 if ($callback !== NULL) {
1011 $inlineCodeKey .= sha1($callback);
1012 $javaScriptCode .= ', ' . $callback;
1013 }
1014 $javaScriptCode .= ');';
1015 $jsonArray['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode;
1016 }
1017 }
1018 return $jsonArray;
1019 }
1020
1021 /**
1022 * Parses the HTML tags that would have been inserted to the <head> of a HTML document and returns the found tags as multidimensional array.
1023 *
1024 * @return array The parsed tags with their attributes and innerHTML parts
1025 * @todo: WTF?
1026 */
1027 protected function getInlineHeadTags() {
1028 $headTags = array();
1029 $headDataRaw = $this->JStop() . $this->getJavaScriptAndStyleSheetsOfPageRenderer();
1030 if ($headDataRaw) {
1031 // Create instance of the HTML parser:
1032 $parseObj = GeneralUtility::makeInstance(HtmlParser::class);
1033 // Removes script wraps:
1034 $headDataRaw = str_replace(array('/*<![CDATA[*/', '/*]]>*/'), '', $headDataRaw);
1035 // Removes leading spaces of a multi-line string:
1036 $headDataRaw = trim(preg_replace('/(^|\\r|\\n)( |\\t)+/', '$1', $headDataRaw));
1037 // Get script and link tags:
1038 $tags = array_merge($parseObj->getAllParts($parseObj->splitTags('link', $headDataRaw)), $parseObj->getAllParts($parseObj->splitIntoBlock('script', $headDataRaw)));
1039 foreach ($tags as $tagData) {
1040 $tagAttributes = $parseObj->get_tag_attributes($parseObj->getFirstTag($tagData), TRUE);
1041 $headTags[] = array(
1042 'name' => $parseObj->getFirstTagName($tagData),
1043 'attributes' => $tagAttributes[0],
1044 'innerHTML' => $parseObj->removeFirstAndLastTag($tagData)
1045 );
1046 }
1047 }
1048 return $headTags;
1049 }
1050
1051 /**
1052 * Gets the JavaScript of the pageRenderer.
1053 * This can be used to extract newly added files which have been added
1054 * during an AJAX request. Due to the spread possibilities of the pageRenderer
1055 * to add JavaScript rendering and extracting seems to be the easiest way.
1056 *
1057 * @return string
1058 * @todo: aaaargs ...
1059 */
1060 protected function getJavaScriptAndStyleSheetsOfPageRenderer() {
1061 /** @var $pageRenderer \TYPO3\CMS\Core\Page\PageRenderer */
1062 $pageRenderer = clone $GLOBALS['SOBE']->doc->getPageRenderer();
1063 $pageRenderer->setCharSet($this->getLanguageService()->charSet);
1064 $pageRenderer->setTemplateFile('EXT:backend/Resources/Private/Templates/helper_javascript_css.html');
1065 $javaScriptAndStyleSheets = $pageRenderer->render();
1066 return $javaScriptAndStyleSheets;
1067 }
1068
1069 /**
1070 * Returns the "returnUrl" of the form. Can be set externally or will be taken from "GeneralUtility::linkThisScript()"
1071 *
1072 * @return string Return URL of current script
1073 */
1074 protected function thisReturnUrl() {
1075 return $this->returnUrl ? $this->returnUrl : GeneralUtility::linkThisScript();
1076 }
1077
1078 /********************************************
1079 *
1080 * Template functions
1081 *
1082 ********************************************/
1083 /**
1084 * Wraps all the table rows into a single table.
1085 * Used externally from scripts like EditDocumentController and PageLayoutController (which uses FormEngine)
1086 *
1087 * @param string $c Code to output between table-parts; table rows
1088 * @param array $rec The record
1089 * @param string $table The table name
1090 * @return string
1091 */
1092 public function wrapTotal($c, $rec, $table) {
1093 $parts = $this->replaceTableWrap(explode('|', $this->totalWrap, 2), $rec, $table);
1094 return $parts[0] . $c . $parts[1] . implode(LF, $this->hiddenFieldAccum);
1095 }
1096
1097 /**
1098 * Generates a token and returns an input field with it
1099 *
1100 * @param string $formName Context of the token
1101 * @param string $tokenName The name of the token GET/POST variable
1102 * @return string A complete input field
1103 */
1104 static public function getHiddenTokenField($formName = 'securityToken', $tokenName = 'formToken') {
1105 $formprotection = FormProtectionFactory::get();
1106 return '<input type="hidden" name="' . $tokenName . '" value="' . $formprotection->generateToken($formName) . '" />';
1107 }
1108
1109 /**
1110 * This replaces markers in the total wrap
1111 *
1112 * @param array $arr An array of template parts containing some markers.
1113 * @param array $rec The record
1114 * @param string $table The table name
1115 * @return string
1116 */
1117 public function replaceTableWrap($arr, $rec, $table) {
1118 $icon = IconUtility::getSpriteIconForRecord($table, $rec, array('title' => $this->getRecordPath($table, $rec)));
1119 // Make "new"-label
1120 $languageService = $this->getLanguageService();
1121 if (strstr($rec['uid'], 'NEW')) {
1122 $newLabel = ' <span class="typo3-TCEforms-newToken">' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.new', TRUE) . '</span>';
1123 // BackendUtility::fixVersioningPid Should not be used here because NEW records are not offline workspace versions...
1124 $truePid = BackendUtility::getTSconfig_pidValue($table, $rec['uid'], $rec['pid']);
1125 $prec = BackendUtility::getRecordWSOL('pages', $truePid, 'title');
1126 $pageTitle = BackendUtility::getRecordTitle('pages', $prec, TRUE, FALSE);
1127 $rLabel = '<em>[PID: ' . $truePid . '] ' . $pageTitle . '</em>';
1128 // Fetch translated title of the table
1129 $tableTitle = $languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
1130 if ($table === 'pages') {
1131 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.createNewPage', TRUE);
1132 $pageTitle = sprintf($label, $tableTitle);
1133 } else {
1134 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.createNewRecord', TRUE);
1135 if ($rec['pid'] == 0) {
1136 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.createNewRecordRootLevel', TRUE);
1137 }
1138 $pageTitle = sprintf($label, $tableTitle, $pageTitle);
1139 }
1140 } else {
1141 $newLabel = ' <span class="typo3-TCEforms-recUid">[' . $rec['uid'] . ']</span>';
1142 $rLabel = BackendUtility::getRecordTitle($table, $rec, TRUE, FALSE);
1143 $prec = BackendUtility::getRecordWSOL('pages', $rec['pid'], 'uid,title');
1144 // Fetch translated title of the table
1145 $tableTitle = $languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
1146 if ($table === 'pages') {
1147 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editPage', TRUE);
1148 // Just take the record title and prepend an edit label.
1149 $pageTitle = sprintf($label, $tableTitle, $rLabel);
1150 } else {
1151 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecord', TRUE);
1152 $pageTitle = BackendUtility::getRecordTitle('pages', $prec, TRUE, FALSE);
1153 if ($rLabel === BackendUtility::getNoRecordTitle(TRUE)) {
1154 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecordNoTitle', TRUE);
1155 }
1156 if ($rec['pid'] == 0) {
1157 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecordRootLevel', TRUE);
1158 }
1159 if ($rLabel !== BackendUtility::getNoRecordTitle(TRUE)) {
1160 // Just take the record title and prepend an edit label.
1161 $pageTitle = sprintf($label, $tableTitle, $rLabel, $pageTitle);
1162 } else {
1163 // Leave out the record title since it is not set.
1164 $pageTitle = sprintf($label, $tableTitle, $pageTitle);
1165 }
1166 }
1167 $icon = $this->getControllerDocumentTemplate()->wrapClickMenuOnIcon($icon, $table, $rec['uid'], 1, '', '+copy,info,edit,view');
1168 }
1169 foreach ($arr as $k => $v) {
1170 // Make substitutions:
1171 $arr[$k] = str_replace(
1172 array(
1173 '###PAGE_TITLE###',
1174 '###ID_NEW_INDICATOR###',
1175 '###RECORD_LABEL###',
1176 '###TABLE_TITLE###',
1177 '###RECORD_ICON###'
1178 ),
1179 array(
1180 $pageTitle,
1181 $newLabel,
1182 $rLabel,
1183 htmlspecialchars($languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title'])),
1184 $icon
1185 ),
1186 $arr[$k]
1187 );
1188 }
1189 return $arr;
1190 }
1191
1192
1193 /********************************************
1194 *
1195 * JavaScript related functions
1196 *
1197 ********************************************/
1198 /**
1199 * JavaScript code added BEFORE the form is drawn:
1200 *
1201 * @return string A <script></script> section with JavaScript.
1202 */
1203 public function JStop() {
1204 $out = '';
1205 // Additional top HTML:
1206 if (!empty($this->additionalCode_pre)) {
1207 $out .= implode('
1208
1209 <!-- NEXT: -->
1210 ', $this->additionalCode_pre);
1211 }
1212 return $out;
1213 }
1214
1215 /**
1216 * JavaScript bottom code
1217 *
1218 * @param string $formname The identification of the form on the page.
1219 * @param bool $update Just extend/update existing settings, e.g. for AJAX call
1220 * @return string A section with JavaScript - if $update is FALSE, embedded in <script></script>
1221 */
1222 public function JSbottom($formname = 'forms[0]', $update = FALSE) {
1223 $languageService = $this->getLanguageService();
1224 $jsFile = array();
1225 $out = '';
1226 $this->TBE_EDITOR_fieldChanged_func = 'TBE_EDITOR.fieldChanged_fName(fName,formObj[fName+"_list"]);';
1227 if (!$update) {
1228 if ($this->loadMD5_JS) {
1229 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/md5.js');
1230 }
1231 $pageRenderer = $this->getPageRenderer();
1232 // load the main module for FormEngine with all important JS functions
1233 $this->requireJsModules['TYPO3/CMS/Backend/FormEngine'] = 'function(FormEngine) {
1234 FormEngine.setBrowserUrl(' . GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('browser')) . ');
1235 }';
1236 foreach ($this->requireJsModules as $moduleName => $callbacks) {
1237 if (!is_array($callbacks)) {
1238 $callbacks = array($callbacks);
1239 }
1240 foreach ($callbacks as $callback) {
1241 $pageRenderer->loadRequireJsModule($moduleName, $callback);
1242 }
1243 }
1244 $pageRenderer->loadPrototype();
1245 $pageRenderer->loadJquery();
1246 $pageRenderer->loadExtJS();
1247 $beUserAuth = $this->getBackendUserAuthentication();
1248 // Make textareas resizable and flexible ("autogrow" in height)
1249 $textareaSettings = array(
1250 'autosize' => (bool)$beUserAuth->uc['resizeTextareas_Flexible']
1251 );
1252 $pageRenderer->addInlineSettingArray('Textarea', $textareaSettings);
1253
1254 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.evalfield.js');
1255 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.tbe_editor.js');
1256 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ValueSlider');
1257 // Needed for FormEngine manipulation (date picker)
1258 $dateFormat = ($GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? array('MM-DD-YYYY', 'HH:mm MM-DD-YYYY') : array('DD-MM-YYYY', 'HH:mm DD-MM-YYYY'));
1259 $pageRenderer->addInlineSetting('DateTimePicker', 'DateFormat', $dateFormat);
1260
1261 // support placeholders for IE9 and lower
1262 $clientInfo = GeneralUtility::clientInfo();
1263 if ($clientInfo['BROWSER'] == 'msie' && $clientInfo['VERSION'] <= 9) {
1264 $this->loadJavascriptLib('sysext/core/Resources/Public/JavaScript/Contrib/placeholders.jquery.min.js');
1265 }
1266
1267 // @todo: remove scriptaclous once suggest & flex form foo is moved to RequireJS, see #55575
1268 $pageRenderer->loadScriptaculous();
1269 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.tceforms_suggest.js');
1270
1271 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Filelist/FileListLocalisation');
1272 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/DragUploader');
1273
1274 $pageRenderer->addInlineLanguagelabelFile(
1275 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('lang') . 'locallang_core.xlf',
1276 'file_upload'
1277 );
1278
1279 // We want to load jQuery-ui inside our js. Enable this using requirejs.
1280 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.inline.js');
1281 $out .= '
1282 inline.setNoTitleString("' . addslashes(BackendUtility::getNoRecordTitle(TRUE)) . '");
1283 ';
1284
1285 $out .= '
1286 TBE_EDITOR.formname = "' . $formname . '";
1287 TBE_EDITOR.formnameUENC = "' . rawurlencode($formname) . '";
1288 TBE_EDITOR.backPath = "";
1289 TBE_EDITOR.isPalettedoc = null;
1290 TBE_EDITOR.doSaveFieldName = "' . ($this->doSaveFieldName ? addslashes($this->doSaveFieldName) : '') . '";
1291 TBE_EDITOR.labels.fieldsChanged = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.fieldsChanged')) . ';
1292 TBE_EDITOR.labels.fieldsMissing = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.fieldsMissing')) . ';
1293 TBE_EDITOR.labels.maxItemsAllowed = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.maxItemsAllowed')) . ';
1294 TBE_EDITOR.labels.refresh_login = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:mess.refresh_login')) . ';
1295 TBE_EDITOR.labels.onChangeAlert = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:mess.onChangeAlert')) . ';
1296 TBE_EDITOR.labels.remainingCharacters = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.remainingCharacters')) . ';
1297 evalFunc.USmode = ' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '1' : '0') . ';
1298
1299 TBE_EDITOR.customEvalFunctions = {};
1300
1301 ';
1302 }
1303 // Add JS required for inline fields
1304 if (!empty($this->inlineData)) {
1305 $out .= '
1306 inline.addToDataArray(' . json_encode($this->inlineData) . ');
1307 ';
1308 }
1309 // $this->additionalJS_submit:
1310 if ($this->additionalJS_submit) {
1311 $additionalJS_submit = implode('', $this->additionalJS_submit);
1312 $additionalJS_submit = str_replace(array(CR, LF), '', $additionalJS_submit);
1313 $out .= '
1314 TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJS_submit) . '");
1315 ';
1316 }
1317 $out .= LF . implode(LF, $this->additionalJS_post) . LF . $this->extJSCODE;
1318 // Regular direct output:
1319 if (!$update) {
1320 $spacer = LF . TAB;
1321 $out = $spacer . implode($spacer, $jsFile) . GeneralUtility::wrapJS($out);
1322 }
1323 return $out;
1324 }
1325
1326 /**
1327 * Prints necessary JavaScript for TCEforms (after the form HTML).
1328 * currently this is used to transform page-specific options in the TYPO3.Settings array for JS
1329 * so the JS module can access these values
1330 *
1331 * @return string
1332 */
1333 public function printNeededJSFunctions() {
1334 /** @var $pageRenderer \TYPO3\CMS\Core\Page\PageRenderer */
1335 $pageRenderer = $this->getControllerDocumentTemplate()->getPageRenderer();
1336
1337 // set variables to be accessible for JS
1338 $pageRenderer->addInlineSetting('FormEngine', 'formName', 'editform');
1339 $pageRenderer->addInlineSetting('FormEngine', 'backPath', '');
1340
1341 // Integrate JS functions for the element browser if such fields or IRRE fields were processed
1342 $pageRenderer->addInlineSetting('FormEngine', 'legacyFieldChangedCb', 'function() { ' . $this->TBE_EDITOR_fieldChanged_func . ' };');
1343
1344 return $this->JSbottom('editform');
1345 }
1346
1347 /**
1348 * Returns necessary JavaScript for the top
1349 *
1350 * @return string
1351 */
1352 public function printNeededJSFunctions_top() {
1353 return $this->JStop('editform');
1354 }
1355
1356 /**
1357 * Includes a javascript library that exists in the core /typo3/ directory. The
1358 * backpath is automatically applied.
1359 * This method acts as wrapper for $GLOBALS['SOBE']->doc->loadJavascriptLib($lib).
1360 *
1361 * @param string $lib Library name. Call it with the full path like "contrib/prototype/prototype.js" to load it
1362 * @return void
1363 */
1364 public function loadJavascriptLib($lib) {
1365 $this->getControllerDocumentTemplate()->loadJavascriptLib($lib);
1366 }
1367
1368 /********************************************
1369 *
1370 * Various helper functions
1371 *
1372 ********************************************/
1373
1374 /**
1375 * Return record path (visually formatted, using BackendUtility::getRecordPath() )
1376 *
1377 * @param string $table Table name
1378 * @param array $rec Record array
1379 * @return string The record path.
1380 * @see BackendUtility::getRecordPath()
1381 */
1382 public function getRecordPath($table, $rec) {
1383 BackendUtility::fixVersioningPid($table, $rec);
1384 list($tscPID, $thePidValue) = BackendUtility::getTSCpidCached($table, $rec['uid'], $rec['pid']);
1385 if ($thePidValue >= 0) {
1386 return BackendUtility::getRecordPath($tscPID, $this->readPerms(), 15);
1387 }
1388 return '';
1389 }
1390
1391 /**
1392 * Returns the select-page read-access SQL clause.
1393 * Returns cached string, so you can call this function as much as you like without performance loss.
1394 *
1395 * @return string
1396 */
1397 public function readPerms() {
1398 if (!$this->perms_clause_set) {
1399 $this->perms_clause = $this->getBackendUserAuthentication()->getPagePermsClause(1);
1400 $this->perms_clause_set = TRUE;
1401 }
1402 return $this->perms_clause;
1403 }
1404
1405 /**
1406 * Returns TRUE if descriptions should be loaded always
1407 *
1408 * @param string $table Table for which to check
1409 * @return bool
1410 */
1411 public function doLoadTableDescr($table) {
1412 return $GLOBALS['TCA'][$table]['interface']['always_description'];
1413 }
1414
1415 /**
1416 * Initialize list of additional preview languages.
1417 * Sets according list in $this->additionalPreviewLanguages
1418 *
1419 * @return void
1420 */
1421 protected function initializeAdditionalPreviewLanguages() {
1422 $backendUserAuthentication = $this->getBackendUserAuthentication();
1423 $additionalPreviewLanguageListOfUser = $backendUserAuthentication->getTSConfigVal('options.additionalPreviewLanguages');
1424 $additionalPreviewLanguages = array();
1425 if ($additionalPreviewLanguageListOfUser) {
1426 $uids = GeneralUtility::intExplode(',', $additionalPreviewLanguageListOfUser);
1427 foreach ($uids as $uid) {
1428 if ($sys_language_rec = BackendUtility::getRecord('sys_language', $uid)) {
1429 $additionalPreviewLanguages[$uid]['uid'] = $uid;
1430 if (!empty($sys_language_rec['language_isocode'])) {
1431 $additionalPreviewLanguages[$uid]['ISOcode'] = $sys_language_rec['language_isocode'];
1432 } elseif ($sys_language_rec['static_lang_isocode'] && ExtensionManagementUtility::isLoaded('static_info_tables')) {
1433 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.');
1434 $staticLangRow = BackendUtility::getRecord('static_languages', $sys_language_rec['static_lang_isocode'], 'lg_iso_2');
1435 if ($staticLangRow['lg_iso_2']) {
1436 $additionalPreviewLanguages[$uid]['ISOcode'] = $staticLangRow['lg_iso_2'];
1437 }
1438 }
1439 }
1440 }
1441 }
1442 $this->additionalPreviewLanguages = $additionalPreviewLanguages;
1443 }
1444
1445 /**
1446 * @return BackendUserAuthentication
1447 */
1448 protected function getBackendUserAuthentication() {
1449 return $GLOBALS['BE_USER'];
1450 }
1451
1452 /**
1453 * @return DocumentTemplate
1454 */
1455 protected function getControllerDocumentTemplate() {
1456 // $GLOBALS['SOBE'] might be any kind of PHP class (controller most of the times)
1457 // These class do not inherit from any common class, but they all seem to have a "doc" member
1458 return $GLOBALS['SOBE']->doc;
1459 }
1460
1461 /**
1462 * @return LanguageService
1463 */
1464 protected function getLanguageService() {
1465 return $GLOBALS['LANG'];
1466 }
1467
1468 /**
1469 * Wrapper for access to the current page renderer object
1470 *
1471 * @return \TYPO3\CMS\Core\Page\PageRenderer
1472 */
1473 protected function getPageRenderer() {
1474 return $this->getControllerDocumentTemplate()->getPageRenderer();
1475 }
1476
1477 }