1<?php
2namespace TYPO3\CMS\Linkvalidator\Report;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use Doctrine\DBAL\Driver\Statement;
18use TYPO3\CMS\Backend\Template\DocumentTemplate;
19use TYPO3\CMS\Backend\Template\ModuleTemplate;
20use TYPO3\CMS\Backend\Utility\BackendUtility;
21use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
22use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
23use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
24use TYPO3\CMS\Core\Database\Connection;
25use TYPO3\CMS\Core\Database\ConnectionPool;
26use TYPO3\CMS\Core\Database\Query\QueryHelper;
27use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
28use TYPO3\CMS\Core\Imaging\Icon;
29use TYPO3\CMS\Core\Imaging\IconFactory;
30use TYPO3\CMS\Core\Localization\LanguageService;
31use TYPO3\CMS\Core\Messaging\FlashMessage;
32use TYPO3\CMS\Core\Messaging\FlashMessageService;
33use TYPO3\CMS\Core\Page\PageRenderer;
34use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
35use TYPO3\CMS\Core\Type\Bitmask\Permission;
36use TYPO3\CMS\Core\Utility\GeneralUtility;
37use TYPO3\CMS\Info\Controller\InfoModuleController;
38use TYPO3\CMS\Linkvalidator\LinkAnalyzer;
39
40/**
41 * Module 'Link validator' as sub module of Web -> Info
42 * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
43 */
44class LinkValidatorReport
45{
46    use PublicPropertyDeprecationTrait;
47    use PublicMethodDeprecationTrait;
48
49    /**
50     * @var array
51     */
52    private $deprecatedPublicProperties = [
53        'pObj' => 'Using LinkValidatorReport::$pObj is deprecated and will not be possible anymore in TYPO3 v10.0.',
54        'doc' => 'Using LinkValidatorReport::$doc is deprecated and will not be possible anymore in TYPO3 v10.0.',
55        'function_key' => 'Using LinkValidatorReport::$function_key is deprecated, property will be removed in TYPO3 v10.0.',
56        'extClassConf' => 'Using LinkValidatorReport::$extClassConf is deprecated, property will be removed in TYPO3 v10.0.',
57        'localLangFile' => 'Using LinkValidatorReport::$localLangFile is deprecated, property will be removed in TYPO3 v10.0.',
58        'extObj' => 'Using LinkValidatorReport::$extObj is deprecated, property will be removed in TYPO3 v10.0.',
59    ];
60
61    /**
62     * @var array
63     */
64    private $deprecatedPublicMethods = [
65        'extObjContent' => 'Using LinkValidatorReport::extObjContent() is deprecated, method will be removed in TYPO3 v10.0.',
66    ];
67
68    /**
69     * @var DocumentTemplate
70     */
71    protected $doc;
72
73    /**
74     * Information about the current page record
75     *
76     * @var array
77     */
78    protected $pageRecord = [];
79
80    /**
81     * Information, if the module is accessible for the current user or not
82     *
83     * @var bool
84     */
85    protected $isAccessibleForCurrentUser = false;
86
87    /**
88     * Link validation class
89     *
90     * @var LinkAnalyzer
91     */
92    protected $linkAnalyzer;
93
94    /**
95     * TSconfig of the current module
96     *
97     * @var array
98     */
99    protected $modTS = [];
100
101    /**
102     * List of available link types to check defined in the TSconfig
103     *
104     * @var array
105     */
106    protected $availableOptions = [];
107
108    /**
109     * Depth for the recursive traversal of pages for the link validation
110     * For "Report" and "Check link" tab.
111     *
112     * @var array
113     */
114    protected $searchLevel = ['report' => 0, 'check' => 0];
115
116    /**
117     * List of link types currently chosen in the statistics table
118     * Used to show broken links of these types only
119     * For "Report" and "Check link" tab
120     *
121     * @var array
122     */
123    protected $checkOpt = ['report' => [], 'check' => []];
124
125    /**
126     * Html for the statistics table with the checkboxes of the link types
127     * and the numbers of broken links
128     * For "Report" and "Check link" tab
129     *
130     * @var array
131     */
132    protected $checkOptionsHtml = ['report' => [], 'check' => []];
133
134    /**
135     * Complete content (html) to be displayed
136     *
137     * @var string
138     */
139    protected $content;
140
141    /**
142     * @var \TYPO3\CMS\Linkvalidator\Linktype\LinktypeInterface[]
143     */
144    protected $hookObjectsArr = [];
145
146    /**
147     * @var string
148     */
149    protected $updateListHtml = '';
150
151    /**
152     * @var string
153     */
154    protected $refreshListHtml = '';
155
156    /**
157     * @var MarkerBasedTemplateService
158     */
159    protected $templateService;
160
161    /**
162     * @var IconFactory
163     */
164    protected $iconFactory;
165
166    /**
167     * @var int Value of the GET/POST var 'id'
168     */
169    protected $id;
170
171    /**
172     * @var InfoModuleController Contains a reference to the parent calling object
173     */
174    protected $pObj;
175
176    /**
177     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
178     */
179    protected $extObj;
180
181    /**
182     * Can be hardcoded to the name of a locallang.xlf file (from the same directory as the class file) to use/load
183     * and is included / added to $GLOBALS['LOCAL_LANG']
184     *
185     * @var string
186     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
187     */
188    protected $localLangFile = '';
189
190    /**
191     * Contains module configuration parts from TBE_MODULES_EXT if found
192     *
193     * @see handleExternalFunctionValue()
194     * @var array
195     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
196     */
197    protected $extClassConf;
198
199    /**
200     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
201     */
202    protected $function_key = '';
203
204    /**
205     * Init, called from parent object
206     *
207     * @param InfoModuleController $pObj A reference to the parent (calling) object
208     */
209    public function init($pObj)
210    {
211        $languageService = $this->getLanguageService();
212        $this->pObj = $pObj;
213        // Local lang:
214        if (!empty($this->localLangFile)) {
215            // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
216            $languageService->includeLLFile($this->localLangFile);
217        }
218        $this->id = (int)GeneralUtility::_GP('id');
219    }
220
221    /**
222     * Main, called from parent object
223     *
224     * @return string Module content
225     */
226    public function main()
227    {
228        $this->getLanguageService()->includeLLFile('EXT:linkvalidator/Resources/Private/Language/Module/locallang.xlf');
229        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
230        $update = GeneralUtility::_GP('updateLinkList');
231        $prefix = 'check';
232        $other = 'report';
233
234        if (empty($update)) {
235            $prefix = 'report';
236            $other = 'check';
237        }
238
239        // get searchLevel (number of levels of pages to check / show results)
240        $this->searchLevel[$prefix] = GeneralUtility::_GP($prefix . '_search_levels');
241        if (isset($this->id)) {
242            $this->modTS = BackendUtility::getPagesTSconfig($this->id)['mod.']['linkvalidator.'] ?? [];
243        }
244        if (isset($this->searchLevel[$prefix])) {
245            $this->pObj->MOD_SETTINGS[$prefix . '_searchlevel'] = $this->searchLevel[$prefix];
246        } else {
247            $this->searchLevel[$prefix] = $this->pObj->MOD_SETTINGS[$prefix . '_searchlevel'];
248        }
249        if (isset($this->pObj->MOD_SETTINGS[$other . '_searchlevel'])) {
250            $this->searchLevel[$other] = $this->pObj->MOD_SETTINGS[$other . '_searchlevel'];
251        }
252
253        // which linkTypes to check (internal, file, external, ...)
254        $set = GeneralUtility::_GP($prefix . '_SET');
255
256        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] ?? [] as $linkType => $value) {
257            // Compile list of all available types. Used for checking with button "Check Links".
258            if (strpos($this->modTS['linktypes'], $linkType) !== false) {
259                $this->availableOptions[$linkType] = 1;
260            }
261
262            // 1) if "$prefix_values" = "1" : use POST variables
263            // 2) if not set, use stored configuration in $this->>pObj->MOD_SETTINGS
264            // 3) if not set, use default
265            unset($this->checkOpt[$prefix][$linkType]);
266            if (!empty(GeneralUtility::_GP($prefix . '_values'))) {
267                if (isset($set[$linkType])) {
268                    $this->checkOpt[$prefix][$linkType] = $set[$linkType];
269                } else {
270                    $this->checkOpt[$prefix][$linkType] = '0';
271                }
272                $this->pObj->MOD_SETTINGS[$prefix . '_' . $linkType] = $this->checkOpt[$prefix][$linkType];
273            } elseif (isset($this->pObj->MOD_SETTINGS[$prefix . '_' . $linkType])) {
274                $this->checkOpt[$prefix][$linkType] = $this->pObj->MOD_SETTINGS[$prefix . '_' . $linkType];
275            } else {
276                // use default
277                $this->checkOpt[$prefix][$linkType] = '0';
278                $this->pObj->MOD_SETTINGS[$prefix . '_' . $linkType] = $this->checkOpt[$prefix][$linkType];
279            }
280            if (isset($this->pObj->MOD_SETTINGS[$other . '_' . $linkType])) {
281                $this->checkOpt[$other][$linkType] = $this->pObj->MOD_SETTINGS[$other . '_' . $linkType];
282            }
283        }
284
285        // save settings
286        $this->getBackendUser()->pushModuleData('web_info', $this->pObj->MOD_SETTINGS);
287        $this->initialize();
288
289        // Localization
290        $this->getPageRenderer()->addInlineLanguageLabelFile('EXT:linkvalidator/Resources/Private/Language/Module/locallang.xlf');
291
292        if ($this->modTS['showCheckLinkTab'] == 1) {
293            $this->updateListHtml = '<input class="btn btn-default t3js-update-button" type="submit" name="updateLinkList" id="updateLinkList" value="'
294                . htmlspecialchars($this->getLanguageService()->getLL('label_update'))
295                . '" data-notification-message="'
296                . htmlspecialchars($this->getLanguageService()->getLL('label_update-link-list'))
297                . '"/>';
298        }
299        $this->refreshListHtml = '<input class="btn btn-default t3js-update-button" type="submit" name="refreshLinkList" id="refreshLinkList" value="'
300            . htmlspecialchars($this->getLanguageService()->getLL('label_refresh'))
301            . '" data-notification-message="'
302            . htmlspecialchars($this->getLanguageService()->getLL('label_refresh-link-list'))
303            . '"/>';
304        $this->linkAnalyzer = GeneralUtility::makeInstance(LinkAnalyzer::class);
305        $this->updateBrokenLinks();
306
307        $brokenLinkOverView = $this->linkAnalyzer->getLinkCounts($this->id);
308        $this->checkOptionsHtml['report'] = $this->getCheckOptions($brokenLinkOverView, 'report');
309        $this->checkOptionsHtml['check'] = $this->getCheckOptions($brokenLinkOverView, 'check');
310        $this->render();
311
312        $pageTile = '';
313        if ($this->id) {
314            $pageRecord = BackendUtility::getRecord('pages', $this->id);
315            $pageTile = '<h1>' . htmlspecialchars(BackendUtility::getRecordTitle('pages', $pageRecord)) . '</h1>';
316        }
317
318        return '<div id="linkvalidator-modfuncreport">' . $pageTile . $this->createTabs() . '</div>';
319    }
320
321    /**
322     * Create tabs to split the report and the checkLink functions
323     *
324     * @return string
325     */
326    protected function createTabs()
327    {
328        $languageService = $this->getLanguageService();
329        $menuItems = [
330            0 => [
331                'label' => $languageService->getLL('Report'),
332                'content' => $this->flush(true)
333            ],
334        ];
335
336        if ((bool)$this->modTS['showCheckLinkTab']) {
337            $menuItems[1] = [
338                'label' => $languageService->getLL('CheckLink'),
339                'content' => $this->flush()
340            ];
341        }
342
343        $moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
344        return $moduleTemplate->getDynamicTabMenu($menuItems, 'report-linkvalidator');
345    }
346
347    /**
348     * Initializes the Module
349     */
350    protected function initialize()
351    {
352        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] ?? [] as $linkType => $className) {
353            $this->hookObjectsArr[$linkType] = GeneralUtility::makeInstance($className);
354        }
355
356        $this->doc = GeneralUtility::makeInstance(DocumentTemplate::class);
357        $this->doc->setModuleTemplate('EXT:linkvalidator/Resources/Private/Templates/mod_template.html');
358
359        $this->pageRecord = BackendUtility::readPageAccess($this->id, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
360        if ($this->id && is_array($this->pageRecord) || !$this->id && $this->isCurrentUserAdmin()) {
361            $this->isAccessibleForCurrentUser = true;
362        }
363
364        $pageRenderer = $this->getPageRenderer();
365        $pageRenderer->addCssFile('EXT:linkvalidator/Resources/Public/Css/linkvalidator.css', 'stylesheet', 'screen');
366        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Linkvalidator/Linkvalidator');
367
368        $this->templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
369
370        // Don't access in workspace
371        if ($this->getBackendUser()->workspace !== 0) {
372            $this->isAccessibleForCurrentUser = false;
373        }
374    }
375
376    /**
377     * Updates the table of stored broken links
378     */
379    protected function updateBrokenLinks()
380    {
381        $searchFields = [];
382        // Get the searchFields from TypoScript
383        foreach ($this->modTS['searchFields.'] as $table => $fieldList) {
384            $fields = GeneralUtility::trimExplode(',', $fieldList, true);
385            foreach ($fields as $field) {
386                if (!$searchFields || !is_array($searchFields[$table]) || !in_array($field, $searchFields[$table], true)) {
387                    $searchFields[$table][] = $field;
388                }
389            }
390        }
391        $rootLineHidden = $this->linkAnalyzer->getRootLineIsHidden($this->pObj->pageinfo);
392        if (!$rootLineHidden || $this->modTS['checkhidden'] == 1) {
393            $permsClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
394            // Get children pages
395            $pageList = $this->linkAnalyzer->extGetTreeList(
396                $this->id,
397                $this->searchLevel['check'],
398                0,
399                $permsClause,
400                $this->modTS['checkhidden']
401            );
402            if ($this->pObj->pageinfo['hidden'] == 0 || $this->modTS['checkhidden']) {
403                $pageList .= $this->id;
404                $pageList = $this->addPageTranslationsToPageList($pageList, $permsClause);
405            }
406
407            $this->linkAnalyzer->init($searchFields, $pageList, $this->modTS);
408
409            // Check if button press
410            $update = GeneralUtility::_GP('updateLinkList');
411            if (!empty($update)) {
412                $this->linkAnalyzer->getLinkStatistics($this->checkOpt['check'], $this->modTS['checkhidden']);
413            }
414        }
415    }
416
417    /**
418     * Renders the content of the module
419     */
420    protected function render()
421    {
422        if ($this->isAccessibleForCurrentUser) {
423            $this->content = $this->renderBrokenLinksTable();
424        } else {
425            $languageService = $this->getLanguageService();
426            // If no access or if ID == zero
427            $message = GeneralUtility::makeInstance(
428                FlashMessage::class,
429                $languageService->getLL('no.access'),
430                $languageService->getLL('no.access.title'),
431                FlashMessage::ERROR
432            );
433            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
434            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
435            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
436            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
437            $defaultFlashMessageQueue->enqueue($message);
438        }
439    }
440
441    /**
442     * Flushes the rendered content to the browser
443     *
444     * @param bool $form
445     * @return string $content
446     */
447    protected function flush($form = false)
448    {
449        return $this->doc->moduleBody(
450            $this->pageRecord,
451            $this->getDocHeaderButtons(),
452            $form ? $this->getTemplateMarkers() : $this->getTemplateMarkersCheck()
453        );
454    }
455
456    /**
457     * Builds the selector for the level of pages to search
458     *
459     * @param string $prefix Indicating if the selector is build for the "report" or "check" tab
460     *
461     * @return string Html code of that selector
462     */
463    protected function getLevelSelector($prefix = 'report')
464    {
465        $languageService = $this->getLanguageService();
466        // Build level selector
467        $options = [];
468        $availableOptions = [
469            0 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'),
470            1 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'),
471            2 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'),
472            3 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'),
473            4 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'),
474            999 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi')
475        ];
476        foreach ($availableOptions as $optionValue => $optionLabel) {
477            $options[] = '<option value="' . $optionValue . '"' . ($optionValue === (int)$this->searchLevel[$prefix] ? ' selected="selected"' : '') . '>' . htmlspecialchars($optionLabel) . '</option>';
478        }
479        return '<select name="' . $prefix . '_search_levels" class="form-control">' . implode('', $options) . '</select>';
480    }
481
482    /**
483     * Displays the table of broken links or a note if there were no broken links
484     *
485     * @return string Content of the table or of the note
486     */
487    protected function renderBrokenLinksTable()
488    {
489        $brokenLinkItems = '';
490        $brokenLinksTemplate = $this->templateService->getSubpart(
491            $this->doc->moduleTemplate,
492            '###NOBROKENLINKS_CONTENT###'
493        );
494
495        $linkTypes = [];
496        if (is_array($this->checkOpt['report'])) {
497            $linkTypes = array_keys($this->checkOpt['report'], '1');
498        }
499
500        // Table header
501        $brokenLinksMarker = $this->startTable();
502
503        $rootLineHidden = $this->linkAnalyzer->getRootLineIsHidden($this->pObj->pageinfo);
504        if (!$rootLineHidden || (bool)$this->modTS['checkhidden']) {
505            $pageList = $this->getPageList();
506            $result = false;
507            if (!empty($linkTypes)) {
508                $result = $this->getLinkValidatorBrokenLinks($pageList, $linkTypes);
509            }
510
511            if ($result && $result->rowCount()) {
512                // Display table with broken links
513                $brokenLinksTemplate = $this->templateService->getSubpart(
514                    $this->doc->moduleTemplate,
515                    '###BROKENLINKS_CONTENT###'
516                );
517                $brokenLinksItemTemplate = $this->templateService->getSubpart(
518                    $this->doc->moduleTemplate,
519                    '###BROKENLINKS_ITEM###'
520                );
521
522                // Table rows containing the broken links
523                $items = [];
524                while ($row = $result->fetch()) {
525                    $items[] = $this->renderTableRow($row['table_name'], $row, $brokenLinksItemTemplate);
526                }
527                $brokenLinkItems = implode(LF, $items);
528            } else {
529                $brokenLinksMarker = $this->getNoBrokenLinkMessage($brokenLinksMarker);
530            }
531        } else {
532            $brokenLinksMarker = $this->getNoBrokenLinkMessage($brokenLinksMarker);
533        }
534
535        $brokenLinksTemplate = $this->templateService->substituteMarkerArray(
536            $brokenLinksTemplate,
537            $brokenLinksMarker,
538            '###|###',
539            true
540        );
541
542        return $this->templateService->substituteSubpart($brokenLinksTemplate, '###BROKENLINKS_ITEM', $brokenLinkItems);
543    }
544
545    /**
546     * Generates an array of page uids from current pageUid.
547     * List does include pageUid itself.
548     *
549     * @return array
550     */
551    protected function getPageList(): array
552    {
553        $permsClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
554        $pageList = $this->linkAnalyzer->extGetTreeList(
555            $this->id,
556            $this->searchLevel['report'],
557            0,
558            $permsClause,
559            $this->modTS['checkhidden']
560        );
561        // Always add the current page, because we are just displaying the results
562        $pageList .= $this->id;
563        $pageList = $this->addPageTranslationsToPageList($pageList, $permsClause);
564
565        return GeneralUtility::intExplode(',', $pageList, true);
566    }
567
568    /**
569     * Prepare database query with pageList and keyOpt data.
570     *
571     * @param int[] $pageList Pages to check for broken links
572     * @param string[] $linkTypes Link types to validate
573     * @return Statement
574     */
575    protected function getLinkValidatorBrokenLinks(array $pageList, array $linkTypes): Statement
576    {
577        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
578            ->getQueryBuilderForTable('tx_linkvalidator_link');
579        $queryBuilder
580            ->select('*')
581            ->from('tx_linkvalidator_link')
582            ->where(
583                $queryBuilder->expr()->orX(
584                    $queryBuilder->expr()->andX(
585                        $queryBuilder->expr()->in(
586                            'record_uid',
587                            $queryBuilder->createNamedParameter($pageList, Connection::PARAM_INT_ARRAY)
588                        ),
589                        $queryBuilder->expr()->eq('table_name', $queryBuilder->createNamedParameter('pages'))
590                    ),
591                    $queryBuilder->expr()->andX(
592                        $queryBuilder->expr()->in(
593                            'record_pid',
594                            $queryBuilder->createNamedParameter($pageList, Connection::PARAM_INT_ARRAY)
595                        ),
596                        $queryBuilder->expr()->neq('table_name', $queryBuilder->createNamedParameter('pages'))
597                    )
598                )
599            )
600            ->orderBy('record_uid')
601            ->addOrderBy('uid');
602
603        if (!empty($linkTypes)) {
604            $queryBuilder->andWhere(
605                $queryBuilder->expr()->in(
606                    'link_type',
607                    $queryBuilder->createNamedParameter($linkTypes, Connection::PARAM_STR_ARRAY)
608                )
609            );
610        }
611
612        return $queryBuilder->execute();
613    }
614
615    /**
616     * Replace $brokenLinksMarker['NO_BROKEN_LINKS] with localized flashmessage
617     *
618     * @param array $brokenLinksMarker
619     * @return array $brokenLinksMarker['NO_BROKEN_LINKS] replaced with flashmessage
620     */
621    protected function getNoBrokenLinkMessage(array $brokenLinksMarker)
622    {
623        $languageService = $this->getLanguageService();
624        $brokenLinksMarker['LIST_HEADER'] = '<h3>' . htmlspecialchars($languageService->getLL('list.header')) . '</h3>';
625        /** @var FlashMessage $message */
626        $message = GeneralUtility::makeInstance(
627            FlashMessage::class,
628            $languageService->getLL('list.no.broken.links'),
629            $languageService->getLL('list.no.broken.links.title'),
630            FlashMessage::OK
631        );
632        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
633        /** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
634        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
635        $defaultFlashMessageQueue->enqueue($message);
636        $brokenLinksMarker['NO_BROKEN_LINKS'] = $defaultFlashMessageQueue->renderFlashMessages();
637        return $brokenLinksMarker;
638    }
639
640    /**
641     * Displays the table header of the table with the broken links
642     *
643     * @return array Code of content
644     */
645    protected function startTable()
646    {
647        $languageService = $this->getLanguageService();
648        // Listing head
649        $makerTableHead = [
650            'tablehead_path' => $languageService->getLL('list.tableHead.path'),
651            'tablehead_element' => $languageService->getLL('list.tableHead.element'),
652            'tablehead_headlink' => $languageService->getLL('list.tableHead.headlink'),
653            'tablehead_linktarget' => $languageService->getLL('list.tableHead.linktarget'),
654            'tablehead_linkmessage' => $languageService->getLL('list.tableHead.linkmessage'),
655            'tablehead_lastcheck' => $languageService->getLL('list.tableHead.lastCheck'),
656        ];
657
658        // Add CSH to the header of each column
659        foreach ($makerTableHead as $column => $label) {
660            $makerTableHead[$column] = BackendUtility::wrapInHelp('linkvalidator', $column, $label);
661        }
662        // Add section header
663        $makerTableHead['list_header'] = '<h3>' . htmlspecialchars($languageService->getLL('list.header')) . '</h3>';
664        return $makerTableHead;
665    }
666
667    /**
668     * Displays one line of the broken links table
669     *
670     * @param string $table Name of database table
671     * @param array $row Record row to be processed
672     * @param array $brokenLinksItemTemplate Markup of the template to be used
673     * @return string HTML of the rendered row
674     */
675    protected function renderTableRow($table, array $row, $brokenLinksItemTemplate)
676    {
677        $languageService = $this->getLanguageService();
678        $markerArray = [];
679        $fieldName = '';
680        // Restore the linktype object
681        $hookObj = $this->hookObjectsArr[$row['link_type']];
682
683        // Construct link to edit the content element
684        $requestUri = GeneralUtility::getIndpEnv('REQUEST_URI') .
685            '&id=' . $this->id .
686            '&search_levels=' . $this->searchLevel['report'];
687        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
688        $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
689        $url = (string)$uriBuilder->buildUriFromRoute('record_edit', [
690            'edit' => [
691                $table => [
692                    $row['record_uid'] => 'edit'
693                ]
694            ],
695            'columnsOnly' => $row['field'],
696            'returnUrl' => $requestUri
697        ]);
698        $actionLinkOpen = '<a href="' . htmlspecialchars($url);
699        $actionLinkOpen .= '" title="' . htmlspecialchars($languageService->getLL('list.edit')) . '">';
700        $actionLinkClose = '</a>';
701        $elementHeadline = $row['headline'];
702        // Get the language label for the field from TCA
703        if ($GLOBALS['TCA'][$table]['columns'][$row['field']]['label']) {
704            $fieldName = $languageService->sL($GLOBALS['TCA'][$table]['columns'][$row['field']]['label']);
705            // Crop colon from end if present
706            if (substr($fieldName, '-1', '1') === ':') {
707                $fieldName = substr($fieldName, '0', strlen($fieldName) - 1);
708            }
709        }
710        // Fallback, if there is no label
711        $fieldName = !empty($fieldName) ? $fieldName : $row['field'];
712        // column "Element"
713        $element = '<span title="' . htmlspecialchars($table . ':' . $row['record_uid']) . '">' . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>';
714        if (empty($elementHeadline)) {
715            $element .= '<i>' . htmlspecialchars($languageService->getLL('list.no.headline')) . '</i>';
716        } else {
717            $element .= htmlspecialchars($elementHeadline);
718        }
719        $element .= ' ' . htmlspecialchars(sprintf($languageService->getLL('list.field'), $fieldName));
720        $markerArray['actionlinkOpen'] = $actionLinkOpen;
721        $markerArray['actionlinkClose'] = $actionLinkClose;
722        $markerArray['actionlinkIcon'] = $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render();
723        $markerArray['path'] = BackendUtility::getRecordPath($row['record_pid'], '', 0, 0);
724        $markerArray['element'] = $element;
725        $markerArray['headlink'] = htmlspecialchars($row['link_title']);
726        $markerArray['linktarget'] = htmlspecialchars($hookObj->getBrokenUrl($row));
727        $response = unserialize($row['url_response']);
728        if ($response['valid']) {
729            $linkMessage = '<span class="valid">' . htmlspecialchars($languageService->getLL('list.msg.ok')) . '</span>';
730        } else {
731            $linkMessage = '<span class="error">'
732                . nl2br(
733                // Encode for output
734                    htmlspecialchars(
735                        $hookObj->getErrorMessage($response['errorParams']),
736                        ENT_QUOTES,
737                        'UTF-8',
738                        false
739                    )
740                )
741                . '</span>';
742        }
743        $markerArray['linkmessage'] = $linkMessage;
744
745        $lastRunDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $row['last_check']);
746        $lastRunTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $row['last_check']);
747        $markerArray['lastcheck'] = htmlspecialchars(sprintf($languageService->getLL('list.msg.lastRun'), $lastRunDate, $lastRunTime));
748
749        // Return the table html code as string
750        return $this->templateService->substituteMarkerArray($brokenLinksItemTemplate, $markerArray, '###|###', true, true);
751    }
752
753    /**
754     * Builds the checkboxes out of the hooks array
755     *
756     * @param array $brokenLinkOverView Array of broken links information
757     * @param string $prefix "report" or "check" for "Report" and "Check links" tab
758     * @return string code content
759     */
760    protected function getCheckOptions(array $brokenLinkOverView, $prefix = 'report')
761    {
762        $languageService = $this->getLanguageService();
763        $markerArray = [];
764        if (!empty($prefix)) {
765            $additionalAttr = ' class="' . $prefix . '"';
766        } else {
767            $additionalAttr = ' class="refresh"';
768        }
769        $checkOptionsTemplate = $this->templateService->getSubpart($this->doc->moduleTemplate, '###CHECKOPTIONS_SECTION###');
770        $hookSectionTemplate = $this->templateService->getSubpart($checkOptionsTemplate, '###HOOK_SECTION###');
771        $markerArray['statistics_header'] = '<h3>' . htmlspecialchars($languageService->getLL('report.statistics.header')) . '</h3>';
772        $markerArray['total_count_label'] = BackendUtility::wrapInHelp('linkvalidator', 'checkboxes', $languageService->getLL('overviews.nbtotal'));
773        $markerArray['total_count'] = $brokenLinkOverView['brokenlinkCount'] ?: '0';
774
775        $linktypes = GeneralUtility::trimExplode(',', $this->modTS['linktypes'], true);
776        $hookSectionContent = '';
777        if (is_array($linktypes)) {
778            if (
779                !empty($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])
780                && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])
781            ) {
782                foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] as $type => $value) {
783                    if (in_array($type, $linktypes)) {
784                        $hookSectionMarker = [
785                            'count' => $brokenLinkOverView[$type] ?: '0',
786                        ];
787
788                        $translation = $languageService->getLL('hooks.' . $type) ?: $type;
789
790                        $checked = $this->checkOpt[$prefix][$type] ? 'checked="checked"' : '';
791
792                        $hookSectionMarker['option'] = '<input type="checkbox"' . $additionalAttr
793                            . ' id="' . $prefix . '_SET_' . $type
794                            . '" name="' . $prefix . '_SET[' . $type . ']" value="1"'
795                            . ' ' . $checked . '/>' . '<label for="'
796                            . $prefix . '_SET_' . $type . '">&nbsp;' . htmlspecialchars($translation) . '</label>';
797
798                        $hookSectionContent .= $this->templateService->substituteMarkerArray(
799                            $hookSectionTemplate,
800                            $hookSectionMarker,
801                            '###|###',
802                            true,
803                            true
804                        );
805                    }
806                }
807            }
808        }
809        $checkOptionsTemplate = $this->templateService->substituteSubpart(
810            $checkOptionsTemplate,
811            '###HOOK_SECTION###',
812            $hookSectionContent
813        );
814
815        // set this to signal that $prefix_SET variables should be used
816        $checkOptionsTemplate .= '<input type="hidden" name="' . $prefix . '_values" value="1">';
817
818        return $this->templateService->substituteMarkerArray($checkOptionsTemplate, $markerArray, '###|###', true, true);
819    }
820
821    /**
822     * Gets the buttons that shall be rendered in the docHeader
823     *
824     * @return array Available buttons for the docHeader
825     */
826    protected function getDocHeaderButtons()
827    {
828        return [
829            'csh' => BackendUtility::cshItem('_MOD_web_func', ''),
830            'shortcut' => $this->getShortcutButton(),
831            'save' => ''
832        ];
833    }
834
835    /**
836     * Gets the button to set a new shortcut in the backend (if current user is allowed to).
837     *
838     * @return string HTML representation of the shortcut button
839     */
840    protected function getShortcutButton()
841    {
842        $result = '';
843        if ($this->getBackendUser()->mayMakeShortcut()) {
844            $result = $this->doc->makeShortcutIcon('', 'function', 'web_info');
845        }
846        return $result;
847    }
848
849    /**
850     * Gets the filled markers that are used in the HTML template
851     * Reports tab
852     *
853     * @return array The filled marker array
854     */
855    protected function getTemplateMarkers()
856    {
857        $languageService = $this->getLanguageService();
858        return [
859            'FUNC_TITLE' => $languageService->getLL('report.func.title'),
860            'CHECKOPTIONS_TITLE' => $languageService->getLL('report.statistics.header'),
861            'FUNC_MENU' => $this->getLevelSelector('report'),
862            'CONTENT' => $this->content,
863            'CHECKOPTIONS' => $this->checkOptionsHtml['report'],
864            'ID' => '<input type="hidden" name="id" value="' . $this->id . '" />',
865            'REFRESH' => '<input type="submit" class="btn btn-default t3js-update-button" name="refreshLinkList" id="refreshLinkList" value="'
866                . htmlspecialchars($languageService->getLL('label_refresh'))
867                . '" data-notification-message="'
868                . htmlspecialchars($languageService->getLL('label_refresh-link-list')) . '" />',
869            'UPDATE' => '',
870        ];
871    }
872
873    /**
874     * Gets the filled markers that are used in the HTML template
875     * Check Links tab
876     *
877     * @return array The filled marker array
878     */
879    protected function getTemplateMarkersCheck()
880    {
881        $languageService = $this->getLanguageService();
882        return [
883            'FUNC_TITLE' => $languageService->getLL('checklinks.func.title'),
884            'CHECKOPTIONS_TITLE' => $languageService->getLL('checklinks.statistics.header'),
885            'FUNC_MENU' => $this->getLevelSelector('check'),
886            'CONTENT' => '',
887            'CHECKOPTIONS' => $this->checkOptionsHtml['check'],
888            'ID' => '<input type="hidden" name="id" value="' . $this->id . '" />',
889            'REFRESH' => '',
890            'UPDATE' => '<input type="submit" class="btn btn-default t3js-update-button" name="updateLinkList" id="updateLinkList" value="'
891                . htmlspecialchars($languageService->getLL('label_update'))
892                . '" data-notification-message="'
893                . htmlspecialchars($languageService->getLL('label_update-link-list'))
894                . '"/>',
895        ];
896    }
897
898    /**
899     * Determines whether the current user is an admin
900     *
901     * @return bool Whether the current user is admin
902     */
903    protected function isCurrentUserAdmin()
904    {
905        return $this->getBackendUser()->isAdmin();
906    }
907
908    /**
909     * Called from InfoModuleController until deprecation removal in TYPO3 v10.0
910     *
911     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
912     */
913    public function checkExtObj()
914    {
915        if (is_array($this->extClassConf) && $this->extClassConf['name']) {
916            $this->extObj = GeneralUtility::makeInstance($this->extClassConf['name']);
917            $this->extObj->init($this->pObj, $this->extClassConf);
918            // Re-write:
919            $this->pObj->MOD_SETTINGS = BackendUtility::getModuleData($this->pObj->MOD_MENU, GeneralUtility::_GP('SET'), 'web_info');
920        }
921    }
922
923    /**
924     * Calls the main function inside ANOTHER sub-submodule which might exist.
925     *
926     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0.
927     */
928    protected function extObjContent()
929    {
930        if (is_object($this->extObj)) {
931            return $this->extObj->main();
932        }
933    }
934
935    /**
936     * @return LanguageService
937     */
938    protected function getLanguageService(): LanguageService
939    {
940        return $GLOBALS['LANG'];
941    }
942
943    /**
944     * @return BackendUserAuthentication
945     */
946    protected function getBackendUser(): BackendUserAuthentication
947    {
948        return $GLOBALS['BE_USER'];
949    }
950
951    /**
952     * @return PageRenderer
953     */
954    protected function getPageRenderer(): PageRenderer
955    {
956        return GeneralUtility::makeInstance(PageRenderer::class);
957    }
958
959    /**
960     * @param string $theList
961     * @param string $permsClause
962     * @return string
963     */
964    protected function addPageTranslationsToPageList(string $theList, string $permsClause): string
965    {
966        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
967        $queryBuilder->getRestrictions()
968            ->removeAll()
969            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
970
971        $result = $queryBuilder
972            ->select('uid', 'title', 'hidden')
973            ->from('pages')
974            ->where(
975                $queryBuilder->expr()->eq(
976                    'l10n_parent',
977                    $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
978                ),
979                QueryHelper::stripLogicalOperatorPrefix($permsClause)
980            )
981            ->execute();
982
983        while ($row = $result->fetch()) {
984            if ($row['hidden'] === 0 || $this->modTS['checkhidden']) {
985                $theList .= ',' . $row['uid'];
986            }
987        }
988
989        return $theList;
990    }
991}
992