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