/** * Entry method * * @return array As defined in initializeResultArray() of AbstractNode */ public function render() { $languageService = $this->getLanguageService(); $this->inlineData = $this->data['inlineData']; /** @var InlineStackProcessor $inlineStackProcessor */ $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); $this->inlineStackProcessor = $inlineStackProcessor; $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']); $table = $this->data['tableName']; $row = $this->data['databaseRow']; $field = $this->data['fieldName']; $parameterArray = $this->data['parameterArray']; $resultArray = $this->initializeResultArray(); $config = $parameterArray['fieldConf']['config']; $foreign_table = $config['foreign_table']; $language = 0; if (BackendUtility::isTableLocalizable($table)) { $language = (int) $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; } // Add the current inline job to the structure stack $newStructureItem = array('table' => $table, 'uid' => $row['uid'], 'field' => $field, 'config' => $config, 'localizationMode' => BackendUtility::getInlineLocalizationMode($table, $config)); // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF') if (!empty($parameterArray['itemFormElName'])) { $flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']); if ($flexFormParts !== null) { $newStructureItem['flexform'] = $flexFormParts; } } $inlineStackProcessor->pushStableStructureItem($newStructureItem); // e.g. data[<table>][<uid>][<field>] $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix(); // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2> $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']); $config['inline']['first'] = false; // @todo: This initialization shouldn't be required data provider should take care this is set? if (!is_array($this->data['parameterArray']['fieldConf']['children'])) { $this->data['parameterArray']['fieldConf']['children'] = array(); } $firstChild = reset($this->data['parameterArray']['fieldConf']['children']); if (isset($firstChild['databaseRow']['uid'])) { $config['inline']['first'] = $firstChild['databaseRow']['uid']; } $config['inline']['last'] = false; $lastChild = end($this->data['parameterArray']['fieldConf']['children']); if (isset($lastChild['databaseRow']['uid'])) { $config['inline']['last'] = $lastChild['databaseRow']['uid']; } $top = $inlineStackProcessor->getStructureLevel(0); $this->inlineData['config'][$nameObject] = array('table' => $foreign_table, 'md5' => md5($nameObject)); $this->inlineData['config'][$nameObject . '-' . $foreign_table] = array('min' => $config['minitems'], 'max' => $config['maxitems'], 'sortable' => $config['appearance']['useSortable'], 'top' => array('table' => $top['table'], 'uid' => $top['uid']), 'context' => array('config' => $config, 'hmac' => GeneralUtility::hmac(serialize($config)))); $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack']; // If relations are required to be unique, get the uids that have already been used on the foreign side of the relation $uniqueMax = 0; $possibleRecords = []; $uniqueIds = []; if ($config['foreign_unique']) { // If uniqueness *and* selector are set, they should point to the same field - so, get the configuration of one: $selConfig = FormEngineUtility::getInlinePossibleRecordsSelectorConfig($config, $config['foreign_unique']); // Get the used unique ids: $uniqueIds = $this->getUniqueIds($this->data['parameterArray']['fieldConf']['children'], $config, $selConfig['type'] == 'groupdb'); $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config, 'foreign_unique'); $uniqueMax = $config['appearance']['useCombination'] || $possibleRecords === false ? -1 : count($possibleRecords); $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = array('max' => $uniqueMax, 'used' => $uniqueIds, 'type' => $selConfig['type'], 'table' => $config['foreign_table'], 'elTable' => $selConfig['table'], 'field' => $config['foreign_unique'], 'selector' => $selConfig['selector'], 'possible' => $this->getPossibleRecordsFlat($possibleRecords)); } $resultArray['inlineData'] = $this->inlineData; // Render the localization links $localizationLinks = ''; if ($language > 0 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0 && MathUtility::canBeInterpretedAsInteger($row['uid'])) { // Add the "Localize all records" link before all child records: if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) { $localizationLinks .= ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config); } // Add the "Synchronize with default language" link before all child records: if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) { $localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config); } } $numberOfFullChildren = 0; foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) { if (!$child['inlineIsDefaultLanguage']) { $numberOfFullChildren++; } } // Define how to show the "Create new record" link - if there are more than maxitems, hide it if ($numberOfFullChildren >= $config['maxitems'] || $uniqueMax > 0 && $numberOfFullChildren >= $uniqueMax) { $config['inline']['inlineNewButtonStyle'] = 'display: none;'; $config['inline']['inlineNewRelationButtonStyle'] = 'display: none;'; } // Render the level links (create new record): $levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config); // Wrap all inline fields of a record with a <div> (like a container) $html = '<div class="form-group" id="' . $nameObject . '">'; // Add the level links before all child records: if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') { $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>'; } // If it's required to select from possible child records (reusable children), add a selector box if ($config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== false) { // If not already set by the foreign_unique, set the possibleRecords here and the uniqueIds to an empty array if (!$config['foreign_unique']) { $possibleRecords = $this->getPossibleRecords($table, $field, $row, $config); $uniqueIds = array(); } $selectorBox = $this->renderPossibleRecordsSelector($possibleRecords, $config, $uniqueIds); $html .= $selectorBox . $localizationLinks; } $title = $languageService->sL($parameterArray['fieldConf']['label']); $html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">'; $sortableRecordUids = []; foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) { $options['inlineParentUid'] = $row['uid']; // @todo: this can be removed if this container no longer sets additional info to $config $options['inlineParentConfig'] = $config; $options['inlineData'] = $this->inlineData; $options['inlineStructure'] = $inlineStackProcessor->getStructure(); $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray']; $options['renderType'] = 'inlineRecordContainer'; $childResult = $this->nodeFactory->create($options)->render(); $html .= $childResult['html']; $childArray['html'] = ''; $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult); if (!$options['inlineIsDefaultLanguage']) { // Don't add record to list of "valid" uids if it is only the default // language record of a not yet localized child $sortableRecordUids[] = $options['databaseRow']['uid']; } } $html .= '</div>'; // Add the level links after all child records: if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom') { $html .= $levelLinks . $localizationLinks; } if (is_array($config['customControls'])) { $html .= '<div id="' . $nameObject . '_customControls">'; foreach ($config['customControls'] as $customControlConfig) { $parameters = array('table' => $table, 'field' => $field, 'row' => $row, 'nameObject' => $nameObject, 'nameForm' => $nameForm, 'config' => $config); $html .= GeneralUtility::callUserFunction($customControlConfig, $parameters, $this); } $html .= '</div>'; } // Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post if (count($sortableRecordUids) > 1 && $config['appearance']['useSortable']) { $resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");'; } // Publish the uids of the child records in the given order to the browser $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" ' . $this->getValidationDataAsDataAttribute(array('type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems'])) . ' class="inlineRecord" />'; // Close the wrap for all inline fields (container) $html .= '</div>'; $resultArray['html'] = $html; return $resultArray; }
/** * Convert the DOM object-id of an inline container to an array. * The object-id could look like 'data-parentPageId-tx_mmftest_company-1-employees'. * The result is written to $this->inlineStructure. * There are two keys: * - 'stable': Containing full qualified identifiers (table, uid and field) * - 'unstable': Containting partly filled data (e.g. only table and possibly field) * * @param string $domObjectId The DOM object-id * @param boolean $loadConfig Load the TCA configuration for that level (default: TRUE) * @return void * @todo Define visibility */ public function parseStructureString($string, $loadConfig = TRUE) { $unstable = array(); $vector = array('table', 'uid', 'field'); // Substitute FlexForm additon and make parsing a bit easier $string = str_replace(self::FlexForm_Separator, self::FlexForm_Substitute, $string); // The starting pattern of an object identifer (e.g. "data-<firstPidValue>-<anything>) $pattern = '/^' . $this->prependNaming . self::Structure_Separator . '(.+?)' . self::Structure_Separator . '(.+)$/'; if (preg_match($pattern, $string, $match)) { $this->inlineFirstPid = $match[1]; $parts = explode(self::Structure_Separator, $match[2]); $partsCnt = count($parts); for ($i = 0; $i < $partsCnt; $i++) { if ($i > 0 && $i % 3 == 0) { // Load the TCA configuration of the table field and store it in the stack if ($loadConfig) { $unstable['config'] = $GLOBALS['TCA'][$unstable['table']]['columns'][$unstable['field']]['config']; // Fetch TSconfig: $TSconfig = $this->fObj->setTSconfig($unstable['table'], array('uid' => $unstable['uid'], 'pid' => $this->inlineFirstPid), $unstable['field']); // Override TCA field config by TSconfig: if (!$TSconfig['disabled']) { $unstable['config'] = $this->fObj->overrideFieldConf($unstable['config'], $TSconfig); } $unstable['localizationMode'] = BackendUtility::getInlineLocalizationMode($unstable['table'], $unstable['config']); } // Extract FlexForm from field part (if any) if (strpos($unstable['field'], self::FlexForm_Substitute) !== FALSE) { $fieldParts = GeneralUtility::trimExplode(self::FlexForm_Substitute, $unstable['field']); $unstable['field'] = array_shift($fieldParts); // FlexForm parts start with data: if (count($fieldParts) > 0 && $fieldParts[0] === 'data') { $unstable['flexform'] = $fieldParts; } } $this->inlineStructure['stable'][] = $unstable; $unstable = array(); } $unstable[$vector[$i % 3]] = $parts[$i]; } $this->updateStructureNames(); if (count($unstable)) { $this->inlineStructure['unstable'] = $unstable; } } }
/** * Performs localization or synchronization of child records. * * @param string $table The table of the localized parent record * @param int $id The uid of the localized parent record * @param string $command Defines the type 'localize' or 'synchronize' (string) or a single uid to be localized (int) * @return void */ protected function inlineLocalizeSynchronize($table, $id, $command) { // <field>, (localize | synchronize | <uid>): $parts = GeneralUtility::trimExplode(',', $command); $field = $parts[0]; $type = $parts[1]; if (!$field || $type !== 'localize' && $type !== 'synchronize' && !MathUtility::canBeInterpretedAsInteger($type) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) { return; } $config = $GLOBALS['TCA'][$table]['columns'][$field]['config']; $foreignTable = $config['foreign_table']; $localizationMode = BackendUtility::getInlineLocalizationMode($table, $config); if ($localizationMode != 'select') { return; } $parentRecord = BackendUtility::getRecordWSOL($table, $id); $language = (int) $parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; $transOrigPointer = (int) $parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]; $transOrigTable = BackendUtility::getOriginalTranslationTable($table); $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField']; if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) { return; } $inlineSubType = $this->getInlineFieldType($config); $transOrigRecord = BackendUtility::getRecordWSOL($transOrigTable, $transOrigPointer); if ($inlineSubType === false) { return; } $removeArray = array(); $mmTable = $inlineSubType == 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : ''; // Fetch children from original language parent: /** @var $dbAnalysisOriginal RelationHandler */ $dbAnalysisOriginal = $this->createRelationHandlerInstance(); $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $transOrigTable, $config); $elementsOriginal = array(); foreach ($dbAnalysisOriginal->itemArray as $item) { $elementsOriginal[$item['id']] = $item; } unset($dbAnalysisOriginal); // Fetch children from current localized parent: /** @var $dbAnalysisCurrent RelationHandler */ $dbAnalysisCurrent = $this->createRelationHandlerInstance(); $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config); // Perform synchronization: Possibly removal of already localized records: if ($type == 'synchronize') { foreach ($dbAnalysisCurrent->itemArray as $index => $item) { $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']); if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) { $childTransOrigPointer = $childRecord[$childTransOrigPointerField]; // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it: if (!isset($elementsOriginal[$childTransOrigPointer])) { unset($dbAnalysisCurrent->itemArray[$index]); $removeArray[$item['table']][$item['id']]['delete'] = 1; } } } } // Perform synchronization/localization: Possibly add unlocalized records for original language: if (MathUtility::canBeInterpretedAsInteger($type) && isset($elementsOriginal[$type])) { $item = $elementsOriginal[$type]; $item['id'] = $this->localize($item['table'], $item['id'], $language); $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']); $dbAnalysisCurrent->itemArray[] = $item; } elseif ($type === 'localize' || $type === 'synchronize') { foreach ($elementsOriginal as $originalId => $item) { $item['id'] = $this->localize($item['table'], $item['id'], $language); $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']); $dbAnalysisCurrent->itemArray[] = $item; } } // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record): $value = implode(',', $dbAnalysisCurrent->getValueArray()); $this->registerDBList[$table][$id][$field] = $value; // Remove child records (if synchronization requested it): if (is_array($removeArray) && !empty($removeArray)) { /** @var DataHandler $tce */ $tce = GeneralUtility::makeInstance(__CLASS__); $tce->stripslashes_values = false; $tce->start(array(), $removeArray); $tce->process_cmdmap(); unset($tce); } $updateFields = array(); // Handle, reorder and store relations: if ($inlineSubType == 'list') { $updateFields = array($field => $value); } elseif ($inlineSubType == 'field') { $dbAnalysisCurrent->writeForeignField($config, $id); $updateFields = array($field => $dbAnalysisCurrent->countItems(false)); } elseif ($inlineSubType == 'mm') { $dbAnalysisCurrent->writeMM($config['MM'], $id); $updateFields = array($field => $dbAnalysisCurrent->countItems(false)); } // Update field referencing to child records of localized parent record: if (!empty($updateFields)) { $this->updateDB($table, $id, $updateFields); } }
/** * Injects configuration via AJAX calls. * This is used by inline ajax calls that transfer configuration options back to the stack for initialization * The configuration is validated using HMAC to avoid hijacking. * * @param string $contextString Given context string from ajax call * @return void * @todo: Review this construct - Why can't the ajax call fetch these data on its own and transfers it to client instead? */ public function injectAjaxConfiguration($contextString = '') { $level = $this->calculateStructureLevel(-1); if (empty($contextString) || $level === FALSE) { return; } $current =& $this->inlineStructure['stable'][$level]; $context = json_decode($contextString, TRUE); if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) { return; } $current['config'] = $context['config']; $current['localizationMode'] = BackendUtility::getInlineLocalizationMode($current['table'], $current['config']); }
/** * Entry method * * @return array As defined in initializeResultArray() of AbstractNode */ public function render() { $languageService = $this->getLanguageService(); $this->inlineData = $this->data['inlineData']; /** @var InlineStackProcessor $inlineStackProcessor */ $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); $this->inlineStackProcessor = $inlineStackProcessor; $inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']); $table = $this->data['tableName']; $row = $this->data['databaseRow']; $field = $this->data['fieldName']; $parameterArray = $this->data['parameterArray']; $resultArray = $this->initializeResultArray(); $config = $parameterArray['fieldConf']['config']; $foreign_table = $config['foreign_table']; $language = 0; if (BackendUtility::isTableLocalizable($table)) { $language = (int) $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; } // Add the current inline job to the structure stack $newStructureItem = array('table' => $table, 'uid' => $row['uid'], 'field' => $field, 'config' => $config, 'localizationMode' => BackendUtility::getInlineLocalizationMode($table, $config)); // Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF') if (!empty($parameterArray['itemFormElName'])) { $flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']); if ($flexFormParts !== null) { $newStructureItem['flexform'] = $flexFormParts; } } $inlineStackProcessor->pushStableStructureItem($newStructureItem); // Transport the flexform DS identifier fields to the FormAjaxInlineController if (!empty($newStructureItem['flexform']) && isset($this->data['processedTca']['columns'][$field]['config']['ds']['meta']['dataStructurePointers'])) { $config['flexDataStructurePointers'] = $this->data['processedTca']['columns'][$field]['config']['ds']['meta']['dataStructurePointers']; } // e.g. data[<table>][<uid>][<field>] $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix(); // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2> $nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']); $config['inline']['first'] = false; // @todo: This initialization shouldn't be required data provider should take care this is set? if (!is_array($this->data['parameterArray']['fieldConf']['children'])) { $this->data['parameterArray']['fieldConf']['children'] = array(); } $firstChild = reset($this->data['parameterArray']['fieldConf']['children']); if (isset($firstChild['databaseRow']['uid'])) { $config['inline']['first'] = $firstChild['databaseRow']['uid']; } $config['inline']['last'] = false; $lastChild = end($this->data['parameterArray']['fieldConf']['children']); if (isset($lastChild['databaseRow']['uid'])) { $config['inline']['last'] = $lastChild['databaseRow']['uid']; } $top = $inlineStackProcessor->getStructureLevel(0); $this->inlineData['config'][$nameObject] = array('table' => $foreign_table, 'md5' => md5($nameObject)); $this->inlineData['config'][$nameObject . '-' . $foreign_table] = array('min' => $config['minitems'], 'max' => $config['maxitems'], 'sortable' => $config['appearance']['useSortable'], 'top' => array('table' => $top['table'], 'uid' => $top['uid']), 'context' => array('config' => $config, 'hmac' => GeneralUtility::hmac(serialize($config)))); $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack']; $uniqueMax = 0; $uniqueIds = []; if ($config['foreign_unique']) { // Add inlineData['unique'] with JS unique configuration $type = $config['selectorOrUniqueConfiguration']['config']['type'] === 'select' ? 'select' : 'groupdb'; foreach ($parameterArray['fieldConf']['children'] as $child) { // Determine used unique ids, skip not localized records if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) { $value = $child['databaseRow'][$config['foreign_unique']]; // We're assuming there is only one connected value here for both select and group if ($type === 'select') { // A resolved select field is an array - take first value $value = $value['0']; } else { // A group field is still a list with pipe separated uid|tableName $valueParts = GeneralUtility::trimExplode('|', $value); $itemParts = explode('_', $valueParts[0]); $value = array('uid' => array_pop($itemParts), 'table' => implode('_', $itemParts)); } // @todo: This is weird, $value has different structure for group and select fields? $uniqueIds[$child['databaseRow']['uid']] = $value; } } $possibleRecords = $config['selectorOrUniquePossibleRecords']; $possibleRecordsUidToTitle = []; foreach ($possibleRecords as $possibleRecord) { $possibleRecordsUidToTitle[$possibleRecord[1]] = $possibleRecord[0]; } $uniqueMax = $config['appearance']['useCombination'] || empty($possibleRecords) ? -1 : count($possibleRecords); $this->inlineData['unique'][$nameObject . '-' . $foreign_table] = array('max' => $uniqueMax, 'used' => $uniqueIds, 'type' => $type, 'table' => $foreign_table, 'elTable' => $config['selectorOrUniqueConfiguration']['foreignTable'], 'field' => $config['foreign_unique'], 'selector' => $config['selectorOrUniqueConfiguration']['isSelector'] ? $type : false, 'possible' => $possibleRecordsUidToTitle); } $resultArray['inlineData'] = $this->inlineData; // @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider $isLocalizedParent = $language > 0 && $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']][0] > 0 && MathUtility::canBeInterpretedAsInteger($row['uid']); $numberOfFullLocalizedChildren = 0; $numberOfNotYetLocalizedChildren = 0; foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) { if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) { $numberOfFullLocalizedChildren++; } if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) { $numberOfNotYetLocalizedChildren++; } } // Render the localization links if needed $localizationLinks = ''; if ($numberOfNotYetLocalizedChildren) { // Add the "Localize all records" link before all child records: if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) { $localizationLinks = ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config); } // Add the "Synchronize with default language" link before all child records: if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) { $localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config); } } // Define how to show the "Create new record" link - if there are more than maxitems, hide it if ($numberOfFullLocalizedChildren >= $config['maxitems'] || $uniqueMax > 0 && $numberOfFullLocalizedChildren >= $uniqueMax) { $config['inline']['inlineNewButtonStyle'] = 'display: none;'; $config['inline']['inlineNewRelationButtonStyle'] = 'display: none;'; $config['inline']['inlineOnlineMediaAddButtonStyle'] = 'display: none;'; } // Render the level links (create new record): $levelLinks = ''; if (!empty($config['appearance']['enabledControls']['new'])) { $levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config); } // Wrap all inline fields of a record with a <div> (like a container) $html = '<div class="form-group" id="' . $nameObject . '">'; // Add the level links before all child records: if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') { $html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>'; } // If it's required to select from possible child records (reusable children), add a selector box if ($config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== false) { if ($config['selectorOrUniqueConfiguration']['config']['type'] === 'select') { $selectorBox = $this->renderPossibleRecordsSelectorTypeSelect($config, $uniqueIds); } else { $selectorBox = $this->renderPossibleRecordsSelectorTypeGroupDB($config); } $html .= $selectorBox . $localizationLinks; } $title = $languageService->sL(trim($parameterArray['fieldConf']['label'])); $html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">'; $sortableRecordUids = []; foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) { $options['inlineParentUid'] = $row['uid']; $options['inlineFirstPid'] = $this->data['inlineFirstPid']; // @todo: this can be removed if this container no longer sets additional info to $config $options['inlineParentConfig'] = $config; $options['inlineData'] = $this->inlineData; $options['inlineStructure'] = $inlineStackProcessor->getStructure(); $options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray']; $options['renderType'] = 'inlineRecordContainer'; $childResult = $this->nodeFactory->create($options)->render(); $html .= $childResult['html']; $childArray['html'] = ''; $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult); if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext']) { // Don't add record to list of "valid" uids if it is only the default // language record of a not yet localized child $sortableRecordUids[] = $options['databaseRow']['uid']; } } $html .= '</div>'; // Add the level links after all child records: if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom') { $html .= $levelLinks . $localizationLinks; } if (is_array($config['customControls'])) { $html .= '<div id="' . $nameObject . '_customControls">'; foreach ($config['customControls'] as $customControlConfig) { $parameters = array('table' => $table, 'field' => $field, 'row' => $row, 'nameObject' => $nameObject, 'nameForm' => $nameForm, 'config' => $config); $html .= GeneralUtility::callUserFunction($customControlConfig, $parameters, $this); } $html .= '</div>'; } // Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post if (count($sortableRecordUids) > 1 && $config['appearance']['useSortable']) { $resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");'; } $resultArray['requireJsModules'] = array_merge($resultArray['requireJsModules'], $this->requireJsModules); // Publish the uids of the child records in the given order to the browser $html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" ' . $this->getValidationDataAsDataAttribute(array('type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems'])) . ' class="inlineRecord" />'; // Close the wrap for all inline fields (container) $html .= '</div>'; $resultArray['html'] = $html; return $resultArray; }
/** * Get the related records of the embedding item, this could be 1:n, m:n. * Returns an associative array with the keys records and count. 'count' contains only real existing records on the current parent record. * * @param string $table The table name of the record * @param string $field The field name which this element is supposed to edit * @param array $row The record data array where the value(s) for the field can be found * @param array $PA An array with additional configuration options. * @param array $config (Redundant) content of $PA['fieldConf']['config'] (for convenience) * @param int $inlineFirstPid Inline first pid * @return array The records related to the parent item as associative array. */ public function getRelatedRecords($table, $field, $row, $PA, $config, $inlineFirstPid) { $language = 0; $elements = $PA['itemFormElValue']; $foreignTable = $config['foreign_table']; $localizationMode = BackendUtility::getInlineLocalizationMode($table, $config); if ($localizationMode !== FALSE) { $language = (int) $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; $transOrigPointer = (int) $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]; $transOrigTable = BackendUtility::getOriginalTranslationTable($table); if ($language > 0 && $transOrigPointer) { // Localization in mode 'keep', isn't a real localization, but keeps the children of the original parent record: if ($localizationMode === 'keep') { $transOrigRec = $this->getRecord($transOrigTable, $transOrigPointer); $elements = $transOrigRec[$field]; } elseif ($localizationMode === 'select') { $transOrigRec = $this->getRecord($transOrigTable, $transOrigPointer); $fieldValue = $transOrigRec[$field]; // Checks if it is a flexform field if ($GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'flex') { $flexFormParts = FormEngineUtility::extractFlexFormParts($PA['itemFormElName']); $flexData = GeneralUtility::xml2array($fieldValue); /** @var $flexFormTools FlexFormTools */ $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class); $flexFormFieldValue = $flexFormTools->getArrayValueByPath($flexFormParts, $flexData); if ($flexFormFieldValue !== NULL) { $fieldValue = $flexFormFieldValue; } } $recordsOriginal = $this->getRelatedRecordsArray($foreignTable, $fieldValue); } } } $records = $this->getRelatedRecordsArray($foreignTable, $elements); $relatedRecords = array('records' => $records, 'count' => count($records)); // Merge original language with current localization and show differences: if (!empty($recordsOriginal)) { $options = array('showPossible' => isset($config['appearance']['showPossibleLocalizationRecords']) && $config['appearance']['showPossibleLocalizationRecords'], 'showRemoved' => isset($config['appearance']['showRemovedLocalizationRecords']) && $config['appearance']['showRemovedLocalizationRecords']); // Either show records that possibly can localized or removed if ($options['showPossible'] || $options['showRemoved']) { $relatedRecords['records'] = $this->getLocalizationDifferences($foreignTable, $options, $recordsOriginal, $records); // Otherwise simulate localizeChildrenAtParentLocalization behaviour when creating a new record // (which has language and translation pointer values set) } elseif (!empty($config['behaviour']['localizeChildrenAtParentLocalization']) && !MathUtility::canBeInterpretedAsInteger($row['uid'])) { if (!empty($GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'])) { $foreignLanguageField = $GLOBALS['TCA'][$foreignTable]['ctrl']['languageField']; } if (!empty($GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField'])) { $foreignTranslationPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField']; } // Duplicate child records of default language in form foreach ($recordsOriginal as $record) { if (!empty($foreignLanguageField)) { $record[$foreignLanguageField] = $language; } if (!empty($foreignTranslationPointerField)) { $record[$foreignTranslationPointerField] = $record['uid']; } $newId = uniqid('NEW', TRUE); $record['uid'] = $newId; $record['pid'] = ${$inlineFirstPid}; $relatedRecords['records'][$newId] = $record; } } } return $relatedRecords; }
/** * Injects configuration via AJAX calls. * This is used by inline ajax calls that transfer configuration options back to the stack for initialization * The configuration is validated using HMAC to avoid hijacking. * * @param string $contextString Given context string from ajax call * @return void * @todo: Review this construct - Why can't the ajax call fetch these data on its own and transfers it to client instead? */ public function injectAjaxConfiguration($contextString = '') { $level = $this->calculateStructureLevel(-1); if (empty($contextString) || $level === false) { return; } $current =& $this->inlineStructure['stable'][$level]; $context = json_decode($contextString, true); if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) { return; } // Remove the data structure pointers, only relevant for the FormInlineAjaxController unset($context['flexDataStructurePointers']); $current['config'] = $context['config']; $current['localizationMode'] = BackendUtility::getInlineLocalizationMode($current['table'], $current['config']); }
/** * Convert the DOM object-id of an inline container to an array. * The object-id could look like 'data-parentPageId-tx_mmftest_company-1-employees'. * The result is written to $this->inlineStructure. * There are two keys: * - 'stable': Containing full qualified identifiers (table, uid and field) * - 'unstable': Containting partly filled data (e.g. only table and possibly field) * * @param string $domObjectId The DOM object-id * @param boolean $loadConfig Load the TCA configuration for that level (default: TRUE) * @return void * @todo Define visibility */ public function parseStructureString($string, $loadConfig = TRUE) { $unstable = array(); $vector = array('table', 'uid', 'field'); $pattern = '/^' . $this->prependNaming . self::Structure_Separator . '(.+?)' . self::Structure_Separator . '(.+)$/'; if (preg_match($pattern, $string, $match)) { $this->inlineFirstPid = $match[1]; $parts = explode(self::Structure_Separator, $match[2]); $partsCnt = count($parts); for ($i = 0; $i < $partsCnt; $i++) { if ($i > 0 && $i % 3 == 0) { // Load the TCA configuration of the table field and store it in the stack if ($loadConfig) { \TYPO3\CMS\Core\Utility\GeneralUtility::loadTCA($unstable['table']); $unstable['config'] = $GLOBALS['TCA'][$unstable['table']]['columns'][$unstable['field']]['config']; // Fetch TSconfig: $TSconfig = $this->fObj->setTSconfig($unstable['table'], array('uid' => $unstable['uid'], 'pid' => $this->inlineFirstPid), $unstable['field']); // Override TCA field config by TSconfig: if (!$TSconfig['disabled']) { $unstable['config'] = $this->fObj->overrideFieldConf($unstable['config'], $TSconfig); } $unstable['localizationMode'] = \TYPO3\CMS\Backend\Utility\BackendUtility::getInlineLocalizationMode($unstable['table'], $unstable['config']); } $this->inlineStructure['stable'][] = $unstable; $unstable = array(); } $unstable[$vector[$i % 3]] = $parts[$i]; } $this->updateStructureNames(); if (count($unstable)) { $this->inlineStructure['unstable'] = $unstable; } } }
/** * Performs localization or synchronization of child records. * The $command argument expects an array, but supports a string for backward-compatibility. * * $command = array( * 'field' => 'tx_myfieldname', * 'language' => 2, * // either the key 'action' or 'ids' must be set * 'action' => 'synchronize', // or 'localize' * 'ids' => array(1, 2, 3, 4) // child element ids * ); * * @param string $table The table of the localized parent record * @param int $id The uid of the localized parent record * @param array|string $command Defines the command to be performed (see example above) * @return void */ protected function inlineLocalizeSynchronize($table, $id, $command) { $parentRecord = BackendUtility::getRecordWSOL($table, $id); // Backward-compatibility handling if (!is_array($command)) { // <field>, (localize | synchronize | <uid>): $parts = GeneralUtility::trimExplode(',', $command); $command = []; $command['field'] = $parts[0]; // The previous process expected $id to point to the localized record already $command['language'] = (int) $parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; if (!MathUtility::canBeInterpretedAsInteger($parts[1])) { $command['action'] = $parts[1]; } else { $command['ids'] = [$parts[1]]; } } // In case the parent record is the default language record, fetch the localization if (empty($parentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) { // Fetch the live record $parentRecordLocalization = BackendUtility::getRecordLocalization($table, $id, $command['language'], 'AND pid<>-1'); if (empty($parentRecordLocalization)) { $this->newlog2('Localization for parent record ' . $table . ':' . $id . '" cannot be fetched', $table, $id, $parentRecord['pid']); return; } $parentRecord = $parentRecordLocalization[0]; $id = $parentRecord['uid']; // Process overlay for current selected workspace BackendUtility::workspaceOL($table, $parentRecord); } $field = $command['field']; $language = $command['language']; $action = $command['action']; $ids = $command['ids']; if (!$field || !($action === 'localize' || $action === 'synchronize') && empty($ids) || !isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) { return; } $config = $GLOBALS['TCA'][$table]['columns'][$field]['config']; $foreignTable = $config['foreign_table']; $localizationMode = BackendUtility::getInlineLocalizationMode($table, $config); if ($localizationMode !== 'select') { return; } $transOrigPointer = (int) $parentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]; $transOrigTable = BackendUtility::getOriginalTranslationTable($table); $childTransOrigPointerField = $GLOBALS['TCA'][$foreignTable]['ctrl']['transOrigPointerField']; if (!$parentRecord || !is_array($parentRecord) || $language <= 0 || !$transOrigPointer) { return; } $inlineSubType = $this->getInlineFieldType($config); $transOrigRecord = BackendUtility::getRecordWSOL($transOrigTable, $transOrigPointer); if ($inlineSubType === false) { return; } $removeArray = []; $mmTable = $inlineSubType == 'mm' && isset($config['MM']) && $config['MM'] ? $config['MM'] : ''; // Fetch children from original language parent: /** @var $dbAnalysisOriginal RelationHandler */ $dbAnalysisOriginal = $this->createRelationHandlerInstance(); $dbAnalysisOriginal->start($transOrigRecord[$field], $foreignTable, $mmTable, $transOrigRecord['uid'], $transOrigTable, $config); $elementsOriginal = []; foreach ($dbAnalysisOriginal->itemArray as $item) { $elementsOriginal[$item['id']] = $item; } unset($dbAnalysisOriginal); // Fetch children from current localized parent: /** @var $dbAnalysisCurrent RelationHandler */ $dbAnalysisCurrent = $this->createRelationHandlerInstance(); $dbAnalysisCurrent->start($parentRecord[$field], $foreignTable, $mmTable, $id, $table, $config); // Perform synchronization: Possibly removal of already localized records: if ($action === 'synchronize') { foreach ($dbAnalysisCurrent->itemArray as $index => $item) { $childRecord = BackendUtility::getRecordWSOL($item['table'], $item['id']); if (isset($childRecord[$childTransOrigPointerField]) && $childRecord[$childTransOrigPointerField] > 0) { $childTransOrigPointer = $childRecord[$childTransOrigPointerField]; // If synchronization is requested, child record was translated once, but original record does not exist anymore, remove it: if (!isset($elementsOriginal[$childTransOrigPointer])) { unset($dbAnalysisCurrent->itemArray[$index]); $removeArray[$item['table']][$item['id']]['delete'] = 1; } } } } // Perform synchronization/localization: Possibly add unlocalized records for original language: if ($action === 'localize' || $action === 'synchronize') { foreach ($elementsOriginal as $originalId => $item) { $item['id'] = $this->localize($item['table'], $item['id'], $language); $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']); $dbAnalysisCurrent->itemArray[] = $item; } } elseif (!empty($ids)) { foreach ($ids as $childId) { if (!MathUtility::canBeInterpretedAsInteger($childId) || !isset($elementsOriginal[$childId])) { continue; } $item = $elementsOriginal[$childId]; $item['id'] = $this->localize($item['table'], $item['id'], $language); $item['id'] = $this->overlayAutoVersionId($item['table'], $item['id']); $dbAnalysisCurrent->itemArray[] = $item; } } // Store the new values, we will set up the uids for the subtype later on (exception keep localization from original record): $value = implode(',', $dbAnalysisCurrent->getValueArray()); $this->registerDBList[$table][$id][$field] = $value; // Remove child records (if synchronization requested it): if (is_array($removeArray) && !empty($removeArray)) { /** @var DataHandler $tce */ $tce = GeneralUtility::makeInstance(__CLASS__); $tce->enableLogging = $this->enableLogging; $tce->start([], $removeArray); $tce->process_cmdmap(); unset($tce); } $updateFields = []; // Handle, reorder and store relations: if ($inlineSubType == 'list') { $updateFields = [$field => $value]; } elseif ($inlineSubType == 'field') { $dbAnalysisCurrent->writeForeignField($config, $id); $updateFields = [$field => $dbAnalysisCurrent->countItems(false)]; } elseif ($inlineSubType == 'mm') { $dbAnalysisCurrent->writeMM($config['MM'], $id); $updateFields = [$field => $dbAnalysisCurrent->countItems(false)]; } // Update field referencing to child records of localized parent record: if (!empty($updateFields)) { $this->updateDB($table, $id, $updateFields); } }