[BUGFIX] Disallow Language Mixtures in Web->Page 33/44433/8
authorMathias Schreiber <mathias.schreiber@wmdb.de>
Sat, 31 Oct 2015 13:46:22 +0000 (14:46 +0100)
committerAndreas Fernandez <typo3@scripting-base.de>
Tue, 3 Nov 2015 16:05:25 +0000 (17:05 +0100)
We no longer allow language mixtures in Web->Page anymore.
An editor will now be informed when a language uses stale
data by mixing up translations and standalone content.
Furthermore the standalone content element(s) causing the
trouble will be clearly marked in the page module.

In case the language mixture is intended (which it should not
be) the behavior can be actively turned OFF using PageTSConfig

Resolves: #71196
Releases: master
Change-Id: I98af3bdf4222ac984b894471b62065f1180dbda6
Reviewed-on: https://review.typo3.org/44433
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
Reviewed-by: Andreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez <typo3@scripting-base.de>
Build/Resources/Public/Less/TYPO3/_module_web_page.less
typo3/sysext/backend/Classes/View/PageLayoutView.php
typo3/sysext/backend/Resources/Private/Language/locallang_layout.xlf
typo3/sysext/backend/Resources/Private/Templates/Module.html
typo3/sysext/core/Documentation/Changelog/master/Feature-71196-DisallowLocalizationMixtures.rst [new file with mode: 0644]
typo3/sysext/t3skin/Resources/Public/Css/backend.css

index ba50ecb..0bfbc88 100644 (file)
 @page-ce-body-bg: #fff;
 @page-ce-footer-bg: #fafafa;
 @page-ce-hidden-opacity: 0.4;
+@page-ce-header-bg-danger: @brand-danger;
+@page-ce-header-hover-bg-danger: darken(@brand-danger, 10%);
+@page-ce-header-border-danger: @page-ce-header-bg-danger;
+@page-ce-header-hover-border-danger: @page-ce-header-hover-bg-danger;
 
 @page-ce-dropzone-bg: @state-success-bg;
 @page-ce-dropzone-border: @state-success-border;
                }
        }
 }
