/**
  * 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;
 }
Beispiel #2
0
 /**
  * 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;
         }
     }
 }
Beispiel #3
0
 /**
  * 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']);
 }
Beispiel #8
0
 /**
  * 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;
         }
     }
 }
Beispiel #9
0
 /**
  * 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);
     }
 }