/** * @dataProvider structureStringIsParsedDataProvider * @test */ public function getCurrentStructureDomObjectIdPrefixReturnsExceptedStringAfterInitializationByStructureString($string, array $_, array $expectedFormName) { /** @var InlineStackProcessor|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject */ $subject = new InlineStackProcessor(); $subject->initializeByParsingDomObjectIdString($string); $this->assertEquals($expectedFormName['object'], $subject->getCurrentStructureDomObjectIdPrefix('pageId')); }
/** * Get a selector as used for the select type, to select from all available * records and to create a relation to the embedding record (e.g. like MM). * * @param array $selItems Array of all possible records * @param array $conf TCA configuration of the parent(!) field * @param array $PA An array with additional configuration options * @param array $uniqueIds The uids that have already been used and should be unique * @return string A HTML <select> box with all possible records */ protected function renderPossibleRecordsSelectorTypeSelect($selItems, $conf, &$PA, $uniqueIds = array()) { $foreign_table = $conf['foreign_table']; $foreign_selector = $conf['foreign_selector']; $PA = array(); $PA['fieldConf'] = $GLOBALS['TCA'][$foreign_table]['columns'][$foreign_selector]; $PA['fieldTSConfig'] = FormEngineUtility::getTSconfigForTableRow($foreign_table, array(), $foreign_selector); $config = $PA['fieldConf']['config']; $item = ''; // @todo $disabled is not present - should be read from config? $disabled = FALSE; if (!$disabled) { $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->globalOptions['inlineFirstPid']); // Create option tags: $opt = array(); $styleAttrValue = ''; foreach ($selItems as $p) { if ($config['iconsInOptionTags']) { $styleAttrValue = FormEngineUtility::optionTagStyle($p[2]); } if (!in_array($p[1], $uniqueIds)) { $opt[] = '<option value="' . htmlspecialchars($p[1]) . '"' . ($styleAttrValue ? ' style="' . htmlspecialchars($styleAttrValue) . '"' : '') . '>' . htmlspecialchars($p[0]) . '</option>'; } } // Put together the selector box: $itemListStyle = isset($config['itemListStyle']) ? ' style="' . htmlspecialchars($config['itemListStyle']) . '"' : ''; $size = (int) $conf['size']; $size = $conf['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($selItems) + 1, MathUtility::forceIntegerInRange($size, 1), $conf['autoSizeMax']) : $size; $onChange = 'return inline.importNewRecord(' . GeneralUtility::quoteJSvalue($nameObject . '-' . $conf['foreign_table']) . ')'; $item = ' <select id="' . $nameObject . '-' . $conf['foreign_table'] . '_selector" class="form-control"' . ($size ? ' size="' . $size . '"' : '') . ' onchange="' . htmlspecialchars($onChange) . '"' . $PA['onFocus'] . $itemListStyle . ($conf['foreign_unique'] ? ' isunique="isunique"' : '') . '> ' . implode('', $opt) . ' </select>'; if ($size <= 1) { // Add a "Create new relation" link for adding new relations // This is necessary, if the size of the selector is "1" or if // there is only one record item in the select-box, that is selected by default // The selector-box creates a new relation on using a onChange event (see some line above) if (!empty($conf['appearance']['createNewRelationLinkTitle'])) { $createNewRelationText = $this->getLanguageService()->sL($conf['appearance']['createNewRelationLinkTitle'], TRUE); } else { $createNewRelationText = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', TRUE); } $item .= ' <span class="input-group-btn"> <a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onChange) . '"> ' . IconUtility::getSpriteIcon('actions-document-new', array('title' => $createNewRelationText)) . $createNewRelationText . ' </a> </span>'; } else { $item .= ' <span class="input-group-btn btn"></span>'; } // Wrap the selector and add a spacer to the bottom $item = '<div class="input-group form-group t3js-formengine-validation-marker ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>'; } return $item; }
/** * Get a selector as used for the select type, to select from all available * records and to create a relation to the embedding record (e.g. like MM). * * @param array $config TCA inline configuration of the parent(!) field * @param array $uniqueIds The uids that have already been used and should be unique * @return string A HTML <select> box with all possible records */ protected function renderPossibleRecordsSelectorTypeSelect(array $config, array $uniqueIds) { $possibleRecords = $config['selectorOrUniquePossibleRecords']; $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']); // Create option tags: $opt = []; foreach ($possibleRecords as $p) { if (!in_array($p[1], $uniqueIds)) { $opt[] = '<option value="' . htmlspecialchars($p[1]) . '">' . htmlspecialchars($p[0]) . '</option>'; } } // Put together the selector box: $size = (int) $config['size']; $size = $config['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($possibleRecords) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax']) : $size; $onChange = 'return inline.importNewRecord(' . GeneralUtility::quoteJSvalue($nameObject . '-' . $config['foreign_table']) . ')'; $item = ' <select id="' . $nameObject . '-' . $config['foreign_table'] . '_selector" class="form-control"' . ($size ? ' size="' . $size . '"' : '') . ' onchange="' . htmlspecialchars($onChange) . '"' . ($config['foreign_unique'] ? ' isunique="isunique"' : '') . '> ' . implode('', $opt) . ' </select>'; if ($size <= 1) { // Add a "Create new relation" link for adding new relations // This is necessary, if the size of the selector is "1" or if // there is only one record item in the select-box, that is selected by default // The selector-box creates a new relation on using an onChange event (see some line above) if (!empty($config['appearance']['createNewRelationLinkTitle'])) { $createNewRelationText = $this->getLanguageService()->sL($config['appearance']['createNewRelationLinkTitle'], true); } else { $createNewRelationText = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:cm.createNewRelation', true); } $item .= ' <span class="input-group-btn"> <a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onChange) . '" title="' . $createNewRelationText . '"> ' . $this->iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render() . $createNewRelationText . ' </a> </span>'; } else { $item .= ' <span class="input-group-btn btn"></span>'; } // Wrap the selector and add a spacer to the bottom $item = '<div class="input-group form-group t3js-formengine-validation-marker ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>'; return $item; }
/** * Render the control-icons for a record header (create new, sorting, delete, disable/enable). * Most of the parts are copy&paste from TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList and * modified for the JavaScript calls here * * @param array $data Current data * @return string The HTML code with the control-icons */ protected function renderForeignRecordHeaderControl(array $data) { $rec = $data['databaseRow']; $inlineConfig = $data['inlineParentConfig']; $foreignTable = $inlineConfig['foreign_table']; $languageService = $this->getLanguageService(); $backendUser = $this->getBackendUserAuthentication(); // Initialize: $cells = array(); $additionalCells = array(); $isNewItem = substr($rec['uid'], 0, 3) == 'NEW'; $isParentExisting = MathUtility::canBeInterpretedAsInteger($data['inlineParentUid']); $tcaTableCtrl =& $GLOBALS['TCA'][$foreignTable]['ctrl']; $tcaTableCols =& $GLOBALS['TCA'][$foreignTable]['columns']; $isPagesTable = $foreignTable === 'pages'; $isSysFileReferenceTable = $foreignTable === 'sys_file_reference'; $enableManualSorting = $tcaTableCtrl['sortby'] || $inlineConfig['MM'] || !$data['isOnSymmetricSide'] && $inlineConfig['foreign_sortby'] || $data['isOnSymmetricSide'] && $inlineConfig['symmetric_sortby']; $nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']); $nameObjectFt = $nameObject . '-' . $foreignTable; $nameObjectFtId = $nameObjectFt . '-' . $rec['uid']; $calcPerms = $backendUser->calcPerms(BackendUtility::readPageAccess($rec['pid'], $backendUser->getPagePermsClause(1))); // If the listed table is 'pages' we have to request the permission settings for each page: $localCalcPerms = false; if ($isPagesTable) { $localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid'])); } // This expresses the edit permissions for this particular element: $permsEdit = $isPagesTable && $localCalcPerms & Permission::PAGE_EDIT || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT; // Controls: Defines which controls should be shown $enabledControls = $inlineConfig['appearance']['enabledControls']; // Hook: Can disable/enable single controls for specific child records: foreach ($this->hookObjects as $hookObj) { /** @var InlineElementHookInterface $hookObj */ $hookObj->renderForeignRecordHeaderControl_preProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $enabledControls); } if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { $cells['localize.isLocalizable'] = '<span title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize.isLocalizable', true) . '">' . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render() . '</span>'; } // "Info": (All records) if ($enabledControls['info'] && !$isNewItem) { if ($rec['table_local'] === 'sys_file') { $uid = (int) substr($rec['uid_local'], 9); $table = '_FILE'; } else { $uid = $rec['uid']; $table = $foreignTable; } $cells['info'] = ' <a class="btn btn-default" href="#" onclick="' . htmlspecialchars('top.launchView(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($uid) . '); return false;') . '" title="' . $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:showInfo', true) . '"> ' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . ' </a>'; } // If the table is NOT a read-only table, then show these links: if (!$tcaTableCtrl['readOnly'] && !$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { // "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row or if default values can depend on previous record): if ($enabledControls['new'] && ($enableManualSorting || $tcaTableCtrl['useColumnsForDefaultValues'])) { if (!$isPagesTable && $calcPerms & Permission::CONTENT_EDIT || $isPagesTable && $calcPerms & Permission::PAGE_NEW) { $onClick = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ',' . GeneralUtility::quoteJSvalue($rec['uid']) . ')'; $style = ''; if ($inlineConfig['inline']['inlineNewButtonStyle']) { $style = ' style="' . $inlineConfig['inline']['inlineNewButtonStyle'] . '"'; } $cells['new'] = ' <a class="btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'] . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:new' . ($isPagesTable ? 'Page' : 'Record'), true) . '" ' . $style . '> ' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page' : 'document') . '-new', Icon::SIZE_SMALL)->render() . ' </a>'; } } // "Up/Down" links if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) { // Up $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'1\')'; $style = $inlineConfig['inline']['first'] == $rec['uid'] ? 'style="visibility: hidden;"' : ''; $cells['sort.up'] = ' <a class="btn btn-default sortingUp" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . ' title="' . $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveUp', true) . '"> ' . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . ' </a>'; // Down $onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'-1\')'; $style = $inlineConfig['inline']['last'] == $rec['uid'] ? 'style="visibility: hidden;"' : ''; $cells['sort.down'] = ' <a class="btn btn-default sortingDown" href="#" onclick="' . htmlspecialchars($onClick) . '" ' . $style . ' title="' . $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:moveDown', true) . '"> ' . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . ' </a>'; } // "Edit" link: if ($rec['table_local'] === 'sys_file' && !$isNewItem) { $sys_language_uid = 0; if (!empty($rec['sys_language_uid'])) { $sys_language_uid = $rec['sys_language_uid'][0]; } $recordInDatabase = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('uid', 'sys_file_metadata', 'file = ' . (int) substr($rec['uid_local'], 9) . ' AND sys_language_uid = ' . $sys_language_uid); if ($backendUser->check('tables_modify', 'sys_file_metadata')) { $url = BackendUtility::getModuleUrl('record_edit', array('edit[sys_file_metadata][' . (int) $recordInDatabase['uid'] . ']' => 'edit')); $editOnClick = 'if (top.content.list_frame) {' . 'top.content.list_frame.location.href=' . GeneralUtility::quoteJSvalue($url . '&returnUrl=') . '+top.rawurlencode(top.content.list_frame.document.location.pathname+top.content.list_frame.document.location.search)' . ';' . '}'; $title = $languageService->sL('LLL:EXT:lang/locallang_core.xlf:cm.editMetadata'); $cells['editmetadata'] = ' <a class="btn btn-default" href="#" class="btn" onclick="' . htmlspecialchars($editOnClick) . '" title="' . htmlspecialchars($title) . '"> ' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . ' </a>'; } } // "Delete" link: if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & Permission::PAGE_DELETE || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT || $isSysFileReferenceTable && $calcPerms & Permission::PAGE_EDIT)) { $title = $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:delete', true); $icon = $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render(); $cells['delete'] = '<a href="#" class="btn btn-default t3js-editform-delete-inline-record" data-objectid="' . htmlspecialchars($nameObjectFtId) . '" title="' . $title . '">' . $icon . '</a>'; } // "Hide/Unhide" links: $hiddenField = $tcaTableCtrl['enablecolumns']['disabled']; if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))) { $onClick = 'return inline.enableDisableRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ')'; $className = 't3js-' . $nameObjectFtId . '_disabled'; if ($rec[$hiddenField]) { $title = $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : ''), true); $cells['hide.unhide'] = ' <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">' . $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . ' </a>'; } else { $title = $languageService->sL('LLL:EXT:lang/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : ''), true); $cells['hide.hide'] = ' <a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">' . $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . ' </a>'; } } // Drag&Drop Sorting: Sortable handler for script.aculo.us if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $inlineConfig['appearance']['useSortable']) { $additionalCells['dragdrop'] = ' <span class="btn btn-default sortableHandle" data-id="' . htmlspecialchars($rec['uid']) . '" title="' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.move', true) . '"> ' . $this->iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . ' </span>'; } } elseif ($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] && $isParentExisting) { if ($enabledControls['localize'] && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) { $onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');'; $cells['localize'] = ' <a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $languageService->sL('LLL:EXT:lang/locallang_misc.xlf:localize', true) . '"> ' . $this->iconFactory->getIcon('actions-document-localize', Icon::SIZE_SMALL)->render() . ' </a>'; } } // If the record is edit-locked by another user, we will show a little warning sign: if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) { $cells['locked'] = ' <a class="btn btn-default" href="#" onclick="alert(' . GeneralUtility::quoteJSvalue($lockInfo['msg']) . ');return false;"> ' . '<span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $this->iconFactory->getIcon('status-warning-in-use', Icon::SIZE_SMALL)->render() . '</span>' . ' </a>'; } // Hook: Post-processing of single controls for specific child records: foreach ($this->hookObjects as $hookObj) { $hookObj->renderForeignRecordHeaderControl_postProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $cells); } $out = ''; if (!empty($cells)) { $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>'; } if (!empty($additionalCells)) { $out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $additionalCells) . '</div>'; } return $out; }
/** * Determines and sets several script calls to a JSON array, that would have been executed if processed in non-AJAX mode. * * @param array &$jsonArray Reference of the array to be used for JSON * @param array $config The configuration of the IRRE field of the parent record * @param int $inlineFirstPid Inline first pid * @return array Modified array * @todo: Basically, this methods shouldn't be there at all ... */ protected function getInlineAjaxCommonScriptCalls($jsonArray, $config, $inlineFirstPid) { // Add data that would have been added at the top of a regular FormEngine call: if ($headTags = $this->getInlineHeadTags()) { $jsonArray['headData'] = $headTags; } // Add the JavaScript data that would have been added at the bottom of a regular FormEngine call: $jsonArray['scriptCall'][] = $this->JSbottom('editform', TRUE); // If script.aculo.us Sortable is used, update the Observer to know the record: if ($config['appearance']['useSortable']) { $inlineObjectName = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid); $jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');'; } // If FormEngine has some JavaScript code to be executed, just do it // @todo: this is done by JSBottom() already?! if ($this->extJSCODE) { $jsonArray['scriptCall'][] = $this->extJSCODE; } // require js handling foreach ($this->requireJsModules as $moduleName => $callbacks) { if (!is_array($callbacks)) { $callbacks = array($callbacks); } foreach ($callbacks as $callback) { $inlineCodeKey = $moduleName; $javaScriptCode = 'require(["' . $moduleName . '"]'; if ($callback !== NULL) { $inlineCodeKey .= sha1($callback); $javaScriptCode .= ', ' . $callback; } $javaScriptCode .= ');'; $jsonArray['scriptCall'][] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode; } } return $jsonArray; }