[FEATURE] Allow copying from another language 68/41768/12
authorAndreas Fernandez <a.fernandez@scripting-base.de>
Mon, 20 Jul 2015 18:25:30 +0000 (20:25 +0200)
committerSusanne Moog <typo3@susannemoog.de>
Wed, 22 Jul 2015 21:21:39 +0000 (23:21 +0200)
This patch extends the "Copy from default elements" buttons by a dropdown
list of available languages. Clicking one of those languages creates
independent copies (not references!) of the selected language.

Resolves: #68395
Releases: master
Change-Id: I2ce443644ca1fbc6f0c41bc5917515b2784bc155
Reviewed-on: http://review.typo3.org/41768
Reviewed-by: Mathias Schreiber <mathias.schreiber@wmdb.de>
Tested-by: Mathias Schreiber <mathias.schreiber@wmdb.de>
Reviewed-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Reviewed-by: Susanne Moog <typo3@susannemoog.de>
Tested-by: Susanne Moog <typo3@susannemoog.de>
typo3/sysext/backend/Classes/Controller/PageLayoutController.php
typo3/sysext/backend/Classes/View/PageLayoutView.php
typo3/sysext/backend/Resources/Private/Language/locallang_layout.xlf
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Documentation/Changelog/master/Feature-68395-AllowRealCopiesOfContentElementsIntoForeignLanguages.rst [new file with mode: 0644]