+.t3-page-ce-danger {
+       &:hover {
+               .t3-page-ce-header {
+                       background-color: @page-ce-header-hover-bg-danger;
+                       border-color: @page-ce-header-hover-border-danger;
+               }
+
+               .t3-page-ce-body {
+                       border-color: @page-ce-header-hover-border-danger;
+               }
+       }
+
+       .t3-page-ce-header {
+               background-color: @page-ce-header-bg-danger;
+               border-color: @page-ce-header-border-danger;
+               &:hover {
+                       background-color: @page-ce-header-hover-bg-danger;
+                       border-color: @page-ce-header-hover-border-danger;
+               }
+       }
+
+       .t3-page-ce-body {
+               border-color: @page-ce-header-border-danger;
+       }
+}
 .t3-page-ce-hidden {
        opacity: @page-ce-hidden-opacity;
        .transition(opacity .15s ease-in);
index 6edcb91..0290a1e 100644 (file)
@@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Database\DatabaseConnection;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Messaging\FlashMessageService;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -203,10 +204,18 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
     protected $iconFactory;
 
     /**
+     * Stores whether a certain language has translations in it
+     *
+     * @var array
+     */
+    protected $languageHasTranslationsCache = array();
+
+    /**
      * Construct to initialize class variables.
      */
     public function __construct()
     {
+        parent::__construct();
         $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
         $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
         $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
@@ -398,7 +407,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                 <div class="table-fit">
                                        <table class="table table-striped table-hover typo3-page-pages">' .
                         '<thead>' .
-                            $this->addelement(1, '', $theData) .
+                            $this->addElement(1, '', $theData) .
                         '</thead>' .
                         '<tbody>' .
                             $out .
@@ -501,6 +510,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                 $link = '';
                 if ($this->getPageLayoutController()->pageIsNotLockedForEditors()
                     && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)
+                    && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP))
                 ) {
                     $link = '<a href="#" onclick="' . htmlspecialchars($this->newContentElementOnClick($id, $key, $lP))
                         . '" title="' . $this->getLanguageService()->getLL('newContentElement', true) . '" class="btn btn-default btn-sm">'
@@ -556,7 +566,11 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                         $isDisabled = $this->isDisabled('tt_content', $row);
                         $statusHidden = $isDisabled ? ' t3-page-ce-hidden t3js-hidden-record' : '';
                         $displayNone = !$this->tt_contentConfig['showHidden'] && $isDisabled ? ' style="display: none;"' : '';
-                        $singleElementHTML = '<div class="t3-page-ce t3js-page-ce t3js-page-ce-sortable ' . $statusHidden . '" id="element-tt_content-'
+                        $highlightHeader = false;
+                        if ($this->checkIfTranslationsExistInLanguage([], (int)$row['sys_language_uid']) && (int)$row['l18n_parent'] === 0) {
+                            $highlightHeader = true;
+                        }
+                        $singleElementHTML = '<div class="t3-page-ce ' . ($highlightHeader ? 't3-page-ce-danger' : '') . ' t3js-page-ce t3js-page-ce-sortable ' . $statusHidden . '" id="element-tt_content-'
                             . $row['uid'] . '" data-table="tt_content" data-uid="' . $row['uid'] . '"' . $displayNone . '>' . $singleElementHTML . '</div>';
 
                         if ($this->tt_contentConfig['languageMode']) {
@@ -568,6 +582,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                         if (!$disableMoveAndNewButtons
                             && $this->getPageLayoutController()->pageIsNotLockedForEditors()
                             && $this->getBackendUser()->doesUserHaveAccess($this->pageinfo, Permission::CONTENT_EDIT)
+                            && (!$this->checkIfTranslationsExistInLanguage($contentRecordsPerColumn, $lP))
                         ) {
                             // New content element:
                             if ($this->option_newWizard) {
@@ -869,7 +884,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                 . 'title="' . $this->getLanguageService()->getLL('new', true) . '">'
                 . $this->iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render() . '</a>';
         }
-        $out .= $this->addelement(1, '', $theData, ' class="c-headLine"', 15, '', 'th');
+        $out .= $this->addElement(1, '', $theData, ' class="c-headLine"', 15, '', 'th');
         // Render Items
         $this->eCounter = $this->firstElementNumber;
         while ($row = $this->getDatabase()->sql_fetch_assoc($result)) {
@@ -894,7 +909,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                     } else {
                         $Nrow['__editIconLink__'] = $this->noEditIcon();
                     }
-                    $out .= $this->addelement(1, '', $Nrow);
+                    $out .= $this->addElement(1, '', $Nrow);
                 }
                 $this->eCounter++;
             }
@@ -1125,7 +1140,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
             }
         }
         $this->addElement_tdParams['title'] = $row['_CSSCLASS'] ? ' class="' . $row['_CSSCLASS'] . '"' : '';
