[TASK] BACK_PATH DocumentTemplate
[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 // prevent multiple include of the same files
403 if (!in_array($element, $this->additionalCode_pre)) {
404 $this->additionalCode_pre[] = $element;
405 }
406 }
407
408 if (!empty($resultArray['inlineData'])) {
409 $resultArrayInlineData = $this->inlineData;
410 $resultInlineData = $resultArray['inlineData'];
411 ArrayUtility::mergeRecursiveWithOverrule($resultArrayInlineData, $resultInlineData);
412 $this->inlineData = $resultArrayInlineData;
413 }
414 }
415
416 /**
417 * Returns an array of global form settings to be given to child elements.
418 *
419 * @return array
420 */
421 protected function getConfigurationOptionsForChildElements() {
422 return array(
423 'renderReadonly' => $this->renderReadonly,
424 'disabledWizards' => $this->disableWizards,
425 'returnUrl' => $this->thisReturnUrl(),
426 'table' => $this->table,
427 'databaseRow' => $this->databaseRow,
428 'recordTypeValue' => '',
429 'additionalPreviewLanguages' => $this->additionalPreviewLanguages,
430 'localizationMode' => $this->localizationMode, // @todo: find out the details, Warning, this overlaps with inline behaviour localizationMode
431 'elementBaseName' => '',
432 'tabAndInlineStack' => array(),
433 'inlineFirstPid' => $this->getInlineFirstPid(),
434 'inlineExpandCollapseStateArray' => $this->getInlineExpandCollapseStateArrayForTableUid($this->table, $this->databaseRow['uid']),
435 'inlineData' => $this->inlineData,
436 'inlineStructure' => $this->inlineStackProcessor->getStructure(),
437 'overruleTypesArray' => array(),
438 'hiddenFieldListArray' => $this->hiddenFieldListArr,
439 'flexFormFieldIdentifierPrefix' => 'ID',
440 'nodeFactory' => $this->nodeFactory,
441 );
442 }
443
444 /**
445 * General processor for AJAX requests concerning IRRE.
446 *
447 * @param array $_ Additional parameters (not used here)
448 * @param AjaxRequestHandler $ajaxObj The AjaxRequestHandler object of this request
449 * @throws \RuntimeException
450 * @return void
451 */
452 public function processInlineAjaxRequest($_, AjaxRequestHandler $ajaxObj) {
453 $ajaxArguments = GeneralUtility::_GP('ajax');
454 $ajaxIdParts = explode('::', $GLOBALS['ajaxID'], 2);
455 if (isset($ajaxArguments) && is_array($ajaxArguments) && !empty($ajaxArguments)) {
456 $ajaxMethod = $ajaxIdParts[1];
457 $ajaxObj->setContentFormat('jsonbody');
458 // Construct runtime environment for Inline Relational Record Editing
459 $this->setUpRuntimeEnvironmentForAjaxRequests();
460 // @todo: ajaxArguments[2] is "returnUrl" in the first 3 calls - still needed?
461 switch ($ajaxMethod) {
462 case 'synchronizeLocalizeRecords':
463 $domObjectId = $ajaxArguments[0];
464 $type = $ajaxArguments[1];
465 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
466 $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
467 $this->inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
468 $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId);
469 $ajaxObj->setContent($this->renderInlineSynchronizeLocalizeRecords($type, $inlineFirstPid));
470 break;
471 case 'createNewRecord':
472 $domObjectId = $ajaxArguments[0];
473 $createAfterUid = 0;
474 if (isset($ajaxArguments[1])) {
475 $createAfterUid = $ajaxArguments[1];
476 }
477 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
478 $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
479 $this->inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
480 $ajaxObj->setContent($this->renderInlineNewChildRecord($domObjectId, $createAfterUid));
481 break;
482 case 'getRecordDetails':
483 $domObjectId = $ajaxArguments[0];
484 // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
485 $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
486 $this->inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
487 $ajaxObj->setContent($this->renderInlineChildRecord($domObjectId));
488 break;
489 case 'setExpandedCollapsedState':
490 $domObjectId = $ajaxArguments[0];
491 // Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
492 $this->inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId, FALSE);
493 $expand = $ajaxArguments[1];
494 $collapse = $ajaxArguments[2];
495 $this->setInlineExpandedCollapsedState($expand, $collapse);
496 break;
497 default:
498 throw new \RuntimeException('Not a valid ajax identifier', 1428227862);
499 }
500 }
501 }
502
503 /**
504 * Handle AJAX calls to dynamically load the form fields of a given inline record.
505 *
506 * @param string $domObjectId The calling object in hierarchy, that requested a new record.
507 * @return array An array to be used for JSON
508 */
509 protected function renderInlineChildRecord($domObjectId) {
510 // The current table - for this table we should add/import records
511 $current = $this->inlineStackProcessor->getUnstableStructure();
512 // The parent table - this table embeds the current table
513 $parent = $this->inlineStackProcessor->getStructureLevel(-1);
514 $config = $parent['config'];
515
516 if (empty($config['foreign_table']) || !is_array($GLOBALS['TCA'][$config['foreign_table']])) {
517 return $this->getErrorMessageForAJAX('Wrong configuration in table ' . $parent['table']);
518 }
519
520 $config = FormEngineUtility::mergeInlineConfiguration($config);
521
522 // Set flag in config so that only the fields are rendered
523 $config['renderFieldsOnly'] = TRUE;
524 $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll'];
525 $expandSingle = isset($config['appearance']['expandSingle']) && $config['appearance']['expandSingle'];
526
527 $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
528 $record = $inlineRelatedRecordResolver->getRecord($current['table'], $current['uid']);
529
530 $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId);
531 // The HTML-object-id's prefix of the dynamically created record
532 $objectPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $current['table'];
533 $objectId = $objectPrefix . '-' . $record['uid'];
534
535 $options = $this->getConfigurationOptionsForChildElements();
536 $options['databaseRow'] = array('uid' => $parent['uid']);
537 $options['inlineFirstPid'] = $inlineFirstPid;
538 $options['inlineRelatedRecordToRender'] = $record;
539 $options['inlineRelatedRecordConfig'] = $config;
540 $options['inlineStructure'] = $this->inlineStackProcessor->getStructure();
541
542 $options['renderType'] = 'inlineRecordContainer';
543
544 try {
545 // Access to this record my be denied, create an according error message in this case
546 $childArray = $this->nodeFactory->create($options)->render();
547 } catch (AccessDeniedException $e) {
548 return $this->getErrorMessageForAJAX('Access denied');
549 }
550
551 $this->mergeResult($childArray);
552
553 $jsonArray = array(
554 'data' => $childArray['html'],
555 'scriptCall' => array(),
556 );
557 $jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
558 if ($config['foreign_unique']) {
559 $jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ');';
560 }
561
562 $jsonArray = $this->getInlineAjaxCommonScriptCalls($jsonArray, $config, $inlineFirstPid);
563
564 // Collapse all other records if requested:
565 if (!$collapseAll && $expandSingle) {
566 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ');';
567 }
568
569 return $jsonArray;
570 }
571
572 /**
573 * Handle AJAX calls to show a new inline-record of the given table.
574 *
575 * @param string $domObjectId The calling object in hierarchy, that requested a new record.
576 * @param string|int $foreignUid If set, the new record should be inserted after that one.
577 * @return array An array to be used for JSON
578 */
579 protected function renderInlineNewChildRecord($domObjectId, $foreignUid) {
580 // The current table - for this table we should add/import records
581 $current = $this->inlineStackProcessor->getUnstableStructure();
582 // The parent table - this table embeds the current table
583 $parent = $this->inlineStackProcessor->getStructureLevel(-1);
584 $config = $parent['config'];
585
586 if (empty($config['foreign_table']) || !is_array($GLOBALS['TCA'][$config['foreign_table']])) {
587 return $this->getErrorMessageForAJAX('Wrong configuration in table ' . $parent['table']);
588 }
589
590 $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
591
592 $config = FormEngineUtility::mergeInlineConfiguration($config);
593
594 $collapseAll = isset($config['appearance']['collapseAll']) && $config['appearance']['collapseAll'];
595 $expandSingle = isset($config['appearance']['expandSingle']) && $config['appearance']['expandSingle'];
596
597 $inlineFirstPid = FormEngineUtility::getInlineFirstPidFromDomObjectId($domObjectId);
598
599 // Dynamically create a new record using \TYPO3\CMS\Backend\Form\DataPreprocessor
600 if (!$foreignUid || !MathUtility::canBeInterpretedAsInteger($foreignUid) || $config['foreign_selector']) {
601 $record = $inlineRelatedRecordResolver->getNewRecord($inlineFirstPid, $current['table']);
602 // Set default values for new created records
603 if (isset($config['foreign_record_defaults']) && is_array($config['foreign_record_defaults'])) {
604 $foreignTableConfig = $GLOBALS['TCA'][$current['table']];
605 // The following system relevant fields can't be set by foreign_record_defaults
606 $notSettableFields = array(
607 'uid', 'pid', 't3ver_oid', 't3ver_id', 't3ver_label', 't3ver_wsid', 't3ver_state', 't3ver_stage',
608 't3ver_count', 't3ver_tstamp', 't3ver_move_id'
609 );
610 $configurationKeysForNotSettableFields = array(
611 'crdate', 'cruser_id', 'delete', 'origUid', 'transOrigDiffSourceField', 'transOrigPointerField',
612 'tstamp'
613 );
614 foreach ($configurationKeysForNotSettableFields as $configurationKey) {
615 if (isset($foreignTableConfig['ctrl'][$configurationKey])) {
616 $notSettableFields[] = $foreignTableConfig['ctrl'][$configurationKey];
617 }
618 }
619 foreach ($config['foreign_record_defaults'] as $fieldName => $defaultValue) {
620 if (isset($foreignTableConfig['columns'][$fieldName]) && !in_array($fieldName, $notSettableFields)) {
621 $record[$fieldName] = $defaultValue;
622 }
623 }
624 }
625 // Set language of new child record to the language of the parent record:
626 if ($parent['localizationMode'] === 'select') {
627 $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
628 $parentLanguageField = $GLOBALS['TCA'][$parent['table']]['ctrl']['languageField'];
629 $childLanguageField = $GLOBALS['TCA'][$current['table']]['ctrl']['languageField'];
630 if ($parentRecord[$parentLanguageField] > 0) {
631 $record[$childLanguageField] = $parentRecord[$parentLanguageField];
632 }
633 }
634 } else {
635 // @todo: Check this: Else also hits if $foreignUid = 0?
636 $record = $inlineRelatedRecordResolver->getRecord($current['table'], $foreignUid);
637 }
638 // Now there is a foreign_selector, so there is a new record on the intermediate table, but
639 // this intermediate table holds a field, which is responsible for the foreign_selector, so
640 // we have to set this field to the uid we get - or if none, to a new uid
641 if ($config['foreign_selector'] && $foreignUid) {
642 $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $config['foreign_selector']);
643 // For a selector of type group/db, prepend the tablename (<tablename>_<uid>):
644 $record[$config['foreign_selector']] = $selConfig['type'] != 'groupdb' ? '' : $selConfig['table'] . '_';
645 $record[$config['foreign_selector']] .= $foreignUid;
646 if ($selConfig['table'] === 'sys_file') {
647 $fileRecord = $inlineRelatedRecordResolver->getRecord($selConfig['table'], $foreignUid);
648 if ($fileRecord !== FALSE && !$this->checkInlineFileTypeAccessForField($selConfig, $fileRecord)) {
649 return $this->getErrorMessageForAJAX('File extension ' . $fileRecord['extension'] . ' is not allowed here!');
650 }
651 }
652 }
653 // The HTML-object-id's prefix of the dynamically created record
654 $objectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
655 $objectPrefix = $objectName . '-' . $current['table'];
656 $objectId = $objectPrefix . '-' . $record['uid'];
657
658 $options = $this->getConfigurationOptionsForChildElements();
659 $options['databaseRow'] = array('uid' => $parent['uid']);
660 $options['inlineFirstPid'] = $inlineFirstPid;
661 $options['inlineRelatedRecordToRender'] = $record;
662 $options['inlineRelatedRecordConfig'] = $config;
663 $options['inlineStructure'] = $this->inlineStackProcessor->getStructure();
664
665 $options['renderType'] = 'inlineRecordContainer';
666
667 try {
668 // Access to this record my be denied, create an according error message in this case
669 $childArray = $this->nodeFactory->create($options)->render();
670 } catch (AccessDeniedException $e) {
671 return $this->getErrorMessageForAJAX('Access denied');
672 }
673
674 $this->mergeResult($childArray);
675
676 $jsonArray = array(
677 'data' => $childArray['html'],
678 'scriptCall' => array(),
679 );
680
681 if (!$current['uid']) {
682 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
683 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ',null,' . GeneralUtility::quoteJSvalue($foreignUid) . ');';
684 } else {
685 $jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
686 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($record['uid']) . ',' . GeneralUtility::quoteJSvalue($current['uid']) . ',' . GeneralUtility::quoteJSvalue($foreignUid) . ');';
687 }
688
689 $jsonArray = $this->getInlineAjaxCommonScriptCalls($jsonArray, $config, $inlineFirstPid);
690
691 // Collapse all other records if requested:
692 if (!$collapseAll && $expandSingle) {
693 $jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ', ' . GeneralUtility::quoteJSvalue($objectPrefix) . ', ' . GeneralUtility::quoteJSvalue($record['uid']) . ');';
694 }
695 // Fade out and fade in the new record in the browser view to catch the user's eye
696 $jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
697
698 return $jsonArray;
699 }
700
701 /**
702 * Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent.
703 *
704 * @param string $type Defines the type 'localize' or 'synchronize' (string) or a single uid to be localized (int)
705 * @param int $inlineFirstPid Inline first pid
706 * @return array An array to be used for JSON
707 */
708 protected function renderInlineSynchronizeLocalizeRecords($type, $inlineFirstPid) {
709 $jsonArray = FALSE;
710 if (GeneralUtility::inList('localize,synchronize', $type) || MathUtility::canBeInterpretedAsInteger($type)) {
711 $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
712 // The parent level:
713 $parent = $this->inlineStackProcessor->getStructureLevel(-1);
714 $current = $this->inlineStackProcessor->getUnstableStructure();
715 $parentRecord = $inlineRelatedRecordResolver->getRecord($parent['table'], $parent['uid']);
716
717 $cmd = array();
718 $cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = $parent['field'] . ',' . $type;
719 /** @var $tce \TYPO3\CMS\Core\DataHandling\DataHandler */
720 $tce = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
721 $tce->stripslashes_values = FALSE;
722 $tce->start(array(), $cmd);
723 $tce->process_cmdmap();
724
725 $oldItemList = $parentRecord[$parent['field']];
726 $newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parent['field']];
727
728 $jsonArray = array(
729 'scriptCall' => array(),
730 );
731 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
732 $nameObjectForeignTable = $nameObject . '-' . $current['table'];
733 // Get the name of the field pointing to the original record:
734 $transOrigPointerField = $GLOBALS['TCA'][$current['table']]['ctrl']['transOrigPointerField'];
735 // Get the name of the field used as foreign selector (if any):
736 $foreignSelector = isset($parent['config']['foreign_selector']) && $parent['config']['foreign_selector'] ? $parent['config']['foreign_selector'] : FALSE;
737 // Convert lists to array with uids of child records:
738 $oldItems = FormEngineUtility::getInlineRelatedRecordsUidArray($oldItemList);
739 $newItems = FormEngineUtility::getInlineRelatedRecordsUidArray($newItemList);
740 // Determine the items that were localized or localized:
741 $removedItems = array_diff($oldItems, $newItems);
742 $localizedItems = array_diff($newItems, $oldItems);
743 // Set the items that should be removed in the forms view:
744 foreach ($removedItems as $item) {
745 $jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $item) . ', {forceDirectRemoval: true});';
746 }
747 // Set the items that should be added in the forms view:
748 $html = '';
749 $resultArray = NULL;
750 // @todo: This should be another container ...
751 foreach ($localizedItems as $item) {
752 $row = $inlineRelatedRecordResolver->getRecord($current['table'], $item);
753 $selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($row[$foreignSelector]) : 'null';
754
755 $options = $this->getConfigurationOptionsForChildElements();
756 $options['databaseRow'] = array('uid' => $parent['uid']);
757 $options['inlineFirstPid'] = $inlineFirstPid;
758 $options['inlineRelatedRecordToRender'] = $row;
759 $options['inlineRelatedRecordConfig'] = $parent['config'];
760 $options['inlineStructure'] = $this->inlineStackProcessor->getStructure();
761
762 $options['renderType'] = 'inlineRecordContainer';
763 try {
764 // Access to this record my be denied, create an according error message in this case
765 $childArray = $this->nodeFactory->create($options)->render();
766 } catch (AccessDeniedException $e) {
767 return $this->getErrorMessageForAJAX('Access denied');
768 }
769
770 $html .= $childArray['html'];
771 $childArray['html'] = '';
772
773 // @todo: Obsolete if a container and copied from AbstractContainer for now
774 if ($resultArray === NULL) {
775 $resultArray = $childArray;
776 } else {
777 if (!empty($childArray['extJSCODE'])) {
778 $resultArray['extJSCODE'] .= LF . $childArray['extJSCODE'];
779 }
780 foreach ($childArray['additionalJavaScriptPost'] as $value) {
781 $resultArray['additionalJavaScriptPost'][] = $value;
782 }
783 foreach ($childArray['additionalJavaScriptSubmit'] as $value) {
784 $resultArray['additionalJavaScriptSubmit'][] = $value;
785 }
786 if (!empty($childArray['inlineData'])) {
787 $resultArrayInlineData = $resultArray['inlineData'];
788 $childInlineData = $childArray['inlineData'];
789 ArrayUtility::mergeRecursiveWithOverrule($resultArrayInlineData, $childInlineData);
790 $resultArray['inlineData'] = $resultArrayInlineData;
791 }
792 }
793
794 $jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($item) . ', null, ' . $selectedValue . ');';
795 // Remove possible virtual records in the form which showed that a child records could be localized:
796 if (isset($row[$transOrigPointerField]) && $row[$transOrigPointerField]) {
797 $jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $row[$transOrigPointerField] . '_div') . ');';
798 }
799 }
800 if (!empty($html)) {
801 $jsonArray['data'] = $html;
802 array_unshift($jsonArray['scriptCall'], 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records') . ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', json.data);');
803 }
804
805 $this->mergeResult($resultArray);
806
807 $jsonArray = $this->getInlineAjaxCommonScriptCalls($jsonArray, $parent['config'], $inlineFirstPid);
808 }
809 return $jsonArray;
810 }
811
812 /**
813 * Save the expanded/collapsed state of a child record in the BE_USER->uc.
814 *
815 * @param string $expand Whether this record is expanded.
816 * @param string $collapse Whether this record is collapsed.
817 * @return void
818 */
819 protected function setInlineExpandedCollapsedState($expand, $collapse) {
820 $backendUser = $this->getBackendUserAuthentication();
821 // The current table - for this table we should add/import records
822 $currentTable = $this->inlineStackProcessor->getUnstableStructure();
823 $currentTable = $currentTable['table'];
824 // The top parent table - this table embeds the current table
825 $top = $this->inlineStackProcessor->getStructureLevel(0);
826 $topTable = $top['table'];
827 $topUid = $top['uid'];
828 $inlineView = $this->getInlineExpandCollapseStateArray();
829 // Only do some action if the top record and the current record were saved before
830 if (MathUtility::canBeInterpretedAsInteger($topUid)) {
831 $expandUids = GeneralUtility::trimExplode(',', $expand);
832 $collapseUids = GeneralUtility::trimExplode(',', $collapse);
833 // Set records to be expanded
834 foreach ($expandUids as $uid) {
835 $inlineView[$topTable][$topUid][$currentTable][] = $uid;
836 }
837 // Set records to be collapsed
838 foreach ($collapseUids as $uid) {
839 $inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
840 }
841 // Save states back to database
842 if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
843 $inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
844 $backendUser->uc['inlineView'] = serialize($inlineView);
845 $backendUser->writeUC();
846 }
847 }
848 }
849
850 /**
851 * Construct runtime environment for Inline Relational Record Editing.
852 * - creates an anonymous \TYPO3\CMS\Backend\Controller\EditDocumentController in $GLOBALS['SOBE']
853 * - sets $this to $GLOBALS['SOBE']->tceforms
854 *
855 * @return void
856 */
857 protected function setUpRuntimeEnvironmentForAjaxRequests() {
858 $this->getLanguageService()->includeLLFile('EXT:lang/locallang_alt_doc.xlf');
859 // Create a new anonymous object:
860 $GLOBALS['SOBE'] = new \stdClass();
861 $GLOBALS['SOBE']->MOD_MENU = array();
862 // Setting virtual document name
863 $GLOBALS['SOBE']->MCONF['name'] = 'xMOD_alt_doc.php';
864 // CLEANSE SETTINGS
865 $GLOBALS['SOBE']->MOD_SETTINGS = array();
866 // Create an instance of the document template object
867 // @todo: resolve clash getDocumentTemplate() / getControllerDocumenttemplate()
868 $GLOBALS['SOBE']->doc = GeneralUtility::makeInstance(DocumentTemplate::class);
869 // Initialize FormEngine (rendering the forms)
870 // @todo: check if this is still needed, simplify
871 $GLOBALS['SOBE']->tceforms = $this;
872 }
873
874 /**
875 * Return expand / collapse state array for a given table / uid combination
876 *
877 * @param string $table Handled table
878 * @param int $uid Handled uid
879 * @return array
880 */
881 protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid) {
882 $inlineView = $this->getInlineExpandCollapseStateArray();
883 $result = array();
884 if (MathUtility::canBeInterpretedAsInteger($uid)) {
885 if (!empty($inlineView[$table][$uid])) {
886 $result = $inlineView[$table][$uid];
887 }
888 }
889 return $result;
890 }
891
892 /**
893 * Get expand / collapse state of inline items
894 *
895 * @return array
896 */
897 protected function getInlineExpandCollapseStateArray() {
898 $backendUser = $this->getBackendUserAuthentication();
899 $inlineView = unserialize($backendUser->uc['inlineView']);
900 if (!is_array($inlineView)) {
901 $inlineView = array();
902 }
903 return $inlineView;
904 }
905
906 /**
907 * The "entry" pid for inline records. Nested inline records can potentially hang around on different
908 * 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.
909 *
910 * @return int
911 */
912 protected function getInlineFirstPid() {
913 $table = $this->table;
914 $row = $this->databaseRow;
915 // If the parent is a page, use the uid(!) of the (new?) page as pid for the child records:
916 if ($table == 'pages') {
917 $liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']);
918 $pid = is_null($liveVersionId) ? $row['uid'] : $liveVersionId;
919 } elseif ($row['pid'] < 0) {
920 $prevRec = BackendUtility::getRecord($table, abs($row['pid']));
921 $pid = $prevRec['pid'];
922 } else {
923 $pid = $row['pid'];
924 }
925 return $pid;
926 }
927
928 /**
929 * Checks if a record selector may select a certain file type
930 *
931 * @param array $selectorConfiguration
932 * @param array $fileRecord
933 * @return bool
934 */
935 protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord) {
936 if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
937 $allowedFileExtensions = GeneralUtility::trimExplode(
938 ',',
939 $selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'],
940 TRUE
941 );
942 if (!in_array(strtolower($fileRecord['extension']), $allowedFileExtensions, TRUE)) {
943 return FALSE;
944 }
945 }
946 return TRUE;
947 }
948
949 /**
950 * Remove an element from an array.
951 *
952 * @param mixed $needle The element to be removed.
953 * @param array $haystack The array the element should be removed from.
954 * @param mixed $strict Search elements strictly.
955 * @return array The array $haystack without the $needle
956 */
957 protected function removeFromArray($needle, $haystack, $strict = NULL) {
958 $pos = array_search($needle, $haystack, $strict);
959 if ($pos !== FALSE) {
960 unset($haystack[$pos]);
961 }
962 return $haystack;
963 }
964
965 /**
966 * Generates an error message that transferred as JSON for AJAX calls
967 *
968 * @param string $message The error message to be shown
969 * @return array The error message in a JSON array
970 */
971 protected function getErrorMessageForAJAX($message) {
972 $jsonArray = array(
973 'data' => $message,
974 'scriptCall' => array(
975 'alert("' . $message . '");'
976 )
977 );
978 return $jsonArray;
979 }
980
981 /**
982 * Determines and sets several script calls to a JSON array, that would have been executed if processed in non-AJAX mode.
983 *
984 * @param array &$jsonArray Reference of the array to be used for JSON
985 * @param array $config The configuration of the IRRE field of the parent record
986 * @param int $inlineFirstPid Inline first pid
987 * @return array Modified array
988 * @todo: Basically, this methods shouldn't be there at all ...
989 */
990 protected function getInlineAjaxCommonScriptCalls($jsonArray, $config, $inlineFirstPid) {
991 // Add data that would have been added at the top of a regular FormEngine call:
992 if ($headTags = $this->getInlineHeadTags()) {
993 $jsonArray['headData'] = $headTags;
994 }
995 // Add the JavaScript data that would have been added at the bottom of a regular FormEngine call:
996 $jsonArray['scriptCall'][] = $this->JSbottom('editform', TRUE);
997 // If script.aculo.us Sortable is used, update the Observer to know the record:
998 if ($config['appearance']['useSortable']) {
999 $inlineObjectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
1000 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
1001 }
1002 // If FormEngine has some JavaScript code to be executed, just do it
1003 // @todo: this is done by JSBottom() already?!
1004 if ($this->extJSCODE) {
1005 $jsonArray['scriptCall'][] = $this->extJSCODE;
1006 }
1007
1008 // require js handling
1009 foreach ($this->requireJsModules as $moduleName => $callbacks) {
1010 if (!is_array($callbacks)) {
1011 $callbacks = array($callbacks);
1012 }
1013 foreach ($callbacks as $callback) {
1014 $inlineCodeKey = $moduleName;
1015 $javaScriptCode = 'require(["' . $moduleName . '"]';
1016 if ($callback !== NULL) {
1017 $inlineCodeKey .= sha1($callback);
1018 $javaScriptCode .= ', ' . $callback;
1019 }
1020 $javaScriptCode .= ');';
1021 $jsonArray['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode;
1022 }
1023 }
1024 return $jsonArray;
1025 }
1026
1027 /**
1028 * Parses the HTML tags that would have been inserted to the <head> of a HTML document and returns the found tags as multidimensional array.
1029 *
1030 * @return array The parsed tags with their attributes and innerHTML parts
1031 * @todo: WTF?
1032 */
1033 protected function getInlineHeadTags() {
1034 $headTags = array();
1035 $headDataRaw = $this->JStop() . $this->getJavaScriptOfPageRenderer();
1036 if ($headDataRaw) {
1037 // Create instance of the HTML parser:
1038 $parseObj = GeneralUtility::makeInstance(HtmlParser::class);
1039 // Removes script wraps:
1040 $headDataRaw = str_replace(array('/*<![CDATA[*/', '/*]]>*/'), '', $headDataRaw);
1041 // Removes leading spaces of a multi-line string:
1042 $headDataRaw = trim(preg_replace('/(^|\\r|\\n)( |\\t)+/', '$1', $headDataRaw));
1043 // Get script and link tags:
1044 $tags = array_merge(
1045 $parseObj->getAllParts($parseObj->splitTags('link', $headDataRaw)),
1046 $parseObj->getAllParts($parseObj->splitIntoBlock('script', $headDataRaw))
1047 );
1048 foreach ($tags as $tagData) {
1049 $tagAttributes = $parseObj->get_tag_attributes($parseObj->getFirstTag($tagData), TRUE);
1050 $headTags[] = array(
1051 'name' => $parseObj->getFirstTagName($tagData),
1052 'attributes' => $tagAttributes[0],
1053 'innerHTML' => $parseObj->removeFirstAndLastTag($tagData)
1054 );
1055 }
1056 }
1057 return $headTags;
1058 }
1059
1060 /**
1061 * Gets the JavaScript of the pageRenderer.
1062 * This can be used to extract newly added files which have been added
1063 * during an AJAX request. Due to the spread possibilities of the pageRenderer
1064 * to add JavaScript rendering and extracting seems to be the easiest way.
1065 *
1066 * @return string
1067 * @todo: aaaargs ...
1068 */
1069 protected function getJavaScriptOfPageRenderer() {
1070 /** @var $pageRenderer PageRenderer */
1071 $pageRenderer = clone $this->getPageRenderer();
1072 $pageRenderer->setCharSet($this->getLanguageService()->charSet);
1073 $pageRenderer->setTemplateFile('EXT:backend/Resources/Private/Templates/helper_javascript_css.html');
1074 return $pageRenderer->render();
1075 }
1076
1077 /**
1078 * Returns the "returnUrl" of the form. Can be set externally or will be taken from "GeneralUtility::linkThisScript()"
1079 *
1080 * @return string Return URL of current script
1081 */
1082 protected function thisReturnUrl() {
1083 return $this->returnUrl ? $this->returnUrl : GeneralUtility::linkThisScript();
1084 }
1085
1086 /********************************************
1087 *
1088 * Template functions
1089 *
1090 ********************************************/
1091 /**
1092 * Wraps all the table rows into a single table.
1093 * Used externally from scripts like EditDocumentController and PageLayoutController (which uses FormEngine)
1094 *
1095 * @param string $c Code to output between table-parts; table rows
1096 * @param array $rec The record
1097 * @param string $table The table name
1098 * @return string
1099 */
1100 public function wrapTotal($c, $rec, $table) {
1101 $parts = $this->replaceTableWrap(explode('|', $this->totalWrap, 2), $rec, $table);
1102 return $parts[0] . $c . $parts[1] . implode(LF, $this->hiddenFieldAccum);
1103 }
1104
1105 /**
1106 * Generates a token and returns an input field with it
1107 *
1108 * @param string $formName Context of the token
1109 * @param string $tokenName The name of the token GET/POST variable
1110 * @return string A complete input field
1111 */
1112 static public function getHiddenTokenField($formName = 'securityToken', $tokenName = 'formToken') {
1113 $formprotection = FormProtectionFactory::get();
1114 return '<input type="hidden" name="' . $tokenName . '" value="' . $formprotection->generateToken($formName) . '" />';
1115 }
1116
1117 /**
1118 * This replaces markers in the total wrap
1119 *
1120 * @param array $arr An array of template parts containing some markers.
1121 * @param array $rec The record
1122 * @param string $table The table name
1123 * @return string
1124 */
1125 public function replaceTableWrap($arr, $rec, $table) {
1126 $icon = IconUtility::getSpriteIconForRecord($table, $rec, array('title' => $this->getRecordPath($table, $rec)));
1127 // Make "new"-label
1128 $languageService = $this->getLanguageService();
1129 if (strstr($rec['uid'], 'NEW')) {
1130 $newLabel = ' <span class="typo3-TCEforms-newToken">' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.new', TRUE) . '</span>';
1131 // BackendUtility::fixVersioningPid Should not be used here because NEW records are not offline workspace versions...
1132 $truePid = BackendUtility::getTSconfig_pidValue($table, $rec['uid'], $rec['pid']);
1133 $prec = BackendUtility::getRecordWSOL('pages', $truePid, 'title');
1134 $pageTitle = BackendUtility::getRecordTitle('pages', $prec, TRUE, FALSE);
1135 $rLabel = '<em>[PID: ' . $truePid . '] ' . $pageTitle . '</em>';
1136 // Fetch translated title of the table
1137 $tableTitle = $languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
1138 if ($table === 'pages') {
1139 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.createNewPage', TRUE);
1140 $pageTitle = sprintf($label, $tableTitle);
1141 } else {
1142 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.createNewRecord', TRUE);
1143 if ($rec['pid'] == 0) {
1144 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.createNewRecordRootLevel', TRUE);
1145 }
1146 $pageTitle = sprintf($label, $tableTitle, $pageTitle);
1147 }
1148 } else {
1149 $newLabel = ' <span class="typo3-TCEforms-recUid">[' . $rec['uid'] . ']</span>';
1150 $rLabel = BackendUtility::getRecordTitle($table, $rec, TRUE, FALSE);
1151 $prec = BackendUtility::getRecordWSOL('pages', $rec['pid'], 'uid,title');
1152 // Fetch translated title of the table
1153 $tableTitle = $languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
1154 if ($table === 'pages') {
1155 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editPage', TRUE);
1156 // Just take the record title and prepend an edit label.
1157 $pageTitle = sprintf($label, $tableTitle, $rLabel);
1158 } else {
1159 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecord', TRUE);
1160 $pageTitle = BackendUtility::getRecordTitle('pages', $prec, TRUE, FALSE);
1161 if ($rLabel === BackendUtility::getNoRecordTitle(TRUE)) {
1162 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecordNoTitle', TRUE);
1163 }
1164 if ($rec['pid'] == 0) {
1165 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecordRootLevel', TRUE);
1166 }
1167 if ($rLabel !== BackendUtility::getNoRecordTitle(TRUE)) {
1168 // Just take the record title and prepend an edit label.
1169 $pageTitle = sprintf($label, $tableTitle, $rLabel, $pageTitle);
1170 } else {
1171 // Leave out the record title since it is not set.
1172 $pageTitle = sprintf($label, $tableTitle, $pageTitle);
1173 }
1174 }
1175 $icon = $this->getControllerDocumentTemplate()->wrapClickMenuOnIcon($icon, $table, $rec['uid'], 1, '', '+copy,info,edit,view');
1176 }
1177 foreach ($arr as $k => $v) {
1178 // Make substitutions:
1179 $arr[$k] = str_replace(
1180 array(
1181 '###PAGE_TITLE###',
1182 '###ID_NEW_INDICATOR###',
1183 '###RECORD_LABEL###',
1184 '###TABLE_TITLE###',
1185 '###RECORD_ICON###'
1186 ),
1187 array(
1188 $pageTitle,
1189 $newLabel,
1190 $rLabel,
1191 htmlspecialchars($languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title'])),
1192 $icon
1193 ),
1194 $arr[$k]
1195 );
1196 }
1197 return $arr;
1198 }
1199
1200
1201 /********************************************
1202 *
1203 * JavaScript related functions
1204 *
1205 ********************************************/
1206 /**
1207 * JavaScript code added BEFORE the form is drawn:
1208 *
1209 * @return string A <script></script> section with JavaScript.
1210 */
1211 public function JStop() {
1212 $out = '';
1213 if (!empty($this->additionalCode_pre)) {
1214 $out = implode(LF, $this->additionalCode_pre) . LF;
1215 }
1216 return $out;
1217 }
1218
1219 /**
1220 * JavaScript bottom code
1221 *
1222 * @param string $formname The identification of the form on the page.
1223 * @param bool $update Just extend/update existing settings, e.g. for AJAX call
1224 * @return string A section with JavaScript - if $update is FALSE, embedded in <script></script>
1225 */
1226 public function JSbottom($formname = 'forms[0]', $update = FALSE) {
1227 $languageService = $this->getLanguageService();
1228 $jsFile = array();
1229 $out = '';
1230 $this->TBE_EDITOR_fieldChanged_func = 'TBE_EDITOR.fieldChanged_fName(fName,formObj[fName+"_list"]);';
1231 if (!$update) {
1232 if ($this->loadMD5_JS) {
1233 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/md5.js');
1234 }
1235 // load the main module for FormEngine with all important JS functions
1236 $this->requireJsModules['TYPO3/CMS/Backend/FormEngine'] = 'function(FormEngine) {
1237 FormEngine.setBrowserUrl(' . GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('browser')) . ');
1238 }';
1239 $this->requireJsModules['TYPO3/CMS/Backend/FormEngineValidation'] = 'function(FormEngineValidation) {
1240 FormEngineValidation.setUsMode(' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '1' : '0') . ');
1241 FormEngineValidation.registerReady();
1242 }';
1243
1244 $pageRenderer = $this->getPageRenderer();
1245 foreach ($this->requireJsModules as $moduleName => $callbacks) {
1246 if (!is_array($callbacks)) {
1247 $callbacks = array($callbacks);
1248 }
1249 foreach ($callbacks as $callback) {
1250 $pageRenderer->loadRequireJsModule($moduleName, $callback);
1251 }
1252 }
1253 $pageRenderer->loadJquery();
1254 $pageRenderer->loadExtJS();
1255 // Load tree stuff here
1256 $pageRenderer->addJsFile('sysext/backend/Resources/Public/JavaScript/tree.js');
1257 $pageRenderer->addInlineLanguageLabelFile(ExtensionManagementUtility::extPath('lang') . 'locallang_csh_corebe.xlf', 'tcatree');
1258 $pageRenderer->addJsFile('sysext/backend/Resources/Public/JavaScript/notifications.js');
1259 if (ExtensionManagementUtility::isLoaded('rtehtmlarea')) {
1260 // This js addition is hackish ... it will always load this file even if not RTE
1261 // is added here. But this simplifies RTE initialization a lot and is thus kept for now.
1262 $pageRenderer->addJsFile('sysext/rtehtmlarea/Resources/Public/JavaScript/HTMLArea/NameSpace/NameSpace.js');
1263 }
1264
1265 $beUserAuth = $this->getBackendUserAuthentication();
1266 // Make textareas resizable and flexible ("autogrow" in height)
1267 $textareaSettings = array(
1268 'autosize' => (bool)$beUserAuth->uc['resizeTextareas_Flexible']
1269 );
1270 $pageRenderer->addInlineSettingArray('Textarea', $textareaSettings);
1271
1272 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.tbe_editor.js');
1273 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ValueSlider');
1274 // Needed for FormEngine manipulation (date picker)
1275 $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'));
1276 $pageRenderer->addInlineSetting('DateTimePicker', 'DateFormat', $dateFormat);
1277
1278 // support placeholders for IE9 and lower
1279 $clientInfo = GeneralUtility::clientInfo();
1280 if ($clientInfo['BROWSER'] == 'msie' && $clientInfo['VERSION'] <= 9) {
1281 $this->loadJavascriptLib('sysext/core/Resources/Public/JavaScript/Contrib/placeholders.jquery.min.js');
1282 }
1283
1284 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Filelist/FileListLocalisation');
1285 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/DragUploader');
1286
1287 $pageRenderer->addInlineLanguagelabelFile(
1288 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('lang') . 'locallang_core.xlf',
1289 'file_upload'
1290 );
1291 // Load codemirror for T3Editor
1292 if (ExtensionManagementUtility::isLoaded('t3editor')) {
1293 $this->loadJavascriptLib('sysext/t3editor/Resources/Public/JavaScript/Contrib/codemirror/js/codemirror.js');
1294 }
1295 // We want to load jQuery-ui inside our js. Enable this using requirejs.
1296 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.inline.js');
1297 $out .= '
1298 inline.setNoTitleString("' . addslashes(BackendUtility::getNoRecordTitle(TRUE)) . '");
1299 ';
1300
1301 $out .= '
1302 TBE_EDITOR.formname = "' . $formname . '";
1303 TBE_EDITOR.formnameUENC = "' . rawurlencode($formname) . '";
1304 TBE_EDITOR.backPath = "";
1305 TBE_EDITOR.isPalettedoc = null;
1306 TBE_EDITOR.doSaveFieldName = "' . ($this->doSaveFieldName ? addslashes($this->doSaveFieldName) : '') . '";
1307 TBE_EDITOR.labels.fieldsChanged = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.fieldsChanged')) . ';
1308 TBE_EDITOR.labels.fieldsMissing = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.fieldsMissing')) . ';
1309 TBE_EDITOR.labels.maxItemsAllowed = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.maxItemsAllowed')) . ';
1310 TBE_EDITOR.labels.refresh_login = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:mess.refresh_login')) . ';
1311 TBE_EDITOR.labels.onChangeAlert = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:mess.onChangeAlert')) . ';
1312 TBE_EDITOR.labels.remainingCharacters = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.remainingCharacters')) . ';
1313 TBE_EDITOR.customEvalFunctions = {};
1314
1315 ';
1316 }
1317 // Add JS required for inline fields
1318 if (!empty($this->inlineData)) {
1319 $out .= '
1320 inline.addToDataArray(' . json_encode($this->inlineData) . ');
1321 ';
1322 }
1323 // $this->additionalJS_submit:
1324 if ($this->additionalJS_submit) {
1325 $additionalJS_submit = implode('', $this->additionalJS_submit);
1326 $additionalJS_submit = str_replace(array(CR, LF), '', $additionalJS_submit);
1327 $out .= '
1328 TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJS_submit) . '");
1329 ';
1330 }
1331 $out .= LF . implode(LF, $this->additionalJS_post) . LF . $this->extJSCODE;
1332 // Regular direct output:
1333 if (!$update) {
1334 $spacer = LF . TAB;
1335 $out = $spacer . implode($spacer, $jsFile) . GeneralUtility::wrapJS($out);
1336 }
1337 return $out;
1338 }
1339
1340 /**
1341 * Prints necessary JavaScript for TCEforms (after the form HTML).
1342 * currently this is used to transform page-specific options in the TYPO3.Settings array for JS
1343 * so the JS module can access these values
1344 *
1345 * @return string
1346 */
1347 public function printNeededJSFunctions() {
1348 // set variables to be accessible for JS
1349 $pageRenderer = $this->getPageRenderer();
1350 $pageRenderer->addInlineSetting('FormEngine', 'formName', 'editform');
1351 $pageRenderer->addInlineSetting('FormEngine', 'backPath', '');
1352
1353 // Integrate JS functions for the element browser if such fields or IRRE fields were processed
1354 $pageRenderer->addInlineSetting('FormEngine', 'legacyFieldChangedCb', 'function() { ' . $this->TBE_EDITOR_fieldChanged_func . ' };');
1355
1356 return $this->JSbottom('editform');
1357 }
1358
1359 /**
1360 * Returns necessary JavaScript for the top
1361 *
1362 * @return string
1363 */
1364 public function printNeededJSFunctions_top() {
1365 return $this->JStop('editform');
1366 }
1367
1368 /**
1369 * Includes a javascript library that exists in the core /typo3/ directory. The
1370 * backpath is automatically applied.
1371 * This method acts as wrapper for $GLOBALS['SOBE']->doc->loadJavascriptLib($lib).
1372 *
1373 * @param string $lib Library name. Call it with the full path like "sysext/core/Resources/Public/JavaScript/QueryGenerator.js" to load it
1374 * @return void
1375 */
1376 public function loadJavascriptLib($lib) {
1377 $this->getControllerDocumentTemplate()->loadJavascriptLib($lib);
1378 }
1379
1380 /********************************************
1381 *
1382 * Various helper functions
1383 *
1384 ********************************************/
1385
1386 /**
1387 * Return record path (visually formatted, using BackendUtility::getRecordPath() )
1388 *
1389 * @param string $table Table name
1390 * @param array $rec Record array
1391 * @return string The record path.
1392 * @see BackendUtility::getRecordPath()
1393 */
1394 public function getRecordPath($table, $rec) {
1395 BackendUtility::fixVersioningPid($table, $rec);
1396 list($tscPID, $thePidValue) = BackendUtility::getTSCpidCached($table, $rec['uid'], $rec['pid']);
1397 if ($thePidValue >= 0) {
1398 return BackendUtility::getRecordPath($tscPID, $this->readPerms(), 15);
1399 }
1400 return '';
1401 }
1402
1403 /**
1404 * Returns the select-page read-access SQL clause.
1405 * Returns cached string, so you can call this function as much as you like without performance loss.
1406 *
1407 * @return string
1408 */
1409 public function readPerms() {
1410 if (!$this->perms_clause_set) {
1411 $this->perms_clause = $this->getBackendUserAuthentication()->getPagePermsClause(1);
1412 $this->perms_clause_set = TRUE;
1413 }
1414 return $this->perms_clause;
1415 }
1416
1417 /**
1418 * Returns TRUE if descriptions should be loaded always
1419 *
1420 * @param string $table Table for which to check
1421 * @return bool
1422 */
1423 public function doLoadTableDescr($table) {
1424 return $GLOBALS['TCA'][$table]['interface']['always_description'];
1425 }
1426
1427 /**
1428 * Initialize list of additional preview languages.
1429 * Sets according list in $this->additionalPreviewLanguages
1430 *
1431 * @return void
1432 */
1433 protected function initializeAdditionalPreviewLanguages() {
1434 $backendUserAuthentication = $this->getBackendUserAuthentication();
1435 $additionalPreviewLanguageListOfUser = $backendUserAuthentication->getTSConfigVal('options.additionalPreviewLanguages');
1436 $additionalPreviewLanguages = array();
1437 if ($additionalPreviewLanguageListOfUser) {
1438 $uids = GeneralUtility::intExplode(',', $additionalPreviewLanguageListOfUser);
1439 foreach ($uids as $uid) {
1440 if ($sys_language_rec = BackendUtility::getRecord('sys_language', $uid)) {
1441 $additionalPreviewLanguages[$uid]['uid'] = $uid;
1442 if (!empty($sys_language_rec['language_isocode'])) {
1443 $additionalPreviewLanguages[$uid]['ISOcode'] = $sys_language_rec['language_isocode'];
1444 } elseif ($sys_language_rec['static_lang_isocode'] && ExtensionManagementUtility::isLoaded('static_info_tables')) {
1445 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.');
1446 $staticLangRow = BackendUtility::getRecord('static_languages', $sys_language_rec['static_lang_isocode'], 'lg_iso_2');
1447 if ($staticLangRow['lg_iso_2']) {
1448 $additionalPreviewLanguages[$uid]['ISOcode'] = $staticLangRow['lg_iso_2'];
1449 }
1450 }
1451 }
1452 }
1453 }
1454 $this->additionalPreviewLanguages = $additionalPreviewLanguages;
1455 }
1456
1457 /**
1458 * @return BackendUserAuthentication
1459 */
1460 protected function getBackendUserAuthentication() {
1461 return $GLOBALS['BE_USER'];
1462 }
1463
1464 /**
1465 * @return DocumentTemplate
1466 */
1467 protected function getControllerDocumentTemplate() {
1468 // $GLOBALS['SOBE'] might be any kind of PHP class (controller most of the times)
1469 // These class do not inherit from any common class, but they all seem to have a "doc" member
1470 return $GLOBALS['SOBE']->doc;
1471 }
1472
1473 /**
1474 * @return LanguageService
1475 */
1476 protected function getLanguageService() {
1477 return $GLOBALS['LANG'];
1478 }
1479
1480 /**
1481 * Wrapper for access to the current page renderer object
1482 *
1483 * @return \TYPO3\CMS\Core\Page\PageRenderer
1484 */
1485 protected function getPageRenderer() {
1486 if ($this->pageRenderer === NULL) {
1487 $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
1488 }
1489
1490 return $this->pageRenderer;
1491 }
1492
1493 }