[FEATURE] Show only sys_languages in Backend which are available
[Packages/TYPO3.CMS.git] / typo3 / sysext / info / Classes / Controller / TranslationStatusController.php
1 <?php
2
3 namespace TYPO3\CMS\Info\Controller;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Backend\Tree\View\PageTreeView;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
22 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
23 use TYPO3\CMS\Core\Imaging\Icon;
24 use TYPO3\CMS\Core\Imaging\IconFactory;
25 use TYPO3\CMS\Core\Site\Entity\Site;
26 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
27 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
28 use TYPO3\CMS\Core\Type\Bitmask\Permission;
29 use TYPO3\CMS\Core\Utility\GeneralUtility;
30
31 /**
32 * Class for displaying translation status of pages in the tree.
33 */
34 class TranslationStatusController extends \TYPO3\CMS\Backend\Module\AbstractFunctionModule
35 {
36 /**
37 * @var IconFactory
38 */
39 protected $iconFactory;
40
41 /**
42 * @var SiteLanguage[]
43 */
44 protected $siteLanguages;
45
46 /**
47 * Construct for initialize class variables
48 */
49 public function __construct()
50 {
51 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
52 $this->initializeSiteLanguages();
53 }
54
55 /**
56 * Returns the menu array
57 *
58 * @return array
59 */
60 public function modMenu()
61 {
62 $lang = $this->getLanguageService();
63 $menuArray = [
64 'depth' => [
65 0 => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'),
66 1 => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'),
67 2 => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'),
68 3 => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'),
69 4 => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'),
70 999 => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi')
71 ]
72 ];
73 // Languages:
74 $menuArray['lang'] = [];
75 foreach ($this->siteLanguages as $language) {
76 if ($language->getLanguageId() === 0) {
77 $menuArray['lang'][0] = '[All]';
78 } else {
79 $menuArray['lang'][$language->getLanguageId()] = $language->getTitle();
80 }
81 }
82 return $menuArray;
83 }
84
85 /**
86 * MAIN function for page information of localization
87 *
88 * @return string Output HTML for the module.
89 */
90 public function main()
91 {
92 $theOutput = '<h1>' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_title')) . '</h1>';
93 if ($this->pObj->id) {
94 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Info/TranslationStatus');
95
96 // Depth selector:
97 $theOutput .= '<div class="form-inline form-inline-spaced">';
98 $h_func = BackendUtility::getDropdownMenu($this->pObj->id, 'SET[depth]', $this->pObj->MOD_SETTINGS['depth'], $this->pObj->MOD_MENU['depth']);
99 $h_func .= BackendUtility::getDropdownMenu($this->pObj->id, 'SET[lang]', $this->pObj->MOD_SETTINGS['lang'], $this->pObj->MOD_MENU['lang']);
100 $theOutput .= $h_func;
101 // Add CSH:
102 $theOutput .= BackendUtility::cshItem('_MOD_web_info', 'lang', null, '<div class="form-group"><span class="btn btn-default btn-sm">|</span></div><br />');
103 $theOutput .= '</div>';
104 // Showing the tree:
105 // Initialize starting point of page tree:
106 $treeStartingPoint = (int)$this->pObj->id;
107 $treeStartingRecord = BackendUtility::getRecordWSOL('pages', $treeStartingPoint);
108 $depth = $this->pObj->MOD_SETTINGS['depth'];
109 // Initialize tree object:
110 $tree = GeneralUtility::makeInstance(PageTreeView::class);
111 $tree->init('AND ' . $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
112 $tree->addField('l18n_cfg');
113 // Creating top icon; the current page
114 $HTML = $this->iconFactory->getIconForRecord('pages', $treeStartingRecord, Icon::SIZE_SMALL)->render();
115 $tree->tree[] = [
116 'row' => $treeStartingRecord,
117 'HTML' => $HTML
118 ];
119 // Create the tree from starting point:
120 if ($depth) {
121 $tree->getTree($treeStartingPoint, $depth, '');
122 }
123 // Render information table:
124 $theOutput .= $this->renderL10nTable($tree);
125 }
126 return $theOutput;
127 }
128
129 /**
130 * Rendering the localization information table.
131 *
132 * @param array $tree The Page tree data
133 * @return string HTML for the localization information table.
134 */
135 public function renderL10nTable(&$tree)
136 {
137 $lang = $this->getLanguageService();
138 // Title length:
139 $titleLen = $this->getBackendUser()->uc['titleLen'];
140 // Put together the TREE:
141 $output = '';
142 $newOL_js = [];
143 $langRecUids = [];
144 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
145 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
146 foreach ($tree->tree as $data) {
147 $tCells = [];
148 $langRecUids[0][] = $data['row']['uid'];
149 // Page icons / titles etc.
150 $tCells[] = '<td' . ($data['row']['_CSSCLASS'] ? ' class="' . $data['row']['_CSSCLASS'] . '"' : '') . '>' .
151 ($data['depthData'] ?: '') .
152 BackendUtility::wrapClickMenuOnIcon($data['HTML'], 'pages', $data['row']['uid']) .
153 '<a href="#" onclick="' . htmlspecialchars(
154 'top.loadEditId(' . (int)$data['row']['uid'] . ',"&SET[language]=0"); return false;'
155 ) . '" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editPage') . '">' .
156 htmlspecialchars(GeneralUtility::fixed_lgd_cs($data['row']['title'], $titleLen)) .
157 '</a>' .
158 ((string)$data['row']['nav_title'] !== '' ? ' [Nav: <em>' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($data['row']['nav_title'], $titleLen)) . '</em>]' : '') .
159 '</td>';
160 // DEFAULT language:
161 // "View page" link is created:
162 $viewPageLink = '<a href="#" onclick="' . htmlspecialchars(
163 BackendUtility::viewOnClick(
164 $data['row']['uid'],
165 '',
166 null,
167 '',
168 '',
169 '&L=###LANG_UID###'
170 )
171 ) . '" class="btn btn-default" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_viewPage') . '">' .
172 $this->iconFactory->getIcon('actions-view', Icon::SIZE_SMALL)->render() . '</a>';
173 $status = GeneralUtility::hideIfDefaultLanguage($data['row']['l18n_cfg']) ? 'danger' : 'success';
174 // Create links:
175 $editUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
176 'edit' => [
177 'pages' => [
178 $data['row']['uid'] => 'edit'
179 ]
180 ],
181 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
182 ]);
183 $info = '<a href="#" onclick="' . htmlspecialchars(
184 BackendUtility::viewOnClick(
185 $data['row']['uid'],
186 '',
187 null,
188 '',
189 '',
190 ''
191 )
192 ) . '" class="btn btn-default" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_viewPage') . '">' .
193 $this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL)->render() . '</a>';
194 $info .= '<a href="' . htmlspecialchars($editUrl)
195 . '" class="btn btn-default" title="' . $lang->sL(
196 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editDefaultLanguagePage'
197 ) . '">' . $this->iconFactory->getIcon('actions-page-open', Icon::SIZE_SMALL)->render() . '</a>';
198 $info .= '&nbsp;';
199 $info .= GeneralUtility::hideIfDefaultLanguage($data['row']['l18n_cfg']) ? '<span title="' . htmlspecialchars($lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.l18n_cfg.I.1')) . '">D</span>' : '&nbsp;';
200 $info .= GeneralUtility::hideIfNotTranslated($data['row']['l18n_cfg']) ? '<span title="' . htmlspecialchars($lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.l18n_cfg.I.2')) . '">N</span>' : '&nbsp;';
201 // Put into cell:
202 $tCells[] = '<td class="' . $status . ' col-border-left"><div class="btn-group">' . $info . '</div></td>';
203 $tCells[] = '<td class="' . $status . '" title="' . $lang->sL(
204 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_CEcount'
205 ) . '" align="center">' . $this->getContentElementCount($data['row']['uid'], 0) . '</td>';
206 // Traverse system languages:
207 foreach ($this->siteLanguages as $siteLanguage) {
208 $languageId = $siteLanguage->getLanguageId();
209 if ($languageId === 0) {
210 continue;
211 }
212 if ($this->pObj->MOD_SETTINGS['lang'] == 0 || (int)$this->pObj->MOD_SETTINGS['lang'] === $languageId) {
213 $row = $this->getLangStatus($data['row']['uid'], $languageId);
214 if (is_array($row)) {
215 $langRecUids[$languageId][] = $row['uid'];
216 $status = $row['_HIDDEN'] ? (GeneralUtility::hideIfNotTranslated($data['row']['l18n_cfg']) || GeneralUtility::hideIfDefaultLanguage($data['row']['l18n_cfg']) ? 'danger' : '') : 'success';
217 $icon = $this->iconFactory->getIconForRecord('pages', $row, Icon::SIZE_SMALL)->render();
218 $info = $icon . htmlspecialchars(
219 GeneralUtility::fixed_lgd_cs($row['title'], $titleLen)
220 ) . ((string)$row['nav_title'] !== '' ? ' [Nav: <em>' . htmlspecialchars(
221 GeneralUtility::fixed_lgd_cs($row['nav_title'], $titleLen)
222 ) . '</em>]' : '') . ($row['_COUNT'] > 1 ? '<div>' . $lang->sL(
223 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_badThingThereAre'
224 ) . '</div>' : '');
225 $tCells[] = '<td class="' . $status . ' col-border-left">' .
226 '<a href="#" onclick="' . htmlspecialchars(
227 'top.loadEditId(' . (int)$data['row']['uid'] . ',"&SET[language]=' . $languageId . '"); return false;'
228 ) . '" title="' . $lang->sL(
229 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editPageLang'
230 ) . '">' . $info . '</a></td>';
231 // Edit whole record:
232 // Create links:
233 $editUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
234 'edit' => [
235 'pages' => [
236 $row['uid'] => 'edit'
237 ]
238 ],
239 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
240 ]);
241 $info = str_replace('###LANG_UID###', $languageId, $viewPageLink);
242 $info .= '<a href="' . htmlspecialchars($editUrl)
243 . '" class="btn btn-default" title="' . $lang->sL(
244 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editLanguageOverlayRecord'
245 ) . '">' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
246 $tCells[] = '<td class="' . $status . '"><div class="btn-group">' . $info . '</div></td>';
247 $tCells[] = '<td class="' . $status . '" title="' . $lang->sL(
248 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_CEcount'
249 ) . '" align="center">' . $this->getContentElementCount($data['row']['uid'], $languageId) . '</td>';
250 } else {
251 $status = GeneralUtility::hideIfNotTranslated($data['row']['l18n_cfg']) || GeneralUtility::hideIfDefaultLanguage($data['row']['l18n_cfg']) ? 'danger' : '';
252 $info = '<div class="btn-group"><label class="btn btn-default btn-checkbox">';
253 $info .= '<input type="checkbox" data-lang="' . $languageId . '" name="newOL[' . $languageId . '][' . $data['row']['uid'] . ']" value="1" />';
254 $info .= '<span class="t3-icon fa"></span></label></div>';
255 $newOL_js[$languageId] .=
256 ' +(document.webinfoForm['
257 . GeneralUtility::quoteJSvalue('newOL[' . $languageId . '][' . $data['row']['uid'] . ']')
258 . '].checked ? '
259 . GeneralUtility::quoteJSvalue('&edit[pages][' . $data['row']['uid'] . ']=new')
260 . ' : \'\')'
261 ;
262 $tCells[] = '<td class="' . $status . ' col-border-left">&nbsp;</td>';
263 $tCells[] = '<td class="' . $status . '">&nbsp;</td>';
264 $tCells[] = '<td class="' . $status . '">' . $info . '</td>';
265 }
266 }
267 }
268 $output .= '
269 <tr>
270 ' . implode('
271 ', $tCells) . '
272 </tr>';
273 }
274 // Put together HEADER:
275 $tCells = [];
276 $tCells[] = '<td>' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_page') . '</td>';
277 if (is_array($langRecUids[0])) {
278 $editUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
279 'edit' => [
280 'pages' => [
281 implode(',', $langRecUids[0]) => 'edit'
282 ]
283 ],
284 'columnsOnly' => 'title,nav_title,l18n_cfg,hidden',
285 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
286 ]);
287 $editIco = '<a href="' . htmlspecialchars($editUrl)
288 . '" class="btn btn-default" title="' . $lang->sL(
289 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editPageProperties'
290 ) . '">' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
291 } else {
292 $editIco = '';
293 }
294 $tCells[] = '<td class="col-border-left" colspan="2">' . $lang->sL(
295 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_default'
296 ) . '&nbsp;' . $editIco . '</td>';
297 foreach ($this->siteLanguages as $siteLanguage) {
298 $languageId = $siteLanguage->getLanguageId();
299 if ($languageId === 0) {
300 continue;
301 }
302 if ($this->pObj->MOD_SETTINGS['lang'] == 0 || (int)$this->pObj->MOD_SETTINGS['lang'] === $languageId) {
303 // Title:
304 $tCells[] = '<td class="col-border-left">' . htmlspecialchars($siteLanguage->getTitle()) . '</td>';
305 // Edit language overlay records:
306 if (is_array($langRecUids[$languageId])) {
307 $editUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
308 'edit' => [
309 'pages' => [
310 implode(',', $langRecUids[$languageId]) => 'edit'
311 ]
312 ],
313 'columnsOnly' => 'title,nav_title,hidden',
314 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
315 ]);
316 $editButton = '<a href="' . htmlspecialchars($editUrl)
317 . '" class="btn btn-default" title="' . $lang->sL(
318 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_editLangOverlays'
319 ) . '">' . $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
320 } else {
321 $editButton = '';
322 }
323 // Create new overlay records:
324 $params = '&columnsOnly=title,hidden,sys_language_uid&overrideVals[pages][sys_language_uid]=' . $languageId;
325 $onClick = BackendUtility::editOnClick($params);
326 if (!empty($newOL_js[$languageId])) {
327 $onClickArray = explode('?', $onClick, 2);
328 $lastElement = array_pop($onClickArray);
329 $onClickArray[] = '\'' . $newOL_js[$languageId] . ' + \'&' . $lastElement;
330 $onClick = implode('?', $onClickArray);
331 }
332 $newButton = '<a href="#" class="btn btn-default disabled t3js-language-new-' . $languageId . '" onclick="' . htmlspecialchars($onClick)
333 . '" title="' . $lang->sL(
334 'LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_getlangsta_createNewTranslationHeaders'
335 ) . '">' . $this->iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render() . '</a>';
336
337 $tCells[] = '<td class="btn-group">' . $editButton . $newButton . '</td>';
338 $tCells[] = '<td>&nbsp;</td>';
339 }
340 }
341
342 $output =
343 '<div class="table-fit">' .
344 '<table class="table table-striped table-hover" id="langTable">' .
345 '<thead>' .
346 '<tr>' .
347 implode('', $tCells) .
348 '</tr>' .
349 '</thead>' .
350 '<tbody>' .
351 $output .
352 '</tbody>' .
353 '</table>' .
354 '</div>';
355 return $output;
356 }
357
358 /**
359 * Selects all system languages (from sys_language)
360 *
361 * @return array System language records in an array.
362 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
363 */
364 public function getSystemLanguages()
365 {
366 trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
367 if (!$this->getBackendUser()->isAdmin() && $this->getBackendUser()->groupData['allowed_languages'] !== '') {
368 $allowed_languages = array_flip(explode(',', $this->getBackendUser()->groupData['allowed_languages']));
369 }
370 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
371 ->getQueryBuilderForTable('sys_language')
372 ->select('*')
373 ->from('sys_language')
374 ->orderBy('sorting');
375 $res = $queryBuilder->execute();
376 $outputArray = [];
377 if (is_array($allowed_languages) && !empty($allowed_languages)) {
378 while ($output = $res->fetch()) {
379 if (isset($allowed_languages[$output['uid']])) {
380 $outputArray[] = $output;
381 }
382 }
383 } else {
384 $outputArray = $res->fetchAll();
385 }
386 return $outputArray;
387 }
388
389 /**
390 * Get an alternative language record for a specific page / language
391 *
392 * @param int $pageId Page ID to look up for.
393 * @param int $langId Language UID to select for.
394 * @return array translated pages record
395 */
396 public function getLangStatus($pageId, $langId)
397 {
398 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
399 ->getQueryBuilderForTable('pages');
400 $queryBuilder
401 ->getRestrictions()
402 ->removeAll()
403 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class))
404 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
405 $result = $queryBuilder
406 ->select('*')
407 ->from('pages')
408 ->where(
409 $queryBuilder->expr()->eq(
410 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
411 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
412 )
413 )
414 ->andWhere(
415 $queryBuilder->expr()->eq(
416 $GLOBALS['TCA']['pages']['ctrl']['languageField'],
417 $queryBuilder->createNamedParameter($langId, \PDO::PARAM_INT)
418 )
419 )
420 ->execute();
421
422 $row = $result->fetch();
423 BackendUtility::workspaceOL('pages', $row);
424 if (is_array($row)) {
425 $row['_COUNT'] = $queryBuilder->count('uid')->execute()->fetchColumn(0);
426 $row['_HIDDEN'] = $row['hidden'] || (int)$row['endtime'] > 0 && (int)$row['endtime'] < $GLOBALS['EXEC_TIME'] || $GLOBALS['EXEC_TIME'] < (int)$row['starttime'];
427 }
428 $result->closeCursor();
429 return $row;
430 }
431
432 /**
433 * Counting content elements for a single language on a page.
434 *
435 * @param int $pageId Page id to select for.
436 * @param int $sysLang Sys language uid
437 * @return int Number of content elements from the PID where the language is set to a certain value.
438 */
439 public function getContentElementCount($pageId, $sysLang)
440 {
441 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
442 ->getQueryBuilderForTable('tt_content');
443 $queryBuilder->getRestrictions()
444 ->removeAll()
445 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
446 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
447 $count = $queryBuilder
448 ->count('uid')
449 ->from('tt_content')
450 ->where(
451 $queryBuilder->expr()->eq(
452 'pid',
453 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
454 )
455 )
456 ->andWhere(
457 $queryBuilder->expr()->eq(
458 'sys_language_uid',
459 $queryBuilder->createNamedParameter($sysLang, \PDO::PARAM_INT)
460 )
461 )
462 ->execute()
463 ->fetchColumn(0);
464 return $count ?: '-';
465 }
466
467 /**
468 * Returns LanguageService
469 *
470 * @return \TYPO3\CMS\Core\Localization\LanguageService
471 */
472 protected function getLanguageService()
473 {
474 return $GLOBALS['LANG'];
475 }
476
477 /**
478 * Returns the current BE user.
479 *
480 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
481 */
482 protected function getBackendUser()
483 {
484 return $GLOBALS['BE_USER'];
485 }
486
487 /**
488 * Since the AbstractFunctionModule cannot access the current request yet, we'll do it "old school"
489 * to fetch the Site based on the current ID.
490 */
491 protected function initializeSiteLanguages()
492 {
493 /** @var SiteInterface $currentSite */
494 $currentSite = $GLOBALS['TYPO3_REQUEST']->getAttribute('site');
495 $this->siteLanguages = $currentSite->getAvailableLanguages($this->getBackendUser(), false, (int)$this->pObj->id);
496 }
497 }