TYPO3 CMS  TYPO3_8-7
LinkAnalyzer.php
Go to the documentation of this file.
1 <?php
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 
25 
30 {
31 
37  protected $searchFields = [];
38 
44  protected $pids = [];
45 
51  protected $linkCounts = [];
52 
58  protected $brokenLinkCounts = [];
59 
65  protected $recordsWithBrokenLinks = [];
66 
72  protected $hookObjectsArr = [];
73 
79  protected $extPageInTreeInfo = [];
80 
86  protected $recordReference = '';
87 
93  protected $pageWithAnchor = '';
94 
100  protected $tsConfig = [];
101 
105  public function __construct()
106  {
107  $this->getLanguageService()->includeLLFile('EXT:linkvalidator/Resources/Private/Language/Module/locallang.xlf');
108  // Hook to handle own checks
109  if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'])) {
110  foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] as $key => $classRef) {
111  $this->hookObjectsArr[$key] = GeneralUtility::getUserObj($classRef);
112  }
113  }
114  }
115 
123  public function init(array $searchField, $pidList, $tsConfig)
124  {
125  $this->searchFields = $searchField;
126  $this->pids = GeneralUtility::intExplode(',', $pidList, true);
127  $this->tsConfig = $tsConfig;
128  }
129 
136  public function getLinkStatistics($checkOptions = [], $considerHidden = false)
137  {
138  $results = [];
139  if (!empty($checkOptions) && !empty($this->pids)) {
140  $checkKeys = array_keys($checkOptions);
141 
142  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
143  ->getQueryBuilderForTable('tx_linkvalidator_link');
144 
145  $queryBuilder->delete('tx_linkvalidator_link')
146  ->where(
147  $queryBuilder->expr()->orX(
148  $queryBuilder->expr()->in(
149  'record_pid',
150  $queryBuilder->createNamedParameter($this->pids, Connection::PARAM_INT_ARRAY)
151  ),
152  $queryBuilder->expr()->andX(
153  $queryBuilder->expr()->in(
154  'record_uid',
155  $queryBuilder->createNamedParameter($this->pids, Connection::PARAM_INT_ARRAY)
156  ),
157  $queryBuilder->expr()->eq(
158  'table_name',
159  $queryBuilder->createNamedParameter('pages', \PDO::PARAM_STR)
160  )
161  )
162  ),
163  $queryBuilder->expr()->in(
164  'link_type',
165  $queryBuilder->createNamedParameter($checkKeys, Connection::PARAM_STR_ARRAY)
166  )
167  )
168  ->execute();
169 
170  // Traverse all configured tables
171  foreach ($this->searchFields as $table => $fields) {
172  // If table is not configured, assume the extension is not installed
173  // and therefore no need to check it
174  if (!is_array($GLOBALS['TCA'][$table])) {
175  continue;
176  }
177  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
178  ->getQueryBuilderForTable($table);
179 
180  if ($considerHidden) {
181  $queryBuilder->getRestrictions()
182  ->removeAll()
183  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
184  }
185 
186  // Re-init selectFields for table
187  $selectFields = array_merge(['uid', 'pid', $GLOBALS['TCA'][$table]['ctrl']['label']], $fields);
188 
189  $result = $queryBuilder->select(...$selectFields)
190  ->from($table)
191  ->where(
192  $queryBuilder->expr()->in(
193  ($table === 'pages' ? 'uid' : 'pid'),
194  $queryBuilder->createNamedParameter($this->pids, Connection::PARAM_INT_ARRAY)
195  )
196  )
197  ->execute();
198 
199  // @todo #64091: only select rows that have content in at least one of the relevant fields (via OR)
200  while ($row = $result->fetch()) {
201  $this->analyzeRecord($results, $table, $fields, $row);
202  }
203  }
204 
205  foreach ($this->hookObjectsArr as $key => $hookObj) {
206  if (is_array($results[$key]) && empty($checkOptions) || is_array($results[$key]) && $checkOptions[$key]) {
207  // Check them
208  foreach ($results[$key] as $entryKey => $entryValue) {
209  $table = $entryValue['table'];
210  $record = [];
211  $record['headline'] = BackendUtility::getRecordTitle($table, $entryValue['row']);
212  $record['record_pid'] = $entryValue['row']['pid'];
213  $record['record_uid'] = $entryValue['uid'];
214  $record['table_name'] = $table;
215  $record['link_title'] = $entryValue['link_title'];
216  $record['field'] = $entryValue['field'];
217  $record['last_check'] = time();
218  $this->recordReference = $entryValue['substr']['recordRef'];
219  $this->pageWithAnchor = $entryValue['pageAndAnchor'];
220  if (!empty($this->pageWithAnchor)) {
221  // Page with anchor, e.g. 18#1580
222  $url = $this->pageWithAnchor;
223  } else {
224  $url = $entryValue['substr']['tokenValue'];
225  }
226  $this->linkCounts[$table]++;
227  $checkUrl = $hookObj->checkLink($url, $entryValue, $this);
228  // Broken link found
229  if (!$checkUrl) {
230  $response = [];
231  $response['valid'] = false;
232  $response['errorParams'] = $hookObj->getErrorParams();
233  $this->brokenLinkCounts[$table]++;
234  $record['link_type'] = $key;
235  $record['url'] = $url;
236  $record['url_response'] = serialize($response);
237  GeneralUtility::makeInstance(ConnectionPool::class)
238  ->getConnectionForTable('tx_linkvalidator_link')
239  ->insert('tx_linkvalidator_link', $record);
240  } elseif (GeneralUtility::_GP('showalllinks')) {
241  $response = [];
242  $response['valid'] = true;
243  $this->brokenLinkCounts[$table]++;
244  $record['url'] = $url;
245  $record['link_type'] = $key;
246  $record['url_response'] = serialize($response);
247  GeneralUtility::makeInstance(ConnectionPool::class)
248  ->getConnectionForTable('tx_linkvalidator_link')
249  ->insert('tx_linkvalidator_link', $record);
250  }
251  }
252  }
253  }
254  }
255  }
256 
265  public function analyzeRecord(array &$results, $table, array $fields, array $record)
266  {
267  list($results, $record) = $this->emitBeforeAnalyzeRecordSignal($results, $record, $table, $fields);
268 
269  // Put together content of all relevant fields
270  $haystack = '';
272  $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
273  $idRecord = $record['uid'];
274  // Get all references
275  foreach ($fields as $field) {
276  $haystack .= $record[$field] . ' --- ';
277  $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
278  $valueField = $record[$field];
279  // Check if a TCA configured field has soft references defined (see TYPO3 Core API document)
280  if ($conf['softref'] && (string)$valueField !== '') {
281  // Explode the list of soft references/parameters
282  $softRefs = BackendUtility::explodeSoftRefParserList($conf['softref']);
283  if ($softRefs !== false) {
284  // Traverse soft references
285  foreach ($softRefs as $spKey => $spParams) {
287  $softRefObj = BackendUtility::softRefParserObj($spKey);
288  // If there is an object returned...
289  if (!is_object($softRefObj)) {
290  continue;
291  }
292  $softRefParams = $spParams;
293  if (!is_array($softRefParams)) {
294  // set subst such that findRef will return substitutes for urls, emails etc
295  $softRefParams = ['subst' => true];
296  }
297 
298  $resultArray = $softRefObj->findRef($table, $field, $idRecord, $valueField, $spKey, $softRefParams);
299  if (empty($resultArray['elements'])) {
300  continue;
301  }
302 
303  if ($spKey === 'typolink_tag') {
304  $this->analyseTypoLinks($resultArray, $results, $htmlParser, $record, $field, $table);
305  } else {
306  $this->analyseLinks($resultArray, $results, $record, $field, $table);
307  }
308  }
309  }
310  }
311  }
312  }
313 
322  public function getTSConfig()
323  {
324  return $this->tsConfig;
325  }
326 
336  protected function analyseLinks(array $resultArray, array &$results, array $record, $field, $table)
337  {
338  foreach ($resultArray['elements'] as $element) {
339  $r = $element['subst'];
340  $type = '';
341  $idRecord = $record['uid'];
342  if (!empty($r)) {
344  foreach ($this->hookObjectsArr as $keyArr => $hookObj) {
345  $type = $hookObj->fetchType($r, $type, $keyArr);
346  // Store the type that was found
347  // This prevents overriding by internal validator
348  if (!empty($type)) {
349  $r['type'] = $type;
350  }
351  }
352  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['substr'] = $r;
353  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['row'] = $record;
354  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['table'] = $table;
355  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['field'] = $field;
356  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['uid'] = $idRecord;
357  }
358  }
359  }
360 
371  protected function analyseTypoLinks(array $resultArray, array &$results, $htmlParser, array $record, $field, $table)
372  {
373  $currentR = [];
374  $linkTags = $htmlParser->splitIntoBlock('a,link', $resultArray['content']);
375  $idRecord = $record['uid'];
376  $type = '';
377  $title = '';
378  $countLinkTags = count($linkTags);
379  for ($i = 1; $i < $countLinkTags; $i += 2) {
380  $referencedRecordType = '';
381  foreach ($resultArray['elements'] as $element) {
382  $type = '';
383  $r = $element['subst'];
384  if (!empty($r['tokenID'])) {
385  if (substr_count($linkTags[$i], $r['tokenID'])) {
386  // Type of referenced record
387  if (strpos($r['recordRef'], 'pages') !== false) {
388  $currentR = $r;
389  // Contains number of the page
390  $referencedRecordType = $r['tokenValue'];
391  $wasPage = true;
392  } elseif (strpos($r['recordRef'], 'tt_content') !== false && (isset($wasPage) && $wasPage === true)) {
393  $referencedRecordType = $referencedRecordType . '#c' . $r['tokenValue'];
394  $wasPage = false;
395  } else {
396  $currentR = $r;
397  }
398  $title = strip_tags($linkTags[$i]);
399  }
400  }
401  }
403  foreach ($this->hookObjectsArr as $keyArr => $hookObj) {
404  $type = $hookObj->fetchType($currentR, $type, $keyArr);
405  // Store the type that was found
406  // This prevents overriding by internal validator
407  if (!empty($type)) {
408  $currentR['type'] = $type;
409  }
410  }
411  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['substr'] = $currentR;
412  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['row'] = $record;
413  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['table'] = $table;
414  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['field'] = $field;
415  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['uid'] = $idRecord;
416  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['link_title'] = $title;
417  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['pageAndAnchor'] = $referencedRecordType;
418  }
419  }
420 
427  public function getLinkCounts($curPage)
428  {
429  $markerArray = [];
430 
431  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
432  ->getQueryBuilderForTable('tx_linkvalidator_link');
433  $queryBuilder->getRestrictions()->removeAll();
434 
435  $result = $queryBuilder->select('link_type')
436  ->addSelectLiteral($queryBuilder->expr()->count('uid', 'nbBrokenLinks'))
437  ->from('tx_linkvalidator_link')
438  ->where(
439  $queryBuilder->expr()->in(
440  'record_pid',
441  $queryBuilder->createNamedParameter($this->pids, Connection::PARAM_INT_ARRAY)
442  )
443  )
444  ->groupBy('link_type')
445  ->execute();
446 
447  while ($row = $result->fetch()) {
448  $markerArray[$row['link_type']] = $row['nbBrokenLinks'];
449  $markerArray['brokenlinkCount'] += $row['nbBrokenLinks'];
450  }
451  return $markerArray;
452  }
453 
469  public function extGetTreeList($id, $depth, $begin = 0, $permsClause, $considerHidden = false)
470  {
471  $depth = (int)$depth;
472  $begin = (int)$begin;
473  $id = (int)$id;
474  $theList = '';
475  if ($depth > 0) {
476  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
477  $queryBuilder->getRestrictions()
478  ->removeAll()
479  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
480 
481  $result = $queryBuilder
482  ->select('uid', 'title', 'hidden', 'extendToSubpages')
483  ->from('pages')
484  ->where(
485  $queryBuilder->expr()->eq(
486  'pid',
487  $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
488  ),
490  )
491  ->execute();
492 
493  while ($row = $result->fetch()) {
494  if ($begin <= 0 && ($row['hidden'] == 0 || $considerHidden)) {
495  $theList .= $row['uid'] . ',';
496  $this->extPageInTreeInfo[] = [$row['uid'], htmlspecialchars($row['title'], $depth)];
497  }
498  if ($depth > 1 && (!($row['hidden'] == 1 && $row['extendToSubpages'] == 1) || $considerHidden)) {
499  $theList .= $this->extGetTreeList(
500  $row['uid'],
501  $depth - 1,
502  $begin - 1,
503  $permsClause,
504  $considerHidden
505  );
506  }
507  }
508  }
509  return $theList;
510  }
511 
518  public function getRootLineIsHidden(array $pageInfo)
519  {
520  $hidden = false;
521  if ($pageInfo['extendToSubpages'] == 1 && $pageInfo['hidden'] == 1) {
522  $hidden = true;
523  } else {
524  if ($pageInfo['pid'] > 0) {
525  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
526  $queryBuilder->getRestrictions()->removeAll();
527 
528  $row = $queryBuilder
529  ->select('uid', 'title', 'hidden', 'extendToSubpages')
530  ->from('pages')
531  ->where(
532  $queryBuilder->expr()->eq(
533  'uid',
534  $queryBuilder->createNamedParameter($pageInfo['pid'], \PDO::PARAM_INT)
535  )
536  )
537  ->execute()
538  ->fetch();
539 
540  if ($row !== false) {
541  $hidden = $this->getRootLineIsHidden($row);
542  }
543  }
544  }
545 
546  return $hidden;
547  }
548 
558  protected function emitBeforeAnalyzeRecordSignal($results, $record, $table, $fields)
559  {
560  return $this->getSignalSlotDispatcher()->dispatch(
561  self::class,
562  'beforeAnalyzeRecord',
563  [$results, $record, $table, $fields, $this]
564  );
565  }
566 
570  protected function getSignalSlotDispatcher()
571  {
572  return $this->getObjectManager()->get(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class);
573  }
574 
578  protected function getObjectManager()
579  {
580  return GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
581  }
582 
586  protected function getLanguageService()
587  {
588  return $GLOBALS['LANG'];
589  }
590 }
static intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
static makeInstance($className,... $constructorArguments)
$fields
Definition: pages.php:4
static getRecordTitle($table, $row, $prep=false, $forceResult=true)
static stripLogicalOperatorPrefix(string $constraint)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']