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