‪TYPO3CMS  9.5
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 
31 {
32 
38  protected ‪$searchFields = [];
39 
45  protected ‪$pids = [];
46 
52  protected ‪$linkCounts = [];
53 
59  protected ‪$brokenLinkCounts = [];
60 
66  protected ‪$recordsWithBrokenLinks = [];
67 
73  protected ‪$hookObjectsArr = [];
74 
80  protected ‪$recordReference = '';
81 
87  protected ‪$pageWithAnchor = '';
88 
94  protected ‪$tsConfig = [];
95 
99  public function ‪__construct()
100  {
101  $this->‪getLanguageService()->‪includeLLFile('EXT:linkvalidator/Resources/Private/Language/Module/locallang.xlf');
102  // Hook to handle own checks
103  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['linkvalidator']['checkLinks'] ?? [] as $key => $className) {
104  $this->hookObjectsArr[$key] = GeneralUtility::makeInstance($className);
105  }
106  }
107 
115  public function ‪init(array $searchField, $pidList, ‪$tsConfig)
116  {
117  $this->searchFields = $searchField;
118  $this->pids = GeneralUtility::intExplode(',', $pidList, true);
119  $this->tsConfig = ‪$tsConfig;
120  }
121 
128  public function ‪getLinkStatistics($checkOptions = [], $considerHidden = false)
129  {
130  $results = [];
131  if (empty($checkOptions) || empty($this->pids)) {
132  return;
133  }
134 
135  $checkKeys = array_keys($checkOptions);
136 
137  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
138  ->getQueryBuilderForTable('tx_linkvalidator_link');
139 
140  $queryBuilder->delete('tx_linkvalidator_link')
141  ->where(
142  $queryBuilder->expr()->orX(
143  $queryBuilder->expr()->andX(
144  $queryBuilder->expr()->in(
145  'record_uid',
146  $queryBuilder->createNamedParameter($this->pids, Connection::PARAM_INT_ARRAY)
147  ),
148  $queryBuilder->expr()->eq('table_name', $queryBuilder->createNamedParameter('pages'))
149  ),
150  $queryBuilder->expr()->andX(
151  $queryBuilder->expr()->in(
152  'record_pid',
153  $queryBuilder->createNamedParameter($this->pids, Connection::PARAM_INT_ARRAY)
154  ),
155  $queryBuilder->expr()->neq(
156  'table_name',
157  $queryBuilder->createNamedParameter('pages')
158  )
159  )
160  ),
161  $queryBuilder->expr()->in(
162  'link_type',
163  $queryBuilder->createNamedParameter($checkKeys, Connection::PARAM_STR_ARRAY)
164  )
165  )
166  ->execute();
167 
168  // Traverse all configured tables
169  foreach ($this->searchFields as $table => ‪$fields) {
170  // If table is not configured, assume the extension is not installed
171  // and therefore no need to check it
172  if (!is_array(‪$GLOBALS['TCA'][$table])) {
173  continue;
174  }
175  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
176  ->getQueryBuilderForTable($table);
177 
178  if ($considerHidden) {
179  $queryBuilder->getRestrictions()
180  ->removeAll()
181  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
182  }
183 
184  // Re-init selectFields for table
185  $selectFields = array_merge(['uid', 'pid', ‪$GLOBALS['TCA'][$table]['ctrl']['label']], ‪$fields);
186 
187  $result = $queryBuilder->select(...$selectFields)
188  ->from($table)
189  ->where(
190  $queryBuilder->expr()->in(
191  ($table === 'pages' ? 'uid' : 'pid'),
192  $queryBuilder->createNamedParameter($this->pids, Connection::PARAM_INT_ARRAY)
193  )
194  )
195  ->execute();
196 
197  // @todo #64091: only select rows that have content in at least one of the relevant fields (via OR)
198  while ($row = $result->fetch()) {
199  $this->‪analyzeRecord($results, $table, ‪$fields, $row);
200  }
201  }
202 
203  foreach ($this->hookObjectsArr as $key => $hookObj) {
204  if (!is_array($results[$key]) || (!empty($checkOptions) && !$checkOptions[$key])) {
205  continue;
206  }
207 
208  // Check them
209  foreach ($results[$key] as $entryKey => $entryValue) {
210  $table = $entryValue['table'];
211  $record = [];
212  $record['headline'] = ‪BackendUtility::getRecordTitle($table, $entryValue['row']);
213  $record['record_pid'] = $entryValue['row']['pid'];
214  $record['record_uid'] = $entryValue['uid'];
215  $record['table_name'] = $table;
216  $record['link_title'] = $entryValue['link_title'];
217  $record['field'] = $entryValue['field'];
218  $record['last_check'] = time();
219  $this->recordReference = $entryValue['substr']['recordRef'];
220  $this->pageWithAnchor = $entryValue['pageAndAnchor'];
221  if (!empty($this->pageWithAnchor)) {
222  // Page with anchor, e.g. 18#1580
224  } else {
225  $url = $entryValue['substr']['tokenValue'];
226  }
227  $this->linkCounts[$table]++;
228  $checkUrl = $hookObj->checkLink($url, $entryValue, $this);
229 
230  // Broken link found
231  if (!$checkUrl) {
232  $response = [];
233  $response['valid'] = false;
234  $response['errorParams'] = $hookObj->getErrorParams();
235  $this->brokenLinkCounts[$table]++;
236  $record['link_type'] = $key;
237  $record['url'] = $url;
238  $record['url_response'] = serialize($response);
239  GeneralUtility::makeInstance(ConnectionPool::class)
240  ->getConnectionForTable('tx_linkvalidator_link')
241  ->insert('tx_linkvalidator_link', $record);
242  } elseif (GeneralUtility::_GP('showalllinks')) {
243  $response = [];
244  $response['valid'] = true;
245  $this->brokenLinkCounts[$table]++;
246  $record['url'] = $url;
247  $record['link_type'] = $key;
248  $record['url_response'] = serialize($response);
249  GeneralUtility::makeInstance(ConnectionPool::class)
250  ->getConnectionForTable('tx_linkvalidator_link')
251  ->insert('tx_linkvalidator_link', $record);
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 
280  // Check if a TCA configured field has soft references defined (see TYPO3 Core API document)
281  if (!$conf['softref'] || (string)$valueField === '') {
282  continue;
283  }
284 
285  // Explode the list of soft references/parameters
286  $softRefs = ‪BackendUtility::explodeSoftRefParserList($conf['softref']);
287  if ($softRefs === false) {
288  continue;
289  }
290 
291  // Traverse soft references
292  foreach ($softRefs as $spKey => $spParams) {
294  $softRefObj = ‪BackendUtility::softRefParserObj($spKey);
295 
296  // If there is an object returned...
297  if (!is_object($softRefObj)) {
298  continue;
299  }
300  $softRefParams = $spParams;
301  if (!is_array($softRefParams)) {
302  // set subst such that findRef will return substitutes for urls, emails etc
303  $softRefParams = ['subst' => true];
304  }
305 
306  // Do processing
307  $resultArray = $softRefObj->findRef($table, $field, $idRecord, $valueField, $spKey, $softRefParams);
308  if (empty($resultArray['elements'])) {
309  continue;
310  }
311 
312  if ($spKey === 'typolink_tag') {
313  $this->‪analyzeTypoLinks($resultArray, $results, $htmlParser, $record, $field, $table);
314  } else {
315  $this->‪analyzeLinks($resultArray, $results, $record, $field, $table);
316  }
317  }
318  }
319  }
320 
329  public function ‪getTSConfig()
330  {
331  return ‪$this->tsConfig;
332  }
333 
343  protected function ‪analyzeLinks(array $resultArray, array &$results, array $record, $field, $table)
344  {
345  foreach ($resultArray['elements'] as $element) {
346  $r = $element['subst'];
347  $type = '';
348  $idRecord = $record['uid'];
349  if (empty($r)) {
350  continue;
351  }
352 
354  foreach ($this->hookObjectsArr as $keyArr => $hookObj) {
355  $type = $hookObj->fetchType($r, $type, $keyArr);
356  // Store the type that was found
357  // This prevents overriding by internal validator
358  if (!empty($type)) {
359  $r['type'] = $type;
360  }
361  }
362  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['substr'] = $r;
363  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['row'] = $record;
364  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['table'] = $table;
365  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['field'] = $field;
366  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $r['tokenID']]['uid'] = $idRecord;
367  }
368  }
369 
380  protected function ‪analyzeTypoLinks(array $resultArray, array &$results, $htmlParser, array $record, $field, $table)
381  {
382  $currentR = [];
383  $linkTags = $htmlParser->splitIntoBlock('a,link', $resultArray['content']);
384  $idRecord = $record['uid'];
385  $type = '';
386  $title = '';
387  $countLinkTags = count($linkTags);
388  for ($i = 1; $i < $countLinkTags; $i += 2) {
389  $referencedRecordType = '';
390  foreach ($resultArray['elements'] as $element) {
391  $type = '';
392  $r = $element['subst'];
393  if (empty($r['tokenID']) || substr_count($linkTags[$i], $r['tokenID']) === 0) {
394  continue;
395  }
396 
397  // Type of referenced record
398  if (strpos($r['recordRef'], 'pages') !== false) {
399  $currentR = $r;
400  // Contains number of the page
401  $referencedRecordType = $r['tokenValue'];
402  $wasPage = true;
403  } elseif (strpos($r['recordRef'], 'tt_content') !== false && (isset($wasPage) && $wasPage === true)) {
404  $referencedRecordType = $referencedRecordType . '#c' . $r['tokenValue'];
405  $wasPage = false;
406  } else {
407  $currentR = $r;
408  }
409  $title = strip_tags($linkTags[$i]);
410  }
412  foreach ($this->hookObjectsArr as $keyArr => $hookObj) {
413  $type = $hookObj->fetchType($currentR, $type, $keyArr);
414  // Store the type that was found
415  // This prevents overriding by internal validator
416  if (!empty($type)) {
417  $currentR['type'] = $type;
418  }
419  }
420  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['substr'] = $currentR;
421  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['row'] = $record;
422  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['table'] = $table;
423  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['field'] = $field;
424  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['uid'] = $idRecord;
425  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['link_title'] = $title;
426  $results[$type][$table . ':' . $field . ':' . $idRecord . ':' . $currentR['tokenID']]['pageAndAnchor'] = $referencedRecordType;
427  }
428  }
429 
436  public function ‪getLinkCounts($curPage)
437  {
438  $markerArray = [];
439 
440  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
441  ->getQueryBuilderForTable('tx_linkvalidator_link');
442  $queryBuilder->getRestrictions()->removeAll();
443 
444  $result = $queryBuilder->select('link_type')
445  ->addSelectLiteral($queryBuilder->expr()->count('uid', 'nbBrokenLinks'))
446  ->from('tx_linkvalidator_link')
447  ->where(
448  $queryBuilder->expr()->orX(
449  $queryBuilder->expr()->andX(
450  $queryBuilder->expr()->in(
451  'record_uid',
452  $queryBuilder->createNamedParameter($this->pids, Connection::PARAM_INT_ARRAY)
453  ),
454  $queryBuilder->expr()->eq('table_name', $queryBuilder->createNamedParameter('pages'))
455  ),
456  $queryBuilder->expr()->andX(
457  $queryBuilder->expr()->in(
458  'record_pid',
459  $queryBuilder->createNamedParameter($this->pids, Connection::PARAM_INT_ARRAY)
460  ),
461  $queryBuilder->expr()->neq('table_name', $queryBuilder->createNamedParameter('pages'))
462  )
463  )
464  )
465  ->groupBy('link_type')
466  ->execute();
467 
468  while ($row = $result->fetch()) {
469  $markerArray[$row['link_type']] = $row['nbBrokenLinks'];
470  $markerArray['brokenlinkCount'] += $row['nbBrokenLinks'];
471  }
472  return $markerArray;
473  }
474 
490  public function ‪extGetTreeList($id, $depth, $begin = 0, $permsClause, $considerHidden = false)
491  {
492  $depth = (int)$depth;
493  $begin = (int)$begin;
494  $id = (int)$id;
495  $theList = '';
496  if ($depth === 0) {
497  return $theList;
498  }
499 
500  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
501  $queryBuilder->getRestrictions()
502  ->removeAll()
503  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
504 
505  $result = $queryBuilder
506  ->select('uid', 'title', 'hidden', 'extendToSubpages')
507  ->from('pages')
508  ->where(
509  $queryBuilder->expr()->eq(
510  'pid',
511  $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
512  ),
514  )
515  ->execute();
516 
517  while ($row = $result->fetch()) {
518  if ($begin <= 0 && ($row['hidden'] == 0 || $considerHidden)) {
519  $theList .= $row['uid'] . ',';
520  }
521  if ($depth > 1 && (!($row['hidden'] == 1 && $row['extendToSubpages'] == 1) || $considerHidden)) {
522  $theList .= $this->‪extGetTreeList(
523  $row['uid'],
524  $depth - 1,
525  $begin - 1,
526  $permsClause,
527  $considerHidden
528  );
529  }
530  }
531  return $theList;
532  }
533 
540  public function ‪getRootLineIsHidden(array $pageInfo)
541  {
542  if ($pageInfo['pid'] === 0) {
543  return false;
544  }
545 
546  if ($pageInfo['extendToSubpages'] == 1 && $pageInfo['hidden'] == 1) {
547  return true;
548  }
549 
550  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
551  $queryBuilder->getRestrictions()->removeAll();
552 
553  $row = $queryBuilder
554  ->select('uid', 'title', 'hidden', 'extendToSubpages')
555  ->from('pages')
556  ->where(
557  $queryBuilder->expr()->eq(
558  'uid',
559  $queryBuilder->createNamedParameter($pageInfo['pid'], \PDO::PARAM_INT)
560  )
561  )
562  ->execute()
563  ->fetch();
564 
565  if ($row !== false) {
566  return $this->‪getRootLineIsHidden($row);
567  }
568  return false;
569  }
570 
580  protected function ‪emitBeforeAnalyzeRecordSignal($results, $record, $table, ‪$fields)
581  {
582  return $this->‪getSignalSlotDispatcher()->dispatch(
583  self::class,
584  'beforeAnalyzeRecord',
585  [$results, $record, $table, ‪$fields, $this]
586  );
587  }
588 
592  protected function ‪getSignalSlotDispatcher()
593  {
594  return $this->‪getObjectManager()->get(\‪TYPO3\CMS\‪Extbase\SignalSlot\Dispatcher::class);
595  }
596 
600  protected function ‪getObjectManager()
601  {
602  return GeneralUtility::makeInstance(\‪TYPO3\CMS\‪Extbase\Object\ObjectManager::class);
603  }
604 
608  protected function ‪getLanguageService()
609  {
610  return ‪$GLOBALS['LANG'];
611  }
612 }
‪TYPO3\CMS\Core\Localization\LanguageService\includeLLFile
‪mixed includeLLFile($fileRef, $setGlobal=true, $mergeLocalOntoDefault=false)
Definition: LanguageService.php:260
‪TYPO3\CMS\Extbase\Annotation
Definition: IgnoreValidation.php:4
‪TYPO3\CMS\Core\Html\HtmlParser
Definition: HtmlParser.php:26
‪TYPO3\CMS\Backend\Utility\BackendUtility\softRefParserObj
‪static mixed & softRefParserObj($spKey)
Definition: BackendUtility.php:3692
‪TYPO3
‪$fields
‪$fields
Definition: pages.php:4
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:30
‪TYPO3\CMS\Backend\Utility\BackendUtility\getRecordTitle
‪static string getRecordTitle($table, $row, $prep=false, $forceResult=true)
Definition: BackendUtility.php:1811
‪TYPO3\CMS\Linkvalidator
Definition: LinkAnalyzer.php:2
‪TYPO3\CMS\Backend\Utility\BackendUtility
Definition: BackendUtility.php:72
‪TYPO3\CMS\Backend\Utility\BackendUtility\explodeSoftRefParserList
‪static array bool explodeSoftRefParserList($parserList)
Definition: BackendUtility.php:3728
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:31
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:163
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:26
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:29
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45