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