[BUGFIX] Fix several typos in php comments
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Container / InlineControlContainer.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\Container;
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\InlineStackProcessor;
18 use TYPO3\CMS\Backend\Form\NodeFactory;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Imaging\Icon;
22 use TYPO3\CMS\Core\Imaging\IconFactory;
23 use TYPO3\CMS\Core\Localization\LanguageService;
24 use TYPO3\CMS\Core\Resource\Folder;
25 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Core\Utility\MathUtility;
28 use TYPO3\CMS\Core\Utility\StringUtility;
29
30 /**
31 * Inline element entry container.
32 *
33 * This container is the entry step to rendering an inline element. It is created by SingleFieldContainer.
34 *
35 * The code creates the main structure for the single inline elements, initializes
36 * the inlineData array, that is manipulated and also returned back in its manipulated state.
37 * The "control" stuff of inline elements is rendered here, for example the "create new" button.
38 *
39 * For each existing inline relation an InlineRecordContainer is called for further processing.
40 */
41 class InlineControlContainer extends AbstractContainer
42 {
43 /**
44 * Inline data array used in JS, returned as JSON object to frontend
45 *
46 * @var array
47 */
48 protected $inlineData = [];
49
50 /**
51 * @var InlineStackProcessor
52 */
53 protected $inlineStackProcessor;
54
55 /**
56 * @var IconFactory
57 */
58 protected $iconFactory;
59
60 /**
61 * @var string[]
62 */
63 protected $requireJsModules = [];
64
65 /**
66 * Default field information enabled for this element.
67 *
68 * @var array
69 */
70 protected $defaultFieldInformation = [
71 'tcaDescription' => [
72 'renderType' => 'tcaDescription',
73 ],
74 ];
75
76 /**
77 * @var array Default wizards
78 */
79 protected $defaultFieldWizard = [
80 'localizationStateSelector' => [
81 'renderType' => 'localizationStateSelector',
82 ],
83 ];
84
85 /**
86 * Container objects give $nodeFactory down to other containers.
87 *
88 * @param NodeFactory $nodeFactory
89 * @param array $data
90 */
91 public function __construct(NodeFactory $nodeFactory, array $data)
92 {
93 parent::__construct($nodeFactory, $data);
94 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
95 }
96
97 /**
98 * Entry method
99 *
100 * @return array As defined in initializeResultArray() of AbstractNode
101 */
102 public function render()
103 {
104 $languageService = $this->getLanguageService();
105
106 $this->inlineData = $this->data['inlineData'];
107
108 /** @var InlineStackProcessor $inlineStackProcessor */
109 $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
110 $this->inlineStackProcessor = $inlineStackProcessor;
111 $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
112
113 $table = $this->data['tableName'];
114 $row = $this->data['databaseRow'];
115 $field = $this->data['fieldName'];
116 $parameterArray = $this->data['parameterArray'];
117
118 $resultArray = $this->initializeResultArray();
119
120 $config = $parameterArray['fieldConf']['config'];
121 $foreign_table = $config['foreign_table'];
122 $isReadOnly = isset($config['readOnly']) && $config['readOnly'];
123 $language = 0;
124 $languageFieldName = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
125 if (BackendUtility::isTableLocalizable($table)) {
126 $language = isset($row[$languageFieldName][0]) ? (int)$row[$languageFieldName][0] : (int)$row[$languageFieldName];
127 }
128
129 // Add the current inline job to the structure stack
130 $newStructureItem = [
131 'table' => $table,
132 'uid' => $row['uid'],
133 'field' => $field,
134 'config' => $config,
135 ];
136 // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
137 if (!empty($parameterArray['itemFormElName'])) {
138 $flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']);
139 if ($flexFormParts !== null) {
140 $newStructureItem['flexform'] = $flexFormParts;
141 }
142 }
143 $inlineStackProcessor->pushStableStructureItem($newStructureItem);
144
145 // Transport the flexform DS identifier fields to the FormInlineAjaxController
146 if (!empty($newStructureItem['flexform'])
147 && isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'])
148 ) {
149 $config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'];
150 }
151
152 // Hand over original returnUrl to FormInlineAjaxController. Needed if opening for instance a
153 // nested element in a new view to then go back to the original returnUrl and not the url of
154 // the inline ajax controller
155 $config['originalReturnUrl'] = $this->data['returnUrl'];
156
157 // e.g. data[<table>][<uid>][<field>]
158 $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix();
159 // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
160 $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
161
162 $config['inline']['first'] = false;
163 $firstChild = reset($this->data['parameterArray']['fieldConf']['children']);
164 if (isset($firstChild['databaseRow']['uid'])) {
165 $config['inline']['first'] = $firstChild['databaseRow']['uid'];
166 }
167 $config['inline']['last'] = false;
168 $lastChild = end($this->data['parameterArray']['fieldConf']['children']);
169 if (isset($lastChild['databaseRow']['uid'])) {
170 $config['inline']['last'] = $lastChild['databaseRow']['uid'];
171 }
172
173 $top = $inlineStackProcessor->getStructureLevel(0);
174
175 $this->inlineData['config'][$nameObject] = [
176 'table' => $foreign_table,
177 ];
178 $this->inlineData['config'][$nameObject . '-' . $foreign_table] = [
179 'min' => $config['minitems'],
180 'max' => $config['maxitems'],
181 'sortable' => $config['appearance']['useSortable'],
182 'top' => [
183 'table' => $top['table'],
184 'uid' => $top['uid']
185 ],
186 'context' => [
187 'config' => $config,
188 'hmac' => GeneralUtility::hmac(json_encode($config), 'InlineContext'),
189 ],
190 ];
191 $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
192
193 $uniqueMax = 0;
194 $uniqueIds = [];
195
196 if ($config['foreign_unique']) {
197 // Add inlineData['unique'] with JS unique configuration
198 // @todo: Improve validation and throw an exception if type is neither select nor group here
199 $type = $config['selectorOrUniqueConfiguration']['config']['type'] === 'select' ? 'select' : 'groupdb';
200 foreach ($parameterArray['fieldConf']['children'] as $child) {
201 // Determine used unique ids, skip not localized records
202 if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
203 $value = $child['databaseRow'][$config['foreign_unique']];
204 // We're assuming there is only one connected value here for both select and group
205 if ($type === 'select') {
206 // A select field is an array of uids. See TcaSelectItems data provider for details.
207 // Pick first entry, ends up as eg. $value = 42.
208 $value = $value['0'];
209 } else {
210 // A group field is an array of arrays containing uid + table + title + row.
211 // See TcaGroup data provider for details.
212 // Pick the first one (always on 0), and use uid + table only. Exclude title + row
213 // since the entire inlineData['unique'] array ends up in JavaScript in the end
214 // and we don't need and want the title and the entire row data in the frontend.
215 // Ends up as $value = [ 'uid' => '42', 'table' => 'tx_my_table' ]
216 $value = [
217 'uid' => $value[0]['uid'],
218 'table' => $value[0]['table'],
219 ];
220 }
221 // Note structure of $value is different in select vs. group: It's a uid for select, but an
222 // array with uid + table for group.
223 $uniqueIds[$child['databaseRow']['uid']] = $value;
224 }
225 }
226 $possibleRecords = $config['selectorOrUniquePossibleRecords'];
227 $possibleRecordsUidToTitle = [];
228 foreach ($possibleRecords as $possibleRecord) {
229 $possibleRecordsUidToTitle[$possibleRecord[1]] = $possibleRecord[0];
230 }
231 $uniqueMax = $config['appearance']['useCombination'] || empty($possibleRecords) ? -1 : count($possibleRecords);
232 $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = [
233 'max' => $uniqueMax,
234 'used' => $uniqueIds,
235 'type' => $type,
236 'table' => $foreign_table,
237 'elTable' => $config['selectorOrUniqueConfiguration']['foreignTable'],
238 'field' => $config['foreign_unique'],
239 'selector' => $config['selectorOrUniqueConfiguration']['isSelector'] ? $type : false,
240 'possible' => $possibleRecordsUidToTitle,
241 ];
242 }
243
244 $resultArray['inlineData'] = $this->inlineData;
245
246 // @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider
247 $uidOfDefaultRecord = $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
248 $isLocalizedParent = $language > 0
249 && ($uidOfDefaultRecord[0] ?? $uidOfDefaultRecord) > 0
250 && MathUtility::canBeInterpretedAsInteger($row['uid']);
251 $numberOfFullLocalizedChildren = 0;
252 $numberOfNotYetLocalizedChildren = 0;
253 foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
254 if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
255 $numberOfFullLocalizedChildren++;
256 }
257 if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
258 $numberOfNotYetLocalizedChildren++;
259 }
260 }
261
262 // Render the localization links if needed
263 $localizationLinks = '';
264 if ($numberOfNotYetLocalizedChildren) {
265 // Add the "Localize all records" link before all child records:
266 if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) {
267 $localizationLinks = ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config);
268 }
269 // Add the "Synchronize with default language" link before all child records:
270 if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) {
271 $localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config);
272 }
273 }
274
275 // Define how to show the "Create new record" link - if there are more than maxitems, hide it
276 if ($isReadOnly || $numberOfFullLocalizedChildren >= $config['maxitems'] || ($uniqueMax > 0 && $numberOfFullLocalizedChildren >= $uniqueMax)) {
277 $config['inline']['inlineNewButtonStyle'] = 'display: none;';
278 $config['inline']['inlineNewRelationButtonStyle'] = 'display: none;';
279 $config['inline']['inlineOnlineMediaAddButtonStyle'] = 'display: none;';
280 }
281
282 // Render the level links (create new record):
283 $levelLinks = '';
284 if (!empty($config['appearance']['enabledControls']['new'])) {
285 $levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config);
286 }
287 // Wrap all inline fields of a record with a <div> (like a container)
288 $html = '<div class="form-group" id="' . htmlspecialchars($nameObject) . '" data-uid="' . htmlspecialchars($row['uid']) . '" data-foreign-table="' . htmlspecialchars($foreign_table) . '" data-object-group="' . htmlspecialchars($nameObject) . '-' . htmlspecialchars($foreign_table) . '" data-form-field="' . htmlspecialchars($nameForm) . '" data-appearance="' . htmlspecialchars(json_encode($config['appearance'])) . '">';
289
290 $fieldInformationResult = $this->renderFieldInformation();
291 $html .= $fieldInformationResult['html'];
292 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
293
294 // Add the level links before all child records:
295 if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
296 $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>';
297 }
298
299 // If it's required to select from possible child records (reusable children), add a selector box
300 if (!$isReadOnly && $config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== false) {
301 if ($config['selectorOrUniqueConfiguration']['config']['type'] === 'select') {
302 $selectorBox = $this->renderPossibleRecordsSelectorTypeSelect($config, $uniqueIds);
303 } else {
304 $selectorBox = $this->renderPossibleRecordsSelectorTypeGroupDB($config);
305 }
306 $html .= $selectorBox . $localizationLinks;
307 }
308
309 $title = $languageService->sL(trim($parameterArray['fieldConf']['label']));
310 $html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">';
311
312 $sortableRecordUids = [];
313 foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) {
314 $options['inlineParentUid'] = $row['uid'];
315 $options['inlineFirstPid'] = $this->data['inlineFirstPid'];
316 // @todo: this can be removed if this container no longer sets additional info to $config
317 $options['inlineParentConfig'] = $config;
318 $options['inlineData'] = $this->inlineData;
319 $options['inlineStructure'] = $inlineStackProcessor->getStructure();
320 $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
321 $options['renderType'] = 'inlineRecordContainer';
322 $childResult = $this->nodeFactory->create($options)->render();
323 $html .= $childResult['html'];
324 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
325 if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
326 // Don't add record to list of "valid" uids if it is only the default
327 // language record of a not yet localized child
328 $sortableRecordUids[] = $options['databaseRow']['uid'];
329 }
330 }
331
332 $html .= '</div>';
333
334 $fieldWizardResult = $this->renderFieldWizard();
335 $fieldWizardHtml = $fieldWizardResult['html'];
336 $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
337 $html .= $fieldWizardHtml;
338
339 // Add the level links after all child records:
340 if (!$isReadOnly && ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom')) {
341 $html .= $levelLinks . $localizationLinks;
342 }
343 if (is_array($config['customControls'])) {
344 $html .= '<div id="' . $nameObject . '_customControls">';
345 foreach ($config['customControls'] as $customControlConfig) {
346 if (!isset($customControlConfig['userFunc'])) {
347 throw new \RuntimeException('Support for customControl without a userFunc key in TCA type inline is not supported.', 1548052629);
348 }
349 $parameters = [
350 'table' => $table,
351 'field' => $field,
352 'row' => $row,
353 'nameObject' => $nameObject,
354 'nameForm' => $nameForm,
355 'config' => $config,
356 'customControlConfig' => $customControlConfig,
357 ];
358 $html .= GeneralUtility::callUserFunction($customControlConfig['userFunc'], $parameters, $this);
359 }
360 $html .= '</div>';
361 }
362 $resultArray['requireJsModules'] = array_merge($resultArray['requireJsModules'], $this->requireJsModules);
363 $resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Container/InlineControlContainer' => '
364 function(InlineControlContainer) {
365 new InlineControlContainer(' . GeneralUtility::quoteJSvalue($nameObject) . ');
366 }'
367 ];
368
369 // Publish the uids of the child records in the given order to the browser
370 $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
371 . ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString(['type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']])) . '"'
372 . ' class="inlineRecord" />';
373 // Close the wrap for all inline fields (container)
374 $html .= '</div>';
375
376 $resultArray['html'] = $html;
377 return $resultArray;
378 }
379
380 /**
381 * Creates the HTML code of a general link to be used on a level of inline children.
382 * The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'.
383 *
384 * @param string $type The link type, values are 'newRecord', 'localize' and 'synchronize'.
385 * @param string $objectPrefix The "path" to the child record to create (e.g. 'data-parentPageId-parentTable-parentUid-parentField-childTable]')
386 * @param array $conf TCA configuration of the parent(!) field
387 * @return string The HTML code of the new link, wrapped in a div
388 */
389 protected function getLevelInteractionLink($type, $objectPrefix, $conf = [])
390 {
391 $languageService = $this->getLanguageService();
392 $attributes = [];
393 switch ($type) {
394 case 'newRecord':
395 $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createnew'));
396 $icon = 'actions-add';
397 $className = 'typo3-newRecordLink';
398 $attributes['class'] = 'btn btn-default t3js-create-new-button';
399 if (!empty($conf['inline']['inlineNewButtonStyle'])) {
400 $attributes['style'] = $conf['inline']['inlineNewButtonStyle'];
401 }
402 if (!empty($conf['appearance']['newRecordLinkAddTitle'])) {
403 $title = htmlspecialchars(sprintf(
404 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createnew.link'),
405 $languageService->sL($GLOBALS['TCA'][$conf['foreign_table']]['ctrl']['title'])
406 ));
407 } elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') {
408 $title = htmlspecialchars($languageService->sL($conf['appearance']['newRecordLinkTitle']));
409 }
410 break;
411 case 'localize':
412 $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localizeAllRecords'));
413 $icon = 'actions-document-localize';
414 $className = 'typo3-localizationLink';
415 $attributes['class'] = 'btn btn-default t3js-synchronizelocalize-button';
416 $attributes['data-type'] = 'localize';
417 break;
418 case 'synchronize':
419 $title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:synchronizeWithOriginalLanguage'));
420 $icon = 'actions-document-synchronize';
421 $className = 'typo3-synchronizationLink';
422 $attributes['class'] = 'btn btn-default inlineNewButton t3js-synchronizelocalize-button';
423 $attributes['data-type'] = 'synchronize';
424 break;
425 default:
426 $title = '';
427 $icon = '';
428 $className = '';
429 }
430 // Create the link:
431 $icon = $icon ? $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() : '';
432 $link = $this->wrapWithAnchor($icon . ' ' . $title, '#', $attributes);
433 return '<div' . ($className ? ' class="' . $className . '"' : '') . 'title="' . $title . '">' . $link . '</div>';
434 }
435
436 /**
437 * Wraps a text with an anchor and returns the HTML representation.
438 *
439 * @param string $text The text to be wrapped by an anchor
440 * @param string $link The link to be used in the anchor
441 * @param array $attributes Array of attributes to be used in the anchor
442 * @return string The wrapped text as HTML representation
443 */
444 protected function wrapWithAnchor($text, $link, $attributes = [])
445 {
446 $attributes['href'] = trim($link ?: '#');
447 return '<a ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>' . $text . '</a>';
448 }
449
450 /**
451 * Generate a link that opens an element browser in a new window.
452 * For group/db there is no way to use a "selector" like a <select>|</select>-box.
453 *
454 * @param array $inlineConfiguration TCA inline configuration of the parent(!) field
455 * @return string A HTML link that opens an element browser in a new window
456 */
457 protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration)
458 {
459 $backendUser = $this->getBackendUserAuthentication();
460 $languageService = $this->getLanguageService();
461
462 $groupFieldConfiguration = $inlineConfiguration['selectorOrUniqueConfiguration']['config'];
463
464 $foreign_table = $inlineConfiguration['foreign_table'];
465 $allowed = $groupFieldConfiguration['allowed'];
466 $currentStructureDomObjectIdPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
467 $objectPrefix = $currentStructureDomObjectIdPrefix . '-' . $foreign_table;
468 $mode = 'db';
469 $showUpload = false;
470 $showByUrl = false;
471 $elementBrowserEnabled = true;
472 if (!empty($inlineConfiguration['appearance']['createNewRelationLinkTitle'])) {
473 $createNewRelationText = htmlspecialchars($languageService->sL($inlineConfiguration['appearance']['createNewRelationLinkTitle']));
474 } else {
475 $createNewRelationText = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation'));
476 }
477 if (is_array($groupFieldConfiguration['appearance'])) {
478 if (isset($groupFieldConfiguration['appearance']['elementBrowserType'])) {
479 $mode = $groupFieldConfiguration['appearance']['elementBrowserType'];
480 }
481 if ($mode === 'file') {
482 $showUpload = true;
483 $showByUrl = true;
484 }
485 if (isset($inlineConfiguration['appearance']['fileUploadAllowed'])) {
486 $showUpload = (bool)$inlineConfiguration['appearance']['fileUploadAllowed'];
487 }
488 if (isset($inlineConfiguration['appearance']['fileByUrlAllowed'])) {
489 $showByUrl = (bool)$inlineConfiguration['appearance']['fileByUrlAllowed'];
490 }
491 if (isset($groupFieldConfiguration['appearance']['elementBrowserAllowed'])) {
492 $allowed = $groupFieldConfiguration['appearance']['elementBrowserAllowed'];
493 }
494 if (isset($inlineConfiguration['appearance']['elementBrowserEnabled'])) {
495 $elementBrowserEnabled = (bool)$inlineConfiguration['appearance']['elementBrowserEnabled'];
496 }
497 }
498 $browserParams = '|||' . $allowed . '|' . $objectPrefix;
499 $buttonStyle = '';
500 if (isset($inlineConfiguration['inline']['inlineNewRelationButtonStyle'])) {
501 $buttonStyle = ' style="' . $inlineConfiguration['inline']['inlineNewRelationButtonStyle'] . '"';
502 }
503 $item = '';
504 if ($elementBrowserEnabled) {
505 $item .= '
506 <a href="#" class="btn btn-default t3js-element-browser" data-mode="' . htmlspecialchars($mode) . '" data-params="' . htmlspecialchars($browserParams) . '"
507 ' . $buttonStyle . ' title="' . $createNewRelationText . '">
508 ' . $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render() . '
509 ' . $createNewRelationText . '
510 </a>';
511 }
512
513 $isDirectFileUploadEnabled = (bool)$backendUser->uc['edit_docModuleUpload'];
514 $allowedArray = GeneralUtility::trimExplode(',', $allowed, true);
515 $onlineMediaAllowed = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
516 if (!empty($allowedArray)) {
517 $onlineMediaAllowed = array_intersect($allowedArray, $onlineMediaAllowed);
518 }
519 if (($showUpload || $showByUrl) && $isDirectFileUploadEnabled) {
520 $folder = $backendUser->getDefaultUploadFolder(
521 $this->data['parentPageRow']['uid'],
522 $this->data['tableName'],
523 $this->data['fieldName']
524 );
525 if (
526 $folder instanceof Folder
527 && $folder->getStorage()->checkUserActionPermission('add', 'File')
528 ) {
529 if ($showUpload) {
530 $maxFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
531 $item .= ' <a href="#" class="btn btn-default t3js-drag-uploader inlineNewFileUploadButton"
532 ' . $buttonStyle . '
533 data-dropzone-target="#' . htmlspecialchars(StringUtility::escapeCssSelector($currentStructureDomObjectIdPrefix)) . '"
534 data-insert-dropzone-before="1"
535 data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
536 data-file-allowed="' . htmlspecialchars($allowed) . '"
537 data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
538 data-max-file-size="' . htmlspecialchars($maxFileSize) . '"
539 >';
540 $item .= $this->iconFactory->getIcon('actions-upload', Icon::SIZE_SMALL)->render() . ' ';
541 $item .= htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.select-and-submit'));
542 $item .= '</a>';
543
544 $this->requireJsModules[] = ['TYPO3/CMS/Backend/DragUploader' => 'function(dragUploader){dragUploader.initialize()}'];
545 }
546 if (!empty($onlineMediaAllowed) && $showByUrl) {
547 $buttonStyle = '';
548 if (isset($inlineConfiguration['inline']['inlineOnlineMediaAddButtonStyle'])) {
549 $buttonStyle = ' style="' . $inlineConfiguration['inline']['inlineOnlineMediaAddButtonStyle'] . '"';
550 }
551 $this->requireJsModules[] = 'TYPO3/CMS/Backend/OnlineMedia';
552 $buttonText = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.button'));
553 $placeholder = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.placeholder'));
554 $buttonSubmit = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.submit'));
555 $allowedMediaUrl = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.allowEmbedSources'));
556 $item .= '
557 <span class="btn btn-default t3js-online-media-add-btn"
558 ' . $buttonStyle . '
559 data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
560 data-online-media-allowed="' . htmlspecialchars(implode(',', $onlineMediaAllowed)) . '"
561 data-online-media-allowed-help-text="' . $allowedMediaUrl . '"
562 data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
563 title="' . $buttonText . '"
564 data-btn-submit="' . $buttonSubmit . '"
565 data-placeholder="' . $placeholder . '"
566 >
567 ' . $this->iconFactory->getIcon('actions-online-media-add', Icon::SIZE_SMALL)->render() . '
568 ' . $buttonText . '</span>';
569 }
570 }
571 }
572
573 $item = '<div class="form-control-wrap t3js-inline-controls">' . $item . '</div>';
574 $allowedList = '';
575 $allowedLabelKey = ($mode === 'file') ? 'allowedFileExtensions' : 'allowedRelations';
576 $allowedLabel = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . $allowedLabelKey));
577 foreach ($allowedArray as $allowedItem) {
578 $allowedList .= '<span class="label label-success">' . strtoupper($allowedItem) . '</span> ';
579 }
580 if (!empty($allowedList)) {
581 $item .= '<div class="help-block">' . $allowedLabel . '<br>' . $allowedList . '</div>';
582 }
583 $item = '<div class="form-group t3js-formengine-validation-marker">' . $item . '</div>';
584 return $item;
585 }
586
587 /**
588 * Get a selector as used for the select type, to select from all available
589 * records and to create a relation to the embedding record (e.g. like MM).
590 *
591 * @param array $config TCA inline configuration of the parent(!) field
592 * @param array $uniqueIds The uids that have already been used and should be unique
593 * @return string A HTML <select> box with all possible records
594 */
595 protected function renderPossibleRecordsSelectorTypeSelect(array $config, array $uniqueIds)
596 {
597 $possibleRecords = $config['selectorOrUniquePossibleRecords'];
598 $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
599 // Create option tags:
600 $opt = [];
601 foreach ($possibleRecords as $p) {
602 if (!in_array($p[1], $uniqueIds)) {
603 $opt[] = '<option value="' . htmlspecialchars($p[1]) . '">' . htmlspecialchars($p[0]) . '</option>';
604 }
605 }
606 // Put together the selector box:
607 $size = (int)$config['size'];
608 $size = $config['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($possibleRecords) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax']) : $size;
609 $item = '
610 <select id="' . $nameObject . '-' . $config['foreign_table'] . '_selector" class="form-control t3js-create-new-selector"' . ($size ? ' size="' . $size . '"' : '') . '>
611 ' . implode('', $opt) . '
612 </select>';
613
614 if ($size <= 1) {
615 // Add a "Create new relation" link for adding new relations
616 // This is necessary, if the size of the selector is "1" or if
617 // there is only one record item in the select-box, that is selected by default
618 // The selector-box creates a new relation on using an onChange event (see some line above)
619 if (!empty($config['appearance']['createNewRelationLinkTitle'])) {
620 $createNewRelationText = htmlspecialchars($this->getLanguageService()->sL($config['appearance']['createNewRelationLinkTitle']));
621 } else {
622 $createNewRelationText = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation'));
623 }
624 $item .= '
625 <span class="input-group-btn">
626 <a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onChange) . '" title="' . $createNewRelationText . '">
627 ' . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . $createNewRelationText . '
628 </a>
629 </span>';
630 } else {
631 $item .= '
632 <span class="input-group-btn btn"></span>';
633 }
634
635 // Wrap the selector and add a spacer to the bottom
636 $item = '<div class="input-group form-group t3js-formengine-validation-marker">' . $item . '</div>';
637 return $item;
638 }
639
640 /**
641 * Extracts FlexForm parts of a form element name like
642 * data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
643 * Helper method used in inline
644 *
645 * @param string $formElementName The form element name
646 * @return array|null
647 */
648 protected function extractFlexFormParts($formElementName)
649 {
650 $flexFormParts = null;
651 $matches = [];
652 if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
653 $flexFormParts = GeneralUtility::trimExplode(
654 '][',
655 trim($matches[1], '[]')
656 );
657 }
658 return $flexFormParts;
659 }
660
661 /**
662 * @return BackendUserAuthentication
663 */
664 protected function getBackendUserAuthentication()
665 {
666 return $GLOBALS['BE_USER'];
667 }
668
669 /**
670 * @return LanguageService
671 */
672 protected function getLanguageService()
673 {
674 return $GLOBALS['LANG'];
675 }
676 }