-        return $this->addelement(1, '', $theData);
+        return $this->addElement(1, '', $theData);
     }
 
     /**
@@ -1333,8 +1348,13 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                 }
             }
         }
+        $allowDragAndDrop = $this->isDragAndDropAllowed($row);
         $additionalIcons = array();
-        $additionalIcons[] = $this->getIcon('tt_content', $row) . ' ';
+        if ($row['sys_language_uid'] > 0 && $this->checkIfTranslationsExistInLanguage([], (int)$row['sys_language_uid'])) {
+            $disabledClickMenuItems = 'new,move';
+            $allowDragAndDrop = false;
+        }
+        $additionalIcons[] = $this->getIcon('tt_content', $row, $disabledClickMenuItems) . ' ';
         $additionalIcons[] = $langMode ? $this->languageFlag($row['sys_language_uid'], false) : '';
         // Get record locking status:
         if ($lockInfo = BackendUtility::isRecordLocked('tt_content', $row['uid'])) {
@@ -1351,7 +1371,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
         }
         // Wrap the whole header
         // NOTE: end-tag for <div class="t3-page-ce-body"> is in getTable_tt_content()
-        return '<div class="t3-page-ce-header ' . ($this->isDragAndDropAllowed($row) ? 't3-page-ce-header-draggable t3js-page-ce-draghandle' : '') . '">
+        return '<div class="t3-page-ce-header ' . ($allowDragAndDrop ? 't3-page-ce-header-draggable t3js-page-ce-draghandle' : '') . '">
                                        <div class="t3-page-ce-header-icons-left">' . implode('', $additionalIcons) . '</div>
                                        <div class="t3-page-ce-header-icons-right">' . ($out ? '<div class="btn-toolbar">' .$out . '</div>' : '') . '</div>
                                </div>
@@ -1627,7 +1647,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
             $where = 'sys_language_uid=' . intval($lP) . ' AND l18n_parent IN ('
                 . implode(',', $defLanguageCount) . ')'
                 . BackendUtility::deleteClause('tt_content');
-            $rowArr = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'tt_content', $where);
+            $rowArr = $this->getDatabase()->exec_SELECTgetRows('*', 'tt_content', $where);
 
             // Flip uids:
             $defLanguageCount = array_flip($defLanguageCount);
@@ -2122,9 +2142,10 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
      *
      * @param string $table Table name
      * @param array $row Record array
+     * @param string $enabledClickMenuItems Passthrough to wrapClickMenuOnIcon
      * @return string HTML for the icon
      */
-    public function getIcon($table, $row)
+    public function getIcon($table, $row, $enabledClickMenuItems = '')
     {
         // Initialization
         $toolTip = BackendUtility::getRecordToolTip($row, 'tt_content');
@@ -2132,7 +2153,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
         $this->counter++;
         // The icon with link
         if ($this->getBackendUser()->recordEditAccessInternals($table, $row)) {
-            $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']);
+            $icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid'], true, '', $enabledClickMenuItems);
         }
         return $icon;
     }
