[!!!][TASK] Remove ExtJS Quicktips where possible
[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 // Tell the browser to scroll to the newly created record
693
694 $jsonArray['scriptCall'][] = 'Element.scrollTo(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
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 $GLOBALS['SOBE']->doc->backPath = $GLOBALS['BACK_PATH'];
870 // Initialize FormEngine (rendering the forms)
871 // @todo: check if this is still needed, simplify
872 $GLOBALS['SOBE']->tceforms = $this;
873 }
874
875 /**
876 * Return expand / collapse state array for a given table / uid combination
877 *
878 * @param string $table Handled table
879 * @param int $uid Handled uid
880 * @return array
881 */
882 protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid) {
883 $inlineView = $this->getInlineExpandCollapseStateArray();
884 $result = array();
885 if (MathUtility::canBeInterpretedAsInteger($uid)) {
886 if (!empty($inlineView[$table][$uid])) {
887 $result = $inlineView[$table][$uid];
888 }
889 }
890 return $result;
891 }
892
893 /**
894 * Get expand / collapse state of inline items
895 *
896 * @return array
897 */
898 protected function getInlineExpandCollapseStateArray() {
899 $backendUser = $this->getBackendUserAuthentication();
900 $inlineView = unserialize($backendUser->uc['inlineView']);
901 if (!is_array($inlineView)) {
902 $inlineView = array();
903 }
904 return $inlineView;
905 }
906
907 /**
908 * The "entry" pid for inline records. Nested inline records can potentially hang around on different
909 * 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.
910 *
911 * @return integer
912 */
913 protected function getInlineFirstPid() {
914 $table = $this->table;
915 $row = $this->databaseRow;
916 // If the parent is a page, use the uid(!) of the (new?) page as pid for the child records:
917 if ($table == 'pages') {
918 $liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']);
919 $pid = is_null($liveVersionId) ? $row['uid'] : $liveVersionId;
920 } elseif ($row['pid'] < 0) {
921 $prevRec = BackendUtility::getRecord($table, abs($row['pid']));
922 $pid = $prevRec['pid'];
923 } else {
924 $pid = $row['pid'];
925 }
926 return $pid;
927 }
928
929 /**
930 * Checks if a record selector may select a certain file type
931 *
932 * @param array $selectorConfiguration
933 * @param array $fileRecord
934 * @return bool
935 */
936 protected function checkInlineFileTypeAccessForField(array $selectorConfiguration, array $fileRecord) {
937 if (!empty($selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'])) {
938 $allowedFileExtensions = GeneralUtility::trimExplode(
939 ',',
940 $selectorConfiguration['PA']['fieldConf']['config']['appearance']['elementBrowserAllowed'],
941 TRUE
942 );
943 if (!in_array(strtolower($fileRecord['extension']), $allowedFileExtensions, TRUE)) {
944 return FALSE;
945 }
946 }
947 return TRUE;
948 }
949
950 /**
951 * Remove an element from an array.
952 *
953 * @param mixed $needle The element to be removed.
954 * @param array $haystack The array the element should be removed from.
955 * @param mixed $strict Search elements strictly.
956 * @return array The array $haystack without the $needle
957 */
958 protected function removeFromArray($needle, $haystack, $strict = NULL) {
959 $pos = array_search($needle, $haystack, $strict);
960 if ($pos !== FALSE) {
961 unset($haystack[$pos]);
962 }
963 return $haystack;
964 }
965
966 /**
967 * Generates an error message that transferred as JSON for AJAX calls
968 *
969 * @param string $message The error message to be shown
970 * @return array The error message in a JSON array
971 */
972 protected function getErrorMessageForAJAX($message) {
973 $jsonArray = array(
974 'data' => $message,
975 'scriptCall' => array(
976 'alert("' . $message . '");'
977 )
978 );
979 return $jsonArray;
980 }
981
982 /**
983 * Determines and sets several script calls to a JSON array, that would have been executed if processed in non-AJAX mode.
984 *
985 * @param array &$jsonArray Reference of the array to be used for JSON
986 * @param array $config The configuration of the IRRE field of the parent record
987 * @param int $inlineFirstPid Inline first pid
988 * @return array Modified array
989 * @todo: Basically, this methods shouldn't be there at all ...
990 */
991 protected function getInlineAjaxCommonScriptCalls($jsonArray, $config, $inlineFirstPid) {
992 // Add data that would have been added at the top of a regular FormEngine call:
993 if ($headTags = $this->getInlineHeadTags()) {
994 $jsonArray['headData'] = $headTags;
995 }
996 // Add the JavaScript data that would have been added at the bottom of a regular FormEngine call:
997 $jsonArray['scriptCall'][] = $this->JSbottom('editform', TRUE);
998 // If script.aculo.us Sortable is used, update the Observer to know the record:
999 if ($config['appearance']['useSortable']) {
1000 $inlineObjectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
1001 $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
1002 }
1003 // If FormEngine has some JavaScript code to be executed, just do it
1004 // @todo: this is done by JSBottom() already?!
1005 if ($this->extJSCODE) {
1006 $jsonArray['scriptCall'][] = $this->extJSCODE;
1007 }
1008
1009 // require js handling
1010 foreach ($this->requireJsModules as $moduleName => $callbacks) {
1011 if (!is_array($callbacks)) {
1012 $callbacks = array($callbacks);
1013 }
1014 foreach ($callbacks as $callback) {
1015 $inlineCodeKey = $moduleName;
1016 $javaScriptCode = 'require(["' . $moduleName . '"]';
1017 if ($callback !== NULL) {
1018 $inlineCodeKey .= sha1($callback);
1019 $javaScriptCode .= ', ' . $callback;
1020 }
1021 $javaScriptCode .= ');';
1022 $jsonArray['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode;
1023 }
1024 }
1025 return $jsonArray;
1026 }
1027
1028 /**
1029 * Parses the HTML tags that would have been inserted to the <head> of a HTML document and returns the found tags as multidimensional array.
1030 *
1031 * @return array The parsed tags with their attributes and innerHTML parts
1032 * @todo: WTF?
1033 */
1034 protected function getInlineHeadTags() {
1035 $headTags = array();
1036 $headDataRaw = $this->JStop() . $this->getJavaScriptOfPageRenderer();
1037 if ($headDataRaw) {
1038 // Create instance of the HTML parser:
1039 $parseObj = GeneralUtility::makeInstance(HtmlParser::class);
1040 // Removes script wraps:
1041 $headDataRaw = str_replace(array('/*<![CDATA[*/', '/*]]>*/'), '', $headDataRaw);
1042 // Removes leading spaces of a multi-line string:
1043 $headDataRaw = trim(preg_replace('/(^|\\r|\\n)( |\\t)+/', '$1', $headDataRaw));
1044 // Get script and link tags:
1045 $tags = array_merge(
1046 $parseObj->getAllParts($parseObj->splitTags('link', $headDataRaw)),
1047 $parseObj->getAllParts($parseObj->splitIntoBlock('script', $headDataRaw))
1048 );
1049 foreach ($tags as $tagData) {
1050 $tagAttributes = $parseObj->get_tag_attributes($parseObj->getFirstTag($tagData), TRUE);
1051 $headTags[] = array(
1052 'name' => $parseObj->getFirstTagName($tagData),
1053 'attributes' => $tagAttributes[0],
1054 'innerHTML' => $parseObj->removeFirstAndLastTag($tagData)
1055 );
1056 }
1057 }
1058 return $headTags;
1059 }
1060
1061 /**
1062 * Gets the JavaScript of the pageRenderer.
1063 * This can be used to extract newly added files which have been added
1064 * during an AJAX request. Due to the spread possibilities of the pageRenderer
1065 * to add JavaScript rendering and extracting seems to be the easiest way.
1066 *
1067 * @return string
1068 * @todo: aaaargs ...
1069 */
1070 protected function getJavaScriptOfPageRenderer() {
1071 /** @var $pageRenderer PageRenderer */
1072 $pageRenderer = clone $this->getPageRenderer();
1073 $pageRenderer->setCharSet($this->getLanguageService()->charSet);
1074 $pageRenderer->setTemplateFile('EXT:backend/Resources/Private/Templates/helper_javascript_css.html');
1075 return $pageRenderer->render();
1076 }
1077
1078 /**
1079 * Returns the "returnUrl" of the form. Can be set externally or will be taken from "GeneralUtility::linkThisScript()"
1080 *
1081 * @return string Return URL of current script
1082 */
1083 protected function thisReturnUrl() {
1084 return $this->returnUrl ? $this->returnUrl : GeneralUtility::linkThisScript();
1085 }
1086
1087 /********************************************
1088 *
1089 * Template functions
1090 *
1091 ********************************************/
1092 /**
1093 * Wraps all the table rows into a single table.
1094 * Used externally from scripts like EditDocumentController and PageLayoutController (which uses FormEngine)
1095 *
1096 * @param string $c Code to output between table-parts; table rows
1097 * @param array $rec The record
1098 * @param string $table The table name
1099 * @return string
1100 */
1101 public function wrapTotal($c, $rec, $table) {
1102 $parts = $this->replaceTableWrap(explode('|', $this->totalWrap, 2), $rec, $table);
1103 return $parts[0] . $c . $parts[1] . implode(LF, $this->hiddenFieldAccum);
1104 }
1105
1106 /**
1107 * Generates a token and returns an input field with it
1108 *
1109 * @param string $formName Context of the token
1110 * @param string $tokenName The name of the token GET/POST variable
1111 * @return string A complete input field
1112 */
1113 static public function getHiddenTokenField($formName = 'securityToken', $tokenName = 'formToken') {
1114 $formprotection = FormProtectionFactory::get();
1115 return '<input type="hidden" name="' . $tokenName . '" value="' . $formprotection->generateToken($formName) . '" />';
1116 }
1117
1118 /**
1119 * This replaces markers in the total wrap
1120 *
1121 * @param array $arr An array of template parts containing some markers.
1122 * @param array $rec The record
1123 * @param string $table The table name
1124 * @return string
1125 */
1126 public function replaceTableWrap($arr, $rec, $table) {
1127 $icon = IconUtility::getSpriteIconForRecord($table, $rec, array('title' => $this->getRecordPath($table, $rec)));
1128 // Make "new"-label
1129 $languageService = $this->getLanguageService();
1130 if (strstr($rec['uid'], 'NEW')) {
1131 $newLabel = ' <span class="typo3-TCEforms-newToken">' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.new', TRUE) . '</span>';
1132 // BackendUtility::fixVersioningPid Should not be used here because NEW records are not offline workspace versions...
1133 $truePid = BackendUtility::getTSconfig_pidValue($table, $rec['uid'], $rec['pid']);
1134 $prec = BackendUtility::getRecordWSOL('pages', $truePid, 'title');
1135 $pageTitle = BackendUtility::getRecordTitle('pages', $prec, TRUE, FALSE);
1136 $rLabel = '<em>[PID: ' . $truePid . '] ' . $pageTitle . '</em>';
1137 // Fetch translated title of the table
1138 $tableTitle = $languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
1139 if ($table === 'pages') {
1140 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.createNewPage', TRUE);
1141 $pageTitle = sprintf($label, $tableTitle);
1142 } else {
1143 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.createNewRecord', TRUE);
1144 if ($rec['pid'] == 0) {
1145 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.createNewRecordRootLevel', TRUE);
1146 }
1147 $pageTitle = sprintf($label, $tableTitle, $pageTitle);
1148 }
1149 } else {
1150 $newLabel = ' <span class="typo3-TCEforms-recUid">[' . $rec['uid'] . ']</span>';
1151 $rLabel = BackendUtility::getRecordTitle($table, $rec, TRUE, FALSE);
1152 $prec = BackendUtility::getRecordWSOL('pages', $rec['pid'], 'uid,title');
1153 // Fetch translated title of the table
1154 $tableTitle = $languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
1155 if ($table === 'pages') {
1156 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editPage', TRUE);
1157 // Just take the record title and prepend an edit label.
1158 $pageTitle = sprintf($label, $tableTitle, $rLabel);
1159 } else {
1160 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecord', TRUE);
1161 $pageTitle = BackendUtility::getRecordTitle('pages', $prec, TRUE, FALSE);
1162 if ($rLabel === BackendUtility::getNoRecordTitle(TRUE)) {
1163 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecordNoTitle', TRUE);
1164 }
1165 if ($rec['pid'] == 0) {
1166 $label = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.editRecordRootLevel', TRUE);
1167 }
1168 if ($rLabel !== BackendUtility::getNoRecordTitle(TRUE)) {
1169 // Just take the record title and prepend an edit label.
1170 $pageTitle = sprintf($label, $tableTitle, $rLabel, $pageTitle);
1171 } else {
1172 // Leave out the record title since it is not set.
1173 $pageTitle = sprintf($label, $tableTitle, $pageTitle);
1174 }
1175 }
1176 $icon = $this->getControllerDocumentTemplate()->wrapClickMenuOnIcon($icon, $table, $rec['uid'], 1, '', '+copy,info,edit,view');
1177 }
1178 foreach ($arr as $k => $v) {
1179 // Make substitutions:
1180 $arr[$k] = str_replace(
1181 array(
1182 '###PAGE_TITLE###',
1183 '###ID_NEW_INDICATOR###',
1184 '###RECORD_LABEL###',
1185 '###TABLE_TITLE###',
1186 '###RECORD_ICON###'
1187 ),
1188 array(
1189 $pageTitle,
1190 $newLabel,
1191 $rLabel,
1192 htmlspecialchars($languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title'])),
1193 $icon
1194 ),
1195 $arr[$k]
1196 );
1197 }
1198 return $arr;
1199 }
1200
1201
1202 /********************************************
1203 *
1204 * JavaScript related functions
1205 *
1206 ********************************************/
1207 /**
1208 * JavaScript code added BEFORE the form is drawn:
1209 *
1210 * @return string A <script></script> section with JavaScript.
1211 */
1212 public function JStop() {
1213 $out = '';
1214 if (!empty($this->additionalCode_pre)) {
1215 $out = implode(LF, $this->additionalCode_pre) . LF;
1216 }
1217 return $out;
1218 }
1219
1220 /**
1221 * JavaScript bottom code
1222 *
1223 * @param string $formname The identification of the form on the page.
1224 * @param bool $update Just extend/update existing settings, e.g. for AJAX call
1225 * @return string A section with JavaScript - if $update is FALSE, embedded in <script></script>
1226 */
1227 public function JSbottom($formname = 'forms[0]', $update = FALSE) {
1228 $languageService = $this->getLanguageService();
1229 $jsFile = array();
1230 $out = '';
1231 $this->TBE_EDITOR_fieldChanged_func = 'TBE_EDITOR.fieldChanged_fName(fName,formObj[fName+"_list"]);';
1232 if (!$update) {
1233 if ($this->loadMD5_JS) {
1234 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/md5.js');
1235 }
1236 // load the main module for FormEngine with all important JS functions
1237 $this->requireJsModules['TYPO3/CMS/Backend/FormEngine'] = 'function(FormEngine) {
1238 FormEngine.setBrowserUrl(' . GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('browser')) . ');
1239 }';
1240 $this->requireJsModules['TYPO3/CMS/Backend/FormEngineValidation'] = 'function(FormEngineValidation) {
1241 FormEngineValidation.setUsMode(' . ($GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '1' : '0') . ');
1242 FormEngineValidation.registerReady();
1243 }';
1244
1245 $pageRenderer = $this->getPageRenderer();
1246 foreach ($this->requireJsModules as $moduleName => $callbacks) {
1247 if (!is_array($callbacks)) {
1248 $callbacks = array($callbacks);
1249 }
1250 foreach ($callbacks as $callback) {
1251 $pageRenderer->loadRequireJsModule($moduleName, $callback);
1252 }
1253 }
1254 $pageRenderer->loadPrototype();
1255 $pageRenderer->loadJquery();
1256 $pageRenderer->loadExtJS();
1257 $beUserAuth = $this->getBackendUserAuthentication();
1258 // Make textareas resizable and flexible ("autogrow" in height)
1259 $textareaSettings = array(
1260 'autosize' => (bool)$beUserAuth->uc['resizeTextareas_Flexible']
1261 );
1262 $pageRenderer->addInlineSettingArray('Textarea', $textareaSettings);
1263
1264 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.tbe_editor.js');
1265 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ValueSlider');
1266 // Needed for FormEngine manipulation (date picker)
1267 $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'));
1268 $pageRenderer->addInlineSetting('DateTimePicker', 'DateFormat', $dateFormat);
1269
1270 // support placeholders for IE9 and lower
1271 $clientInfo = GeneralUtility::clientInfo();
1272 if ($clientInfo['BROWSER'] == 'msie' && $clientInfo['VERSION'] <= 9) {
1273 $this->loadJavascriptLib('sysext/core/Resources/Public/JavaScript/Contrib/placeholders.jquery.min.js');
1274 }
1275
1276 // @todo: remove scriptaclous once suggest & flex form foo is moved to RequireJS, see #55575
1277 $pageRenderer->loadScriptaculous();
1278 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.tceforms_suggest.js');
1279
1280 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Filelist/FileListLocalisation');
1281 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/DragUploader');
1282
1283 $pageRenderer->addInlineLanguagelabelFile(
1284 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('lang') . 'locallang_core.xlf',
1285 'file_upload'
1286 );
1287
1288 // We want to load jQuery-ui inside our js. Enable this using requirejs.
1289 $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.inline.js');
1290 $out .= '
1291 inline.setNoTitleString("' . addslashes(BackendUtility::getNoRecordTitle(TRUE)) . '");
1292 ';
1293
1294 $out .= '
1295 TBE_EDITOR.formname = "' . $formname . '";
1296 TBE_EDITOR.formnameUENC = "' . rawurlencode($formname) . '";
1297 TBE_EDITOR.backPath = "";
1298 TBE_EDITOR.isPalettedoc = null;
1299 TBE_EDITOR.doSaveFieldName = "' . ($this->doSaveFieldName ? addslashes($this->doSaveFieldName) : '') . '";
1300 TBE_EDITOR.labels.fieldsChanged = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.fieldsChanged')) . ';
1301 TBE_EDITOR.labels.fieldsMissing = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.fieldsMissing')) . ';
1302 TBE_EDITOR.labels.maxItemsAllowed = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.maxItemsAllowed')) . ';
1303 TBE_EDITOR.labels.refresh_login = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:mess.refresh_login')) . ';
1304 TBE_EDITOR.labels.onChangeAlert = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:mess.onChangeAlert')) . ';
1305 TBE_EDITOR.labels.remainingCharacters = ' . GeneralUtility::quoteJSvalue($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.remainingCharacters')) . ';
1306 TBE_EDITOR.customEvalFunctions = {};
1307
1308 ';
1309 }
1310 // Add JS required for inline fields
1311 if (!empty($this->inlineData)) {
1312 $out .= '
1313 inline.addToDataArray(' . json_encode($this->inlineData) . ');
1314 ';
1315 }
1316 // $this->additionalJS_submit:
1317 if ($this->additionalJS_submit) {
1318 $additionalJS_submit = implode('', $this->additionalJS_submit);
1319 $additionalJS_submit = str_replace(array(CR, LF), '', $additionalJS_submit);
1320 $out .= '
1321 TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJS_submit) . '");
1322 ';
1323 }
1324 $out .= LF . implode(LF, $this->additionalJS_post) . LF . $this->extJSCODE;
1325 // Regular direct output:
1326 if (!$update) {
1327 $spacer = LF . TAB;
1328 $out = $spacer . implode($spacer, $jsFile) . GeneralUtility::wrapJS($out);
1329 }
1330 return $out;
1331 }
1332
1333 /**
1334 * Prints necessary JavaScript for TCEforms (after the form HTML).
1335 * currently this is used to transform page-specific options in the TYPO3.Settings array for JS
1336 * so the JS module can access these values
1337 *
1338 * @return string
1339 */
1340 public function printNeededJSFunctions() {
1341 // set variables to be accessible for JS
1342 $pageRenderer = $this->getPageRenderer();
1343 $pageRenderer->addInlineSetting('FormEngine', 'formName', 'editform');
1344 $pageRenderer->addInlineSetting('FormEngine', 'backPath', '');
1345
1346 // Integrate JS functions for the element browser if such fields or IRRE fields were processed
1347 $pageRenderer->addInlineSetting('FormEngine', 'legacyFieldChangedCb', 'function() { ' . $this->TBE_EDITOR_fieldChanged_func . ' };');
1348
1349 return $this->JSbottom('editform');
1350 }
1351
1352 /**
1353 * Returns necessary JavaScript for the top
1354 *
1355 * @return string
1356 */
1357 public function printNeededJSFunctions_top() {
1358 return $this->JStop('editform');
1359 }
1360
1361 /**
1362 * Includes a javascript library that exists in the core /typo3/ directory. The
1363 * backpath is automatically applied.
1364 * This method acts as wrapper for $GLOBALS['SOBE']->doc->loadJavascriptLib($lib).
1365 *
1366 * @param string $lib Library name. Call it with the full path like "sysext/core/Resources/Public/JavaScript/QueryGenerator.js" to load it
1367 * @return void
1368 */
1369 public function loadJavascriptLib($lib) {
1370 $this->getControllerDocumentTemplate()->loadJavascriptLib($lib);
1371 }
1372
1373 /********************************************
1374 *
1375 * Various helper functions
1376 *
1377 ********************************************/
1378
1379 /**
1380 * Return record path (visually formatted, using BackendUtility::getRecordPath() )
1381 *
1382 * @param string $table Table name
1383 * @param array $rec Record array
1384 * @return string The record path.
1385 * @see BackendUtility::getRecordPath()
1386 */
1387 public function getRecordPath($table, $rec) {
1388 BackendUtility::fixVersioningPid($table, $rec);
1389 list($tscPID, $thePidValue) = BackendUtility::getTSCpidCached($table, $rec['uid'], $rec['pid']);
1390 if ($thePidValue >= 0) {
1391 return BackendUtility::getRecordPath($tscPID, $this->readPerms(), 15);
1392 }
1393 return '';
1394 }
1395
1396 /**
1397 * Returns the select-page read-access SQL clause.
1398 * Returns cached string, so you can call this function as much as you like without performance loss.
1399 *
1400 * @return string
1401 */
1402 public function readPerms() {
1403 if (!$this->perms_clause_set) {
1404 $this->perms_clause = $this->getBackendUserAuthentication()->getPagePermsClause(1);
1405 $this->perms_clause_set = TRUE;
1406 }
1407 return $this->perms_clause;
1408 }
1409
1410 /**
1411 * Returns TRUE if descriptions should be loaded always
1412 *
1413 * @param string $table Table for which to check
1414 * @return bool
1415 */
1416 public function doLoadTableDescr($table) {
1417 return $GLOBALS['TCA'][$table]['interface']['always_description'];
1418 }
1419
1420 /**
1421 * Initialize list of additional preview languages.
1422 * Sets according list in $this->additionalPreviewLanguages
1423 *
1424 * @return void
1425 */
1426 protected function initializeAdditionalPreviewLanguages() {
1427 $backendUserAuthentication = $this->getBackendUserAuthentication();
1428 $additionalPreviewLanguageListOfUser = $backendUserAuthentication->getTSConfigVal('options.additionalPreviewLanguages');
1429 $additionalPreviewLanguages = array();
1430 if ($additionalPreviewLanguageListOfUser) {
1431 $uids = GeneralUtility::intExplode(',', $additionalPreviewLanguageListOfUser);
1432 foreach ($uids as $uid) {
1433 if ($sys_language_rec = BackendUtility::getRecord('sys_language', $uid)) {
1434 $additionalPreviewLanguages[$uid]['uid'] = $uid;
1435 if (!empty($sys_language_rec['language_isocode'])) {
1436 $additionalPreviewLanguages[$uid]['ISOcode'] = $sys_language_rec['language_isocode'];
1437 } elseif ($sys_language_rec['static_lang_isocode'] && ExtensionManagementUtility::isLoaded('static_info_tables')) {
1438 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.');
1439 $staticLangRow = BackendUtility::getRecord('static_languages', $sys_language_rec['static_lang_isocode'], 'lg_iso_2');
1440 if ($staticLangRow['lg_iso_2']) {
1441 $additionalPreviewLanguages[$uid]['ISOcode'] = $staticLangRow['lg_iso_2'];
1442 }
1443 }
1444 }
1445 }
1446 }
1447 $this->additionalPreviewLanguages = $additionalPreviewLanguages;
1448 }
1449
1450 /**
1451 * @return BackendUserAuthentication
1452 */
1453 protected function getBackendUserAuthentication() {
1454 return $GLOBALS['BE_USER'];
1455 }
1456
1457 /**
1458 * @return DocumentTemplate
1459 */
1460 protected function getControllerDocumentTemplate() {
1461 // $GLOBALS['SOBE'] might be any kind of PHP class (controller most of the times)
1462 // These class do not inherit from any common class, but they all seem to have a "doc" member
1463 return $GLOBALS['SOBE']->doc;
1464 }
1465
1466 /**
1467 * @return LanguageService
1468 */
1469 protected function getLanguageService() {
1470 return $GLOBALS['LANG'];
1471 }
1472
1473 /**
1474 * Wrapper for access to the current page renderer object
1475 *
1476 * @return \TYPO3\CMS\Core\Page\PageRenderer
1477 */
1478 protected function getPageRenderer() {
1479 if ($this->pageRenderer === NULL) {
1480 $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
1481 }
1482
1483 return $this->pageRenderer;
1484 }
1485
1486 }