TYPO3  7.6
LinkValidatorReport.php
Go to the documentation of this file.
1 <?php
2 namespace 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 
27 
32 {
36  public $doc;
37 
43  protected $pageRecord = array();
44 
50  protected $isAccessibleForCurrentUser = false;
51 
57  protected $searchLevel;
58 
64  protected $linkAnalyzer;
65 
71  protected $modTS = array();
72 
78  protected $availableOptions = array();
79 
86  protected $checkOpt = array();
87 
94  protected $checkOptionsHtml;
95 
103 
109  protected $content;
110 
114  protected $hookObjectsArr = array();
115 
119  protected $updateListHtml = '';
120 
124  protected $refreshListHtml = '';
125 
129  protected $templateService;
130 
134  protected $iconFactory;
135 
141  public function main()
142  {
143  $this->getLanguageService()->includeLLFile('EXT:linkvalidator/Resources/Private/Language/Module/locallang.xlf');
144  $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
145  $this->searchLevel = GeneralUtility::_GP('search_levels');
146  if (isset($this->pObj->id)) {
147  $this->modTS = BackendUtility::getModTSconfig($this->pObj->id, 'mod.linkvalidator');
148  $this->modTS = $this->modTS['properties'];
149  }
150  $update = GeneralUtility::_GP('updateLinkList');
151  $prefix = '';
152  if (!empty($update)) {
153  $prefix = 'check';
154  }
155  $set = GeneralUtility::_GP($prefix . 'SET');
156  $this->pObj->handleExternalFunctionValue();
157  if (isset($this->searchLevel)) {
158  $this->pObj->MOD_SETTINGS['searchlevel'] = $this->searchLevel;
159  } else {
160  $this->searchLevel = $this->pObj->MOD_SETTINGS['searchlevel'];
161  }
162  if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])) {
163  foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] as $linkType => $value) {
164  // Compile list of all available types. Used for checking with button "Check Links".
165  if (strpos($this->modTS['linktypes'], $linkType) !== false) {
166  $this->availableOptions[$linkType] = 1;
167  }
168  // Compile list of types currently selected by the checkboxes
169  if ($this->pObj->MOD_SETTINGS[$linkType] && empty($set) || $set[$linkType]) {
170  $this->checkOpt[$linkType] = 1;
171  $this->pObj->MOD_SETTINGS[$linkType] = 1;
172  } else {
173  $this->pObj->MOD_SETTINGS[$linkType] = 0;
174  unset($this->checkOpt[$linkType]);
175  }
176  }
177  }
178  $this->getBackendUser()->pushModuleData('web_info', $this->pObj->MOD_SETTINGS);
179  $this->initialize();
180 
181  // Localization
182  $this->getPageRenderer()->addInlineLanguageLabelFile(
183  ExtensionManagementUtility::extPath('linkvalidator', 'Resources/Private/Language/Module/locallang.xlf')
184  );
185 
186  if ($this->modTS['showCheckLinkTab'] == 1) {
187  $this->updateListHtml = '<input class="btn btn-default" type="submit" name="updateLinkList" id="updateLinkList" value="' . $this->getLanguageService()->getLL('label_update') . '"/>';
188  }
189  $this->refreshListHtml = '<input class="btn btn-default" type="submit" name="refreshLinkList" id="refreshLinkList" value="' . $this->getLanguageService()->getLL('label_refresh') . '"/>';
190  $this->linkAnalyzer = GeneralUtility::makeInstance(LinkAnalyzer::class);
191  $this->updateBrokenLinks();
192 
193  $brokenLinkOverView = $this->linkAnalyzer->getLinkCounts($this->pObj->id);
194  $this->checkOptionsHtml = $this->getCheckOptions($brokenLinkOverView);
195  $this->checkOptionsHtmlCheck = $this->getCheckOptions($brokenLinkOverView, 'check');
196  $this->render();
197 
198  $pageTile = '';
199  if ($this->pObj->id) {
200  $pageRecord = BackendUtility::getRecord('pages', $this->pObj->id);
201  $pageTile = '<h1>' . htmlspecialchars(BackendUtility::getRecordTitle('pages', $pageRecord)) . '</h1>';
202  }
203 
204  return '<div id="linkvalidator-modfuncreport">' . $pageTile . $this->createTabs() . '</div>';
205  }
206 
212  protected function createTabs()
213  {
214  $menuItems = array(
215  0 => array(
216  'label' => $this->getLanguageService()->getLL('Report'),
217  'content' => $this->flush(true)
218  ),
219  );
220 
221  if ((bool)$this->modTS['showCheckLinkTab']) {
222  $menuItems[1] = array(
223  'label' => $this->getLanguageService()->getLL('CheckLink'),
224  'content' => $this->flush()
225  );
226  }
227 
228  // @todo: Use $this-moduleTemplate as soon as this class extends from AbstractModule
230  $moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
231  return $moduleTemplate->getDynamicTabMenu($menuItems, 'report-linkvalidator');
232  }
233 
239  protected function initialize()
240  {
241  if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])) {
242  foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] as $linkType => $classRef) {
243  $this->hookObjectsArr[$linkType] = GeneralUtility::getUserObj($classRef);
244  }
245  }
246 
247  $this->doc = GeneralUtility::makeInstance(DocumentTemplate::class);
248  $this->doc->setModuleTemplate('EXT:linkvalidator/Resources/Private/Templates/mod_template.html');
249 
250  $this->pageRecord = BackendUtility::readPageAccess($this->pObj->id, $this->getBackendUser()->getPagePermsClause(1));
251  if ($this->pObj->id && is_array($this->pageRecord) || !$this->pObj->id && $this->isCurrentUserAdmin()) {
252  $this->isAccessibleForCurrentUser = true;
253  }
254 
255  $this->doc->addStyleSheet('module', 'sysext/linkvalidator/Resources/Public/Css/styles.css');
256  $this->getPageRenderer()->loadJquery();
257  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Linkvalidator/Linkvalidator');
258 
259  $this->templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
260 
261  // Don't access in workspace
262  if ($this->getBackendUser()->workspace !== 0) {
263  $this->isAccessibleForCurrentUser = false;
264  }
265  }
266 
272  protected function updateBrokenLinks()
273  {
274  $searchFields = array();
275  // Get the searchFields from TypoScript
276  foreach ($this->modTS['searchFields.'] as $table => $fieldList) {
277  $fields = GeneralUtility::trimExplode(',', $fieldList, true);
278  foreach ($fields as $field) {
279  if (!$searchFields || !is_array($searchFields[$table]) || array_search($field, $searchFields[$table]) === false) {
280  $searchFields[$table][] = $field;
281  }
282  }
283  }
284  $rootLineHidden = $this->linkAnalyzer->getRootLineIsHidden($this->pObj->pageinfo);
285  if (!$rootLineHidden || $this->modTS['checkhidden'] == 1) {
286  // Get children pages
287  $pageList = $this->linkAnalyzer->extGetTreeList(
288  $this->pObj->id,
289  $this->searchLevel,
290  0,
291  $this->getBackendUser()->getPagePermsClause(1),
292  $this->modTS['checkhidden']
293  );
294  if ($this->pObj->pageinfo['hidden'] == 0 || $this->modTS['checkhidden']) {
295  $pageList .= $this->pObj->id;
296  }
297 
298  $this->linkAnalyzer->init($searchFields, $pageList, $this->modTS);
299 
300  // Check if button press
301  $update = GeneralUtility::_GP('updateLinkList');
302  if (!empty($update)) {
303  $this->linkAnalyzer->getLinkStatistics($this->checkOpt, $this->modTS['checkhidden']);
304  }
305  }
306  }
307 
313  protected function render()
314  {
315  if ($this->isAccessibleForCurrentUser) {
316  $this->content = $this->renderBrokenLinksTable();
317  } else {
318  // If no access or if ID == zero
320  $message = GeneralUtility::makeInstance(
321  FlashMessage::class,
322  $this->getLanguageService()->getLL('no.access'),
323  $this->getLanguageService()->getLL('no.access.title'),
325  );
326  $this->content .= $message->render();
327  }
328  }
329 
336  protected function flush($form = false)
337  {
338  return $this->doc->moduleBody(
339  $this->pageRecord,
340  $this->getDocHeaderButtons(),
341  $form ? $this->getTemplateMarkers() : $this->getTemplateMarkersCheck()
342  );
343  }
344 
350  protected function getLevelSelector()
351  {
352  // Build level selector
353  $options = array();
354  $availableOptions = array(
355  0 => $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.depth_0'),
356  1 => $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.depth_1'),
357  2 => $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.depth_2'),
358  3 => $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.depth_3'),
359  4 => $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.depth_4'),
360  999 => $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.depth_infi')
361  );
362  foreach ($availableOptions as $optionValue => $optionLabel) {
363  $options[] = '<option value="' . $optionValue . '"' . ($optionValue === (int)$this->searchLevel ? ' selected="selected"' : '') . '>' . htmlspecialchars($optionLabel) . '</option>';
364  }
365  return '<select name="search_levels" class="form-control">' . implode('', $options) . '</select>';
366  }
367 
373  protected function renderBrokenLinksTable()
374  {
375  $brokenLinkItems = '';
376  $brokenLinksTemplate = $this->templateService->getSubpart($this->doc->moduleTemplate, '###NOBROKENLINKS_CONTENT###');
377  $keyOpt = array();
378  if (is_array($this->checkOpt)) {
379  $keyOpt = array_keys($this->checkOpt);
380  }
381 
382  // Table header
383  $brokenLinksMarker = $this->startTable();
384 
385  $rootLineHidden = $this->linkAnalyzer->getRootLineIsHidden($this->pObj->pageinfo);
386  if (!$rootLineHidden || (bool)$this->modTS['checkhidden']) {
387  $pageList = $this->linkAnalyzer->extGetTreeList(
388  $this->pObj->id,
389  $this->searchLevel,
390  0,
391  $this->getBackendUser()->getPagePermsClause(1),
392  $this->modTS['checkhidden']
393  );
394  // Always add the current page, because we are just displaying the results
395  $pageList .= $this->pObj->id;
396 
397  $records = $this->getDatabaseConnection()->exec_SELECTgetRows(
398  '*',
399  'tx_linkvalidator_link',
400  'record_pid IN (' . $pageList . ') AND link_type IN (\'' . implode('\',\'', $keyOpt) . '\')',
401  '',
402  'record_uid ASC, uid ASC'
403  );
404  if (!empty($records)) {
405  // Display table with broken links
406  $brokenLinksTemplate = $this->templateService->getSubpart($this->doc->moduleTemplate, '###BROKENLINKS_CONTENT###');
407  $brokenLinksItemTemplate = $this->templateService->getSubpart($this->doc->moduleTemplate, '###BROKENLINKS_ITEM###');
408 
409  // Table rows containing the broken links
410  $items = array();
411  foreach ($records as $record) {
412  $items[] = $this->renderTableRow($record['table_name'], $record, $brokenLinksItemTemplate);
413  }
414  $brokenLinkItems = implode(LF, $items);
415  } else {
416  $brokenLinksMarker = $this->getNoBrokenLinkMessage($brokenLinksMarker);
417  }
418  } else {
419  $brokenLinksMarker = $this->getNoBrokenLinkMessage($brokenLinksMarker);
420  }
421  $brokenLinksTemplate = $this->templateService->substituteMarkerArray(
422  $brokenLinksTemplate,
423  $brokenLinksMarker, '###|###',
424  true
425  );
426  return $this->templateService->substituteSubpart($brokenLinksTemplate, '###BROKENLINKS_ITEM', $brokenLinkItems);
427  }
428 
435  protected function getNoBrokenLinkMessage(array $brokenLinksMarker)
436  {
437  $brokenLinksMarker['LIST_HEADER'] = '<h3>' . $this->getLanguageService()->getLL('list.header', true) . '</h3>';
439  $message = GeneralUtility::makeInstance(
440  FlashMessage::class,
441  $this->getLanguageService()->getLL('list.no.broken.links'),
442  $this->getLanguageService()->getLL('list.no.broken.links.title'),
443  FlashMessage::OK
444  );
445  $brokenLinksMarker['NO_BROKEN_LINKS'] = $message->render();
446  return $brokenLinksMarker;
447  }
448 
454  protected function startTable()
455  {
456  // Listing head
457  $makerTableHead = array(
458  'tablehead_path' => $this->getLanguageService()->getLL('list.tableHead.path'),
459  'tablehead_element' => $this->getLanguageService()->getLL('list.tableHead.element'),
460  'tablehead_headlink' => $this->getLanguageService()->getLL('list.tableHead.headlink'),
461  'tablehead_linktarget' => $this->getLanguageService()->getLL('list.tableHead.linktarget'),
462  'tablehead_linkmessage' => $this->getLanguageService()->getLL('list.tableHead.linkmessage'),
463  'tablehead_lastcheck' => $this->getLanguageService()->getLL('list.tableHead.lastCheck'),
464  );
465 
466  // Add CSH to the header of each column
467  foreach ($makerTableHead as $column => $label) {
468  $makerTableHead[$column] = BackendUtility::wrapInHelp('linkvalidator', $column, $label);
469  }
470  // Add section header
471  $makerTableHead['list_header'] = '<h3>' . $this->getLanguageService()->getLL('list.header', true) . '</h3>';
472  return $makerTableHead;
473  }
474 
483  protected function renderTableRow($table, array $row, $brokenLinksItemTemplate)
484  {
485  $markerArray = array();
486  $fieldName = '';
487  // Restore the linktype object
488  $hookObj = $this->hookObjectsArr[$row['link_type']];
489 
490  // Construct link to edit the content element
491  $requestUri = GeneralUtility::getIndpEnv('REQUEST_URI') .
492  '&id=' . $this->pObj->id .
493  '&search_levels=' . $this->searchLevel;
494  $url = BackendUtility::getModuleUrl('record_edit', [
495  'edit' => [
496  $table => [
497  $row['record_uid'] => 'edit'
498  ]
499  ],
500  'returnUrl' => $requestUri
501  ]);
502  $actionLink = '<a href="' . htmlspecialchars($url);
503  $actionLink .= '" title="' . $this->getLanguageService()->getLL('list.edit') . '">';
504  $actionLink .= $this->iconFactory->getIcon('actions-document-open', Icon::SIZE_SMALL)->render();
505  $actionLink .= '</a>';
506  $elementHeadline = $row['headline'];
507  if (empty($elementHeadline)) {
508  $elementHeadline = '<i>' . $this->getLanguageService()->getLL('list.no.headline') . '</i>';
509  }
510  // Get the language label for the field from TCA
511  if ($GLOBALS['TCA'][$table]['columns'][$row['field']]['label']) {
512  $fieldName = $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['columns'][$row['field']]['label']);
513  // Crop colon from end if present
514  if (substr($fieldName, '-1', '1') === ':') {
515  $fieldName = substr($fieldName, '0', strlen($fieldName) - 1);
516  }
517  }
518  // Fallback, if there is no label
519  $fieldName = !empty($fieldName) ? $fieldName : $row['field'];
520  // column "Element"
521  $element = '<span title="' . htmlspecialchars($table . ':' . $row['record_uid']) . '">' . $this->iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>';
522  $element .= $elementHeadline;
523  $element .= ' ' . sprintf($this->getLanguageService()->getLL('list.field'), $fieldName);
524  $markerArray['actionlink'] = $actionLink;
525  $markerArray['path'] = BackendUtility::getRecordPath($row['record_pid'], '', 0, 0);
526  $markerArray['element'] = $element;
527  $markerArray['headlink'] = $row['link_title'];
528  $markerArray['linktarget'] = $hookObj->getBrokenUrl($row);
529  $response = unserialize($row['url_response']);
530  if ($response['valid']) {
531  $linkMessage = '<span class="valid">' . $this->getLanguageService()->getLL('list.msg.ok') . '</span>';
532  } else {
533  $linkMessage = '<span class="error">' . $hookObj->getErrorMessage($response['errorParams']) . '</span>';
534  }
535  $markerArray['linkmessage'] = $linkMessage;
536 
537  $lastRunDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $row['last_check']);
538  $lastRunTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $row['last_check']);
539  $markerArray['lastcheck'] = sprintf($this->getLanguageService()->getLL('list.msg.lastRun'), $lastRunDate, $lastRunTime);
540 
541  // Return the table html code as string
542  return $this->templateService->substituteMarkerArray($brokenLinksItemTemplate, $markerArray, '###|###', true, true);
543  }
544 
552  protected function getCheckOptions(array $brokenLinkOverView, $prefix = '')
553  {
554  $markerArray = array();
555  if (!empty($prefix)) {
556  $additionalAttr = ' class="' . $prefix . '"';
557  } else {
558  $additionalAttr = ' class="refresh"';
559  }
560  $checkOptionsTemplate = $this->templateService->getSubpart($this->doc->moduleTemplate, '###CHECKOPTIONS_SECTION###');
561  $hookSectionTemplate = $this->templateService->getSubpart($checkOptionsTemplate, '###HOOK_SECTION###');
562  $markerArray['statistics_header'] = '<h3>' . $this->getLanguageService()->getLL('report.statistics.header', true) . '</h3>';
563  $markerArray['total_count_label'] = BackendUtility::wrapInHelp('linkvalidator', 'checkboxes', $this->getLanguageService()->getLL('overviews.nbtotal'));
564  $markerArray['total_count'] = $brokenLinkOverView['brokenlinkCount'] ?: '0';
565 
566  $linktypes = GeneralUtility::trimExplode(',', $this->modTS['linktypes'], true);
567  $hookSectionContent = '';
568  if (is_array($linktypes)) {
569  if (
570  !empty($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])
571  && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])
572  ) {
573  foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] as $type => $value) {
574  if (in_array($type, $linktypes)) {
575  $hookSectionMarker = array(
576  'count' => $brokenLinkOverView[$type] ?: '0',
577  );
578 
579  $translation = $this->getLanguageService()->getLL('hooks.' . $type) ?: $type;
580  $hookSectionMarker['option'] = '<input type="checkbox"' . $additionalAttr . ' id="' . $prefix . 'SET_' . $type . '" name="' . $prefix
581  . 'SET[' . $type . ']" value="1"' . ($this->pObj->MOD_SETTINGS[$type] ? ' checked="checked"' : '') . '/>' . '<label for="'
582  . $prefix . 'SET_' . $type . '">&nbsp;' . htmlspecialchars($translation) . '</label>';
583 
584  $hookSectionContent .= $this->templateService->substituteMarkerArray(
585  $hookSectionTemplate,
586  $hookSectionMarker, '###|###',
587  true,
588  true
589  );
590  }
591  }
592  }
593  }
594  $checkOptionsTemplate = $this->templateService->substituteSubpart(
595  $checkOptionsTemplate,
596  '###HOOK_SECTION###',
597  $hookSectionContent
598  );
599  return $this->templateService->substituteMarkerArray($checkOptionsTemplate, $markerArray, '###|###', true, true);
600  }
601 
607  protected function getDocHeaderButtons()
608  {
609  return array(
610  'csh' => BackendUtility::cshItem('_MOD_web_func', ''),
611  'shortcut' => $this->getShortcutButton(),
612  'save' => ''
613  );
614  }
615 
621  protected function getShortcutButton()
622  {
623  $result = '';
624  if ($this->getBackendUser()->mayMakeShortcut()) {
625  $result = $this->doc->makeShortcutIcon('', 'function', $this->pObj->MCONF['name']);
626  }
627  return $result;
628  }
629 
635  protected function getTemplateMarkers()
636  {
637  return array(
638  'FUNC_TITLE' => $this->getLanguageService()->getLL('report.func.title'),
639  'CHECKOPTIONS_TITLE' => $this->getLanguageService()->getLL('report.statistics.header'),
640  'FUNC_MENU' => $this->getLevelSelector(),
641  'CONTENT' => $this->content,
642  'CHECKOPTIONS' => $this->checkOptionsHtml,
643  'ID' => '<input type="hidden" name="id" value="' . $this->pObj->id . '" />',
644  'REFRESH' => '<input type="submit" class="btn btn-default" name="refreshLinkList" id="refreshLinkList" value="' . $this->getLanguageService()->getLL('label_refresh') . '" />',
645  'UPDATE' => '',
646  );
647  }
648 
654  protected function getTemplateMarkersCheck()
655  {
656  return array(
657  'FUNC_TITLE' => $this->getLanguageService()->getLL('checklinks.func.title'),
658  'CHECKOPTIONS_TITLE' => $this->getLanguageService()->getLL('checklinks.statistics.header'),
659  'FUNC_MENU' => $this->getLevelSelector(),
660  'CONTENT' => '',
661  'CHECKOPTIONS' => $this->checkOptionsHtmlCheck,
662  'ID' => '<input type="hidden" name="id" value="' . $this->pObj->id . '" />',
663  'REFRESH' => '',
664  'UPDATE' => '<input type="submit" class="btn btn-default" name="updateLinkList" id="updateLinkList" value="' . $this->getLanguageService()->getLL('label_update') . '"/>',
665  );
666  }
667 
673  protected function isCurrentUserAdmin()
674  {
675  return $this->getBackendUser()->isAdmin();
676  }
677 
683  protected function getLanguageService()
684  {
685  return $GLOBALS['LANG'];
686  }
687 
693  protected function getBackendUser()
694  {
695  return $GLOBALS['BE_USER'];
696  }
697 
701  protected function getDatabaseConnection()
702  {
703  return $GLOBALS['TYPO3_DB'];
704  }
705 }