index 8bcbce3..f49fc50 100755 (executable)
@@ -291,6 +291,21 @@ class PageLayoutController {
        protected $closeUrl;
 
        /**
+        * Caches the available languages in a colPos
+        *
+        * @var array
+        */
+       protected $languagesInColumnCache = array();
+
+       /**
+        * Caches the amount of content elements as a matrix
+        *
+        * @var array
+        * @internal
+        */
+       public $contentElementCache = array();
+
+       /**
         * Initializing the module
         *
         * @return void
@@ -1301,6 +1316,92 @@ class PageLayoutController {
        }
 
        /**
+        * Get used languages in a colPos of a page
+        *
+        * @param int $pageId
+        * @param int $colPos
+        * @return bool|\mysqli_result|object
+        */
+       public function getUsedLanguagesInPageAndColumn($pageId, $colPos) {
+               if (!isset($languagesInColumnCache[$pageId])) {
+                       $languagesInColumnCache[$pageId] = array();
+               }
+               if (!isset($languagesInColumnCache[$pageId][$colPos])) {
+                       $languagesInColumnCache[$pageId][$colPos] = array();
+               }
+
+               if (empty($languagesInColumnCache[$pageId][$colPos])) {
+                       $exQ = BackendUtility::deleteClause('tt_content') .
+                               ($this->getBackendUser()->isAdmin() ? '' : ' AND sys_language.hidden=0');
+
+                       $databaseConnection = $this->getDatabaseConnection();
+                       $res = $databaseConnection->exec_SELECTquery(
+                               'sys_language.*',
+                               'tt_content,sys_language',
+                               'tt_content.sys_language_uid=sys_language.uid AND tt_content.colPos = ' . (int)$colPos . ' AND tt_content.pid=' . (int)$pageId . $exQ .
+                               BackendUtility::versioningPlaceholderClause('tt_content'),
+                               'tt_content.sys_language_uid,sys_language.uid,sys_language.pid,sys_language.tstamp,sys_language.hidden,sys_language.title,sys_language.language_isocode,sys_language.static_lang_isocode,sys_language.flag',
+                               'sys_language.title'
+                       );
+                       while ($row = $databaseConnection->sql_fetch_assoc($res)) {
+                               $languagesInColumnCache[$pageId][$colPos][$row['uid']] = $row;
+                       }
+                       $databaseConnection->sql_free_result($res);
+               }
+
+               return $languagesInColumnCache[$pageId][$colPos];
+       }
+
+       /**
+        * Check if a column of a page for a language is empty. Translation records are ignored here!
+        *
+        * @param int $colPos
+        * @param int $languageId
+        * @return bool
+        */
+       public function isColumnEmpty($colPos, $languageId) {
+               foreach ($this->contentElementCache[$languageId][$colPos] as $uid => $row) {
+                       if ((int)$row['l18n_parent'] === 0) {
+                               return FALSE;
+                       }
+               }
+               return TRUE;
+       }
+
+       /**
+        * Get elements for a column and a language
+        *
+        * @param int $pageId
+        * @param int $colPos
+        * @param int $languageId
+        * @return array
+        */
+       public function getElementsFromColumnAndLanguage($pageId, $colPos, $languageId) {
+               if (!isset($this->contentElementCache[$languageId][$colPos])) {
+                       $languageId = (int)$languageId;
+                       $whereClause = 'tt_content.pid=' . (int)$pageId . ' AND tt_content.colPos=' . (int)$colPos . ' AND tt_content.sys_language_uid=' . $languageId . BackendUtility::deleteClause('tt_content');
+                       if ($languageId > 0) {
+                               $whereClause .= ' AND tt_content.l18n_parent=0 AND sys_language.uid=' . $languageId . ($this->getBackendUser()->isAdmin() ? '' : ' AND sys_language.hidden=0');
+                       }
+
+                       $databaseConnection = $this->getDatabaseConnection();
+                       $res = $databaseConnection->exec_SELECTquery(
+                               'tt_content.uid',
+                               'tt_content,sys_language',
+                               $whereClause
+                       );
+                       while ($row = $databaseConnection->sql_fetch_assoc($res)) {
+                               $this->contentElementCache[$languageId][$colPos][$row['uid']] = $row;
+                       }
+                       $databaseConnection->sql_free_result($res);
+               }
+               if (is_array($this->contentElementCache[$languageId][$colPos])) {
+                       return array_keys($this->contentElementCache[$languageId][$colPos]);
+               }
+               return array();
+       }
+
+       /**
         * Check the editlock access
         *
         * @return bool
index ce86c8e..3ec681c 100644 (file)
@@ -417,6 +417,11 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                // If not languageMode, then we'll only be through this once.
                foreach ($langListArr as $lP) {
                        $lP = (int)$lP;
+
+                       if (!isset($this->getPageLayoutController()->contentElementCache[$lP])) {
+                               $this->getPageLayoutController()->contentElementCache[$lP] = array();
+                       }
+
                        if (count($langListArr) === 1 || $lP === 0) {
                                $showLanguage = ' AND sys_language_uid IN (' . $lP . ',-1)';
                        } else {
@@ -430,6 +435,10 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                        $contentRecordsPerColumn = $this->getContentRecordsPerColumn('table', $id, array_values($cList), $showLanguage);
                        // For each column, render the content into a variable:
                        foreach ($cList as $key) {
+                               if (!isset($this->getPageLayoutController()->contentElementCache[$lP][$key])) {
+                                       $this->getPageLayoutController()->contentElementCache[$lP][$key] = array();
+                               }
+
                                if (!$lP) {
                                        $defLanguageCount[$key] = array();
                                }
@@ -459,13 +468,16 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                $editUidList = '';
                                $rowArr = $contentRecordsPerColumn[$key];
                                $this->generateTtContentDataArray($rowArr);
+
                                foreach ((array)$rowArr as $rKey => $row) {
+                                       $this->getPageLayoutController()->contentElementCache[$lP][$key][$row['uid']] = $row;
                                        if ($this->tt_contentConfig['languageMode']) {
                                                $languageColumn[$key][$lP] = $head[$key] . $content[$key];
                                                if (!$this->defLangBinding) {
                                                        $languageColumn[$key][$lP] .= $this->newLanguageButton(
                                                                $this->getNonTranslatedTTcontentUids($defLanguageCount[$key], $id, $lP),
-                                                               $lP
+                                                               $lP,
+                                                               $key
                                                        );
                                                }
                                        }
@@ -558,7 +570,8 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                        if (!$this->defLangBinding) {
                                                $languageColumn[$key][$lP] .= $this->newLanguageButton(
                                                        $this->getNonTranslatedTTcontentUids($defLanguageCount[$key], $id, $lP),
-                                                       $lP
+                                                       $lP,
+                                                       $key
                                                );
                                        }
                                        // We sort $languageColumn again according to $cList as it may contain data already from above.
@@ -702,7 +715,8 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                                foreach ($langListArr as $lP) {
                                                        $cCont[] = $defLangBinding[$cKey][$lP][$defUid] . $this->newLanguageButton(
                                                                $this->getNonTranslatedTTcontentUids(array($defUid), $id, $lP),
-                                                               $lP
+                                                               $lP,
+                                                               $cKey
                                                        );
                                                }
                                                $out .= '
@@ -1487,24 +1501,66 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
         *
         * @param array $defLanguageCount Numeric array with uids of tt_content elements in the default language
         * @param int $lP Sys language UID
+        * @param int $colPos Column position
         * @return string "Copy languages" button, if available.
         */
-       public function newLanguageButton($defLanguageCount, $lP) {
-               if (!$this->doEdit || empty($defLanguageCount) || !$lP) {
+       public function newLanguageButton($defLanguageCount, $lP, $colPos = 0) {
+               if (!$this->doEdit || !$lP) {
                        return '';
                }
-               $params = '';
-               foreach ($defLanguageCount as $uidVal) {
-                       $params .= '&cmd[tt_content][' . $uidVal . '][localize]=' . $lP;
+
+               $copyFromLanguageMenu = '';
+               foreach ($this->getLanguagesToCopyFrom(GeneralUtility::_GP('id'), $lP, $colPos) as $languageId => $label) {
+                       $elementsInColumn = $languageId === 0 ? $defLanguageCount : $this->getPageLayoutController()->getElementsFromColumnAndLanguage(GeneralUtility::_GP('id'), $colPos, $languageId);
+                       if (!empty($elementsInColumn)) {
+                               $onClick = 'window.location.href=' . GeneralUtility::quoteJSvalue($this->getPageLayoutController()->doc->issueCommand('&cmd[tt_content][' . implode(',', $elementsInColumn) . '][copyFromLanguage]=' . GeneralUtility::_GP('id') . ',' . $lP)) . '; return false;';
+                               $copyFromLanguageMenu .= '<li><a href="#" onclick="' . htmlspecialchars($onClick) . '">' . $this->languageFlag($languageId, FALSE) . ' ' . htmlspecialchars($label) . '</a></li>' . LF;
+                       }
+               }
+               if ($copyFromLanguageMenu !== '') {
+                       $copyFromLanguageMenu =
+                               '<ul class="dropdown-menu">'
+                                       . $copyFromLanguageMenu
+                               . '</ul>';
+               }
+
+               if (!empty($defLanguageCount)) {
+                       $params = '';
+                       foreach ($defLanguageCount as $uidVal) {
+                               $params .= '&cmd[tt_content][' . $uidVal . '][localize]=' . $lP;
+                       }
+
+                       // We have content in the default language, create a split button
+                       $onClick = 'window.location.href=' . GeneralUtility::quoteJSvalue($this->getPageLayoutController()->doc->issueCommand($params)) . '; return false;';
+                       $theNewButton =
+                               '<div class="btn-group">'
+                                       . $this->getPageLayoutController()->doc->t3Button(
+                                               $onClick,
+                                               $this->getLanguageService()->getLL('newPageContent_copyForLang', TRUE) . ' [' . count($defLanguageCount) . ']'
+                                       );
+                       if ($copyFromLanguageMenu !== '' && $this->getPageLayoutController()->isColumnEmpty($colPos, $lP)) {
+                               $theNewButton .= '<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'
+                                       . '<span class="caret"></span>'
+                                       . '<span class="sr-only">Toggle Dropdown</span>'
+                                       . '</button>'
+                                       . $copyFromLanguageMenu;
+                       }
+                       $theNewButton .= '</div>';
+               } else {
+                       if ($copyFromLanguageMenu !== '' && $this->getPageLayoutController()->isColumnEmpty($colPos, $lP)) {
+                               $theNewButton =
+                                       '<div class="btn-group">'
+                                       . '<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'
+                                       . $this->getLanguageService()->getLL('newPageContent_copyFromAnotherLang_button', TRUE) . ' <span class="caret"></span>'
+                                       . '</button>'
+                                       . $copyFromLanguageMenu
+                                       . '</div>';
+                       } else {
+                               $theNewButton = '';
+                       }
                }
-               // Copy for language:
-               $onClick = 'window.location.href=' . GeneralUtility::quoteJSvalue($this->getPageLayoutController()->doc->issueCommand($params)) . '; return false;';
-               $theNewButton = '<div class="t3-page-lang-copyce">' .
-                       $this->getPageLayoutController()->doc->t3Button(
-                               $onClick,
-                               $this->getLanguageService()->getLL('newPageContent_copyForLang') . ' [' . count($defLanguageCount) . ']'
-                       ) . '</div>';
-               return $theNewButton;
+
+               return '<div class="t3-page-lang-copyce">' . $theNewButton . '</div>';
        }
 
        /**
@@ -1550,6 +1606,79 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
        }
 
        /**
+        * Get available languages for a page
+        *
+        * @param int $pageId
+        * @return array
+        */
+       protected function getAvailableLanguages($pageId) {
+               // First, select all
+               $res = $this->getPageLayoutController()->exec_languageQuery(0);
+               $langSelItems = array();
+               while ($row = $this->getDatabase()->sql_fetch_assoc($res)) {
+                       if ($this->getBackendUser()->checkLanguageAccess($row['uid'])) {
+                               $langSelItems[$row['uid']] = $row['title'];
+                       }
+               }
+               $this->getDatabase()->sql_free_result($res);
+
+               // Remove disallowed languages
+               if (count($langSelItems) > 1
+                       && !$this->getBackendUser()->user['admin']
+                       && $this->getBackendUser()->groupData['allowed_languages'] !== ''
+               ) {
+                       $allowed_languages = array_flip(explode(',', $this->getBackendUser()->groupData['allowed_languages']));
+                       if (!empty($allowed_languages)) {
+                               foreach ($langSelItems as $key => $value) {
+                                       if (!isset($allowed_languages[$key]) && $key != 0) {
+                                               unset($langSelItems[$key]);
+                                       }
+                               }
+                       }
+               }
+               // Remove disabled languages
+               $modSharedTSconfig = BackendUtility::getModTSconfig($pageId, 'mod.SHARED');
+               $disableLanguages = isset($modSharedTSconfig['properties']['disableLanguages'])
+                       ? GeneralUtility::trimExplode(',', $modSharedTSconfig['properties']['disableLanguages'], TRUE)
+                       : array();
+               if (!empty($langSelItems) && !empty($disableLanguages)) {
+                       foreach ($disableLanguages as $language) {
+                               if ($language != 0 && isset($langSelItems[$language])) {
+                                       unset($langSelItems[$language]);
+                               }
+                       }
+               }
+
+               return $langSelItems;
+       }
+
+       /**
+        * Get available languages for copying into another language
+        *
+        * @param int $pageId
+        * @param int $excludeLanguage
+        * @param int $colPos
+        * @return array
+        */
+       protected function getLanguagesToCopyFrom($pageId, $excludeLanguage = NULL, $colPos = 0) {
+               $langSelItems = array();
+               if (!$this->getPageLayoutController()->isColumnEmpty($colPos, 0)) {
+                       $langSelItems[0] = $this->getLanguageService()->getLL('newPageContent_translateFromDefault', TRUE);
+               }
+
+               $languages = $this->getPageLayoutController()->getUsedLanguagesInPageAndColumn($pageId, $colPos);
+               foreach ($languages as $uid => $language) {
+                       $langSelItems[$uid] = sprintf($this->getLanguageService()->getLL('newPageContent_copyFromAnotherLang'), htmlspecialchars($language['title']));
+               }
+
+               if (isset($langSelItems[$excludeLanguage])) {
+                       unset($langSelItems[$excludeLanguage]);
+               }
+
+               return $langSelItems;
+       }
+
+       /**
         * Make selector box for creating new translation in a language
         * Displays only languages which are not yet present for the current page and
         * that are not disabled with page TS.
@@ -1611,13 +1740,13 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                ));
                                $onChangeContent = 'window.location.href=' . GeneralUtility::quoteJSvalue($url . '&overrideVals[pages_language_overlay][sys_language_uid]=') . '+this.options[this.selectedIndex].value';
                                return '<div class="form-inline form-inline-spaced">'
-                                       . '<div class="form-group">'
-                                       . '<label for="createNewLanguage">'
-                                       . $this->getLanguageService()->getLL('new_language', TRUE)
-                                       . '</label>'
-                                       . '<select class="form-control input-sm" name="createNewLanguage" onchange="' . htmlspecialchars($onChangeContent) . '">'
-                                       . implode('', $langSelItems)
-                                       . '</select></div></div>';
+                               . '<div class="form-group">'
+                               . '<label for="createNewLanguage">'
+                               . $this->getLanguageService()->getLL('new_language', TRUE)
+                               . '</label>'
+                               . '<select class="form-control input-sm" name="createNewLanguage" onchange="' . htmlspecialchars($onChangeContent) . '">'
+                               . implode('', $langSelItems)
+                               . '</select></div></div>';
                        }
                }
                return '';
index f5b1c03..a7563bb 100644 (file)
                                <source>Create page content</source>
                        </trans-unit>
                        <trans-unit id="newPageContent_copyForLang">
-                               <source>Copy default content elements</source>
+                               <source>Translate default content elements</source>
+                       </trans-unit>
+                       <trans-unit id="newPageContent_translateFromDefault">
+                               <source>Copy from default language</source>
+                       </trans-unit>
+                       <trans-unit id="newPageContent_copyFromAnotherLang_button">
+                               <source>Copy from language...</source>
+                       </trans-unit>
+                       <trans-unit id="newPageContent_copyFromAnotherLang">
+                               <source>Copy from language "%s"</source>
                        </trans-unit>
                        <trans-unit id="newPageContent2">
                                <source>New content</source>
index 1c05788..cce2f12 100644 (file)
@@ -3269,6 +3269,9 @@ class DataHandler {
                                                case 'inlineLocalizeSynchronize':
                                                        $this->inlineLocalizeSynchronize($table, $id, $value);
                                                        break;
+                                               case 'copyFromLanguage':
+                                                       $this->copyRecordFromLanguage($table, $id, $value);
+                                                       break;
                                                case 'delete':
                                                        $this->deleteAction($table, $id);
                                                        break;
@@ -3326,9 +3329,10 @@ class DataHandler {
         * @param array $overrideValues Associative array with field/value pairs to override directly. Notice; Fields must exist in the table record and NOT be among excluded fields!
         * @param string $excludeFields Commalist of fields to exclude from the copy process (might get default values)
         * @param int $language Language ID (from sys_language table)
+        * @param bool $ignoreLocalization If TRUE, any localization routine is skipped
         * @return int|null ID of new record, if any
         */
-       public function copyRecord($table, $uid, $destPid, $first = FALSE, $overrideValues = array(), $excludeFields = '', $language = 0) {
+       public function copyRecord($table, $uid, $destPid, $first = FALSE, $overrideValues = array(), $excludeFields = '', $language = 0, $ignoreLocalization = FALSE) {
                $uid = ($origUid = (int)$uid);
                // Only copy if the table is defined in $GLOBALS['TCA'], a uid is given and the record wasn't copied before:
                if (!$GLOBALS['TCA'][$table] || !$uid || $this->isRecordCopied($table, $uid)) {
@@ -3353,7 +3357,7 @@ class DataHandler {
 
                $fullLanguageCheckNeeded = $table != 'pages';
                //Used to check language and general editing rights
-               if (($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, FALSE, FALSE, $fullLanguageCheckNeeded)) {
+               if (!$ignoreLocalization && ($language <= 0 || !$this->BE_USER->checkLanguageAccess($language)) && !$this->BE_USER->recordEditAccessInternals($table, $uid, FALSE, FALSE, $fullLanguageCheckNeeded)) {
                        if ($this->enableLogging) {
                                $this->log($table, $uid, 3, 0, 1, 'Attempt to copy record without having permissions to do so. [' . $this->BE_USER->errorMsg . '].');
                        }
@@ -3445,7 +3449,7 @@ class DataHandler {
                $this->cachedTSconfig = $copyTCE->cachedTSconfig;
                $this->errorLog = array_merge($this->errorLog, $copyTCE->errorLog);
                unset($copyTCE);
-               if ($language == 0) {
+               if (!$ignoreLocalization && $language == 0) {
                        //repointing the new translation records to the parent record we just created
                        $overrideValues[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $theNewSQLID;
                        $this->copyL10nOverlayRecords($table, $uid, $destPid, $first, $overrideValues, $excludeFields);
@@ -4643,6 +4647,27 @@ class DataHandler {
                }
        }
 
+       /**
+        * Creates a independent copy of content elements into another language.
+        *
+        * @param string $table The table of the localized parent record
+        * @param string $id Comma separated list of content element ids
+        * @param string $value Comma separated list of the destination and the target language
+        * @return void
+        */
+       public function copyRecordFromLanguage($table, $id, $value) {
+               list($destination, $language) = GeneralUtility::intExplode(',', $value);
+
+               // array_reverse is required to keep the order of elements
+               $idList = array_reverse(GeneralUtility::intExplode(',', $id, TRUE));
+               foreach ($idList as $contentElementUid) {
+                       $this->copyRecord($table, $contentElementUid, $destination, TRUE, array(
+                               $GLOBALS['TCA'][$table]['ctrl']['languageField'] => $language,
+                               $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] => 0
+                       ), '', 0, TRUE);
+               }
+       }
+
        /*********************************************
         *
         * Cmd: Deleting
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-68395-AllowRealCopiesOfContentElementsIntoForeignLanguages.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-68395-AllowRealCopiesOfContentElementsIntoForeignLanguages.rst
new file mode 100644 (file)
index 0000000..25b6682
--- /dev/null
@@ -0,0 +1,18 @@
+==============================================================================
+Feature: #68395 - Allow real copies of content elements into foreign languages
+==============================================================================
+
+Description
+===========
+
+A new button has been added to each column in the "Page" module which allows "real" copies of content element into a language.
+This allows to create copies from any language into the destination.
+References, like FAL records, become independent records and are not relared to the original record, as there is technically no parent anymore.
+
+
+Impact
+======
+
+The button will be either displayed as as standalone button if a page has no records in the default language or as a split button if there are records in the default language.
+
+Creating real copies will cause the loss of any functionality between the copy and the default language (e.g. diff), as the copy is not defined as child of the element where it was copied from.