@@ -2300,6 +2321,59 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
     }
 
     /**
+     * Checks whether translated Content Elements exist in the desired language
+     * If so, deny creating new ones via the UI
+     *
+     * @param array $contentElements
+     * @param int $language
+     * @return bool
+     */
+    protected function checkIfTranslationsExistInLanguage(array $contentElements, $language)
+    {
+        // If in default language, you may always create new entries
+        // Also, you may override this strict behavior via user TS Config
+        // If you do so, you're on your own and cannot rely on any support by the TYPO3 core
+        // We jump out here since we don't need to do the expensive loop operations
+        $allowInconsistentLanguageHandling = BackendUtility::getModTSconfig($this->id, 'mod.web_layout.allowInconsistentLanguageHandling');
+        if ($language === 0 || $allowInconsistentLanguageHandling['value'] === '1') {
+            return false;
+        }
+        /**
+         * Build up caches
+         */
+        if (!isset($this->languageHasTranslationsCache[$language])) {
+            foreach ($contentElements as $columns) {
+                foreach ($columns as $contentElement) {
+                    if ((int)$contentElement['l18n_parent'] === 0) {
+                        $this->languageHasTranslationsCache[$language]['hasStandAloneContent'] = true;
+                    }
+                    if ((int)$contentElement['l18n_parent'] > 0) {
+                        $this->languageHasTranslationsCache[$language]['hasTranslations'] = true;
+                    }
+                }
+            }
+            // Check whether we have a mix of both
+            if ($this->languageHasTranslationsCache[$language]['hasStandAloneContent']
+                && $this->languageHasTranslationsCache[$language]['hasTranslations']
+            ) {
+                $message = GeneralUtility::makeInstance(
+                    FlashMessage::class,
+                    sprintf($this->getLanguageService()->getLL('staleTranslationWarning'), $this->languageIconTitles[$language]['title']),
+                    sprintf($this->getLanguageService()->getLL('staleTranslationWarningTitle'), $this->languageIconTitles[$language]['title']),
+                    FlashMessage::WARNING
+                );
+                $service = GeneralUtility::makeInstance(FlashMessageService::class);
+                $queue = $service->getMessageQueueByIdentifier('module.template.flashmessages');
+                $queue->addMessage($message);
+            }
+        }
+        if ($this->languageHasTranslationsCache[$language]['hasTranslations']) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * @return BackendLayoutView
      */
     protected function getBackendLayoutView()
index 2a507d9..6cd35c6 100644 (file)
                        <trans-unit id="noPluginSelected">
                                <source>No plugin selected</source>
                        </trans-unit>
+                       <trans-unit id="staleTranslationWarning">
+                               <source>Make sure that this behavior is intended and does not pose a problem for you. We highlighted the problematic records for you.</source>
+                       </trans-unit>
+                       <trans-unit id="staleTranslationWarningTitle">
+                               <source>Inconsistent content detected in language "%s"</source>
+                       </trans-unit>
                </body>
        </file>
 </xliff>
index 60b7025..e18a281 100644 (file)
@@ -8,8 +8,11 @@
                <f:render partial="DocHeader" arguments="{docHeader:docHeader}" />
        </f:if>
        <div class="module-body t3js-module-body">
-               <f:flashMessages queueIdentifier="module.template.flashmessages" />
-
+               <f:flashMessages as="flashMessages" queueIdentifier="module.template.flashmessages">
+                       <f:for each="{flashMessages}" as="flashMessage">
+                               {flashMessage}
+                       </f:for>
+               </f:flashMessages>
                <f:format.raw>{content}</f:format.raw>
        </div>
        <f:if condition="{formTag}">
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-71196-DisallowLocalizationMixtures.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-71196-DisallowLocalizationMixtures.rst
new file mode 100644 (file)
index 0000000..c55e153
--- /dev/null
@@ -0,0 +1,18 @@
+================================================
+Feature: #71196 - Disallow localization mixtures
+================================================
+
+Description
+===========
+
+The PageLayout UI will now inform users if a mixture of translated content and standalone content in the page module since this is a major source of confusion for both administrators and editors.
+
+In case an integrator knows what he/she is doing we introduce a `PageTSConfig` setting to turn these warnings off to allow further usage of inconsistent translation handling.
+
+`mod.web_layout.allowInconsistentLanguageHandling = 1`
+
+
+Impact
+======
+
+Upon setting `mod.web_layout.allowInconsistentLanguageHandling` to `1` the page module will behave as before and allow inconsistent mixups of languages in a certain language.
\ No newline at end of file
index 96b4dea..168f71d 100644 (file)
@@ -12956,6 +12956,24 @@ iframe {
 .t3-page-ce:hover .t3-page-ce-header-icons-right {
   opacity: 1;
 }
+.t3-page-ce-danger:hover .t3-page-ce-header {
+  background-color: #a32e2e;
+  border-color: #a32e2e;
+}
+.t3-page-ce-danger:hover .t3-page-ce-body {
+  border-color: #a32e2e;
+}
+.t3-page-ce-danger .t3-page-ce-header {
+  background-color: #c83c3c;
+  border-color: #c83c3c;
+}
+.t3-page-ce-danger .t3-page-ce-header:hover {
+  background-color: #a32e2e;
+  border-color: #a32e2e;
+}
+.t3-page-ce-danger .t3-page-ce-body {
+  border-color: #c83c3c;
+}
 .t3-page-ce-hidden {
   opacity: 0.4;
   transition: opacity 0.15s ease-in;