1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Linkvalidator\Linktype; 17 18use TYPO3\CMS\Core\Database\ConnectionPool; 19use TYPO3\CMS\Core\Utility\GeneralUtility; 20 21/** 22 * This class provides Check Internal Links plugin implementation 23 */ 24class InternalLinktype extends AbstractLinktype 25{ 26 /** 27 * @var string 28 */ 29 const DELETED = 'deleted'; 30 31 /** 32 * @var string 33 */ 34 const HIDDEN = 'hidden'; 35 36 /** 37 * @var string 38 */ 39 const MOVED = 'moved'; 40 41 /** 42 * @var string 43 */ 44 const NOTEXISTING = 'notExisting'; 45 46 /** 47 * Result of the check, if the current page uid is valid or not 48 * 49 * @var bool 50 */ 51 protected $responsePage = true; 52 53 /** 54 * Result of the check, if the current content uid is valid or not 55 * 56 * @var bool 57 */ 58 protected $responseContent = true; 59 60 /** 61 * Checks a given URL + /path/filename.ext for validity 62 * 63 * @param string $url Url to check as page-id or page-id#anchor (if anchor is present) 64 * @param array $softRefEntry The soft reference entry which builds the context of that url 65 * @param \TYPO3\CMS\Linkvalidator\LinkAnalyzer $reference Parent instance 66 * @return bool TRUE on success or FALSE on error 67 */ 68 public function checkLink($url, $softRefEntry, $reference) 69 { 70 $page = null; 71 $anchor = ''; 72 $this->responseContent = true; 73 // Might already contain values - empty it 74 unset($this->errorParams); 75 // Only check pages records. Content elements will also be checked 76 // as we extract the anchor in the next step. 77 [$table] = explode(':', $softRefEntry['substr']['recordRef']); 78 if (!in_array($table, ['pages', 'tt_content'], true)) { 79 return true; 80 } 81 // Defines the linked page and anchor (if any). 82 if (str_contains($url, '#c')) { 83 $parts = explode('#c', $url); 84 $page = $parts[0]; 85 $anchor = $parts[1]; 86 } elseif ( 87 $table === 'tt_content' 88 && strpos($softRefEntry['row'][$softRefEntry['field']], 't3://') === 0 89 ) { 90 $parsedTypoLinkUrl = @parse_url($softRefEntry['row'][$softRefEntry['field']]); 91 if ($parsedTypoLinkUrl['host'] === 'page') { 92 parse_str($parsedTypoLinkUrl['query'], $query); 93 if (isset($query['uid'])) { 94 $page = (int)$query['uid']; 95 $anchor = (int)$url; 96 } 97 } 98 } else { 99 $page = $url; 100 } 101 // Check if the linked page is OK 102 $this->responsePage = $this->checkPage((int)$page); 103 // Check if the linked content element is OK 104 if ($anchor) { 105 // Check if the content element is OK 106 $this->responseContent = $this->checkContent((int)$page, (int)$anchor); 107 } 108 if ( 109 (is_array($this->errorParams['page'] ?? false) && !$this->responsePage) 110 || (is_array($this->errorParams['content'] ?? false) && !$this->responseContent) 111 ) { 112 $this->setErrorParams($this->errorParams); 113 } 114 115 return $this->responsePage && $this->responseContent; 116 } 117 118 /** 119 * Checks a given page uid for validity 120 * 121 * @param int $page Page uid to check 122 * @return bool TRUE on success or FALSE on error 123 */ 124 protected function checkPage($page) 125 { 126 // Get page ID on which the content element in fact is located 127 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 128 $queryBuilder->getRestrictions()->removeAll(); 129 $row = $queryBuilder 130 ->select('uid', 'title', 'deleted', 'hidden', 'starttime', 'endtime') 131 ->from('pages') 132 ->where( 133 $queryBuilder->expr()->eq( 134 'uid', 135 $queryBuilder->createNamedParameter($page, \PDO::PARAM_INT) 136 ) 137 ) 138 ->executeQuery() 139 ->fetchAssociative(); 140 $this->responsePage = true; 141 if ($row) { 142 if ($row['deleted'] == '1') { 143 $this->errorParams['errorType']['page'] = self::DELETED; 144 $this->errorParams['page']['title'] = $row['title']; 145 $this->errorParams['page']['uid'] = $row['uid']; 146 $this->responsePage = false; 147 } elseif ($row['hidden'] == '1' 148 || $GLOBALS['EXEC_TIME'] < (int)$row['starttime'] 149 || $row['endtime'] && (int)$row['endtime'] < $GLOBALS['EXEC_TIME'] 150 ) { 151 $this->errorParams['errorType']['page'] = self::HIDDEN; 152 $this->errorParams['page']['title'] = $row['title']; 153 $this->errorParams['page']['uid'] = $row['uid']; 154 $this->responsePage = false; 155 } 156 } else { 157 $this->errorParams['errorType']['page'] = self::NOTEXISTING; 158 $this->errorParams['page']['uid'] = (int)$page; 159 $this->responsePage = false; 160 } 161 return $this->responsePage; 162 } 163 164 /** 165 * Checks a given content uid for validity 166 * 167 * @param int $page Uid of the page to which the link is pointing 168 * @param int $anchor Uid of the content element to check 169 * @return bool TRUE on success or FALSE on error 170 */ 171 protected function checkContent($page, $anchor) 172 { 173 // Get page ID on which the content element in fact is located 174 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); 175 $queryBuilder->getRestrictions()->removeAll(); 176 $row = $queryBuilder 177 ->select('uid', 'pid', 'header', 'deleted', 'hidden', 'starttime', 'endtime') 178 ->from('tt_content') 179 ->where( 180 $queryBuilder->expr()->eq( 181 'uid', 182 $queryBuilder->createNamedParameter($anchor, \PDO::PARAM_INT) 183 ) 184 ) 185 ->executeQuery() 186 ->fetchAssociative(); 187 $this->responseContent = true; 188 // this content element exists 189 if ($row) { 190 $page = (int)$page; 191 // page ID on which this CE is in fact located. 192 $correctPageID = (int)$row['pid']; 193 // Check if the element is on the linked page 194 // (The element might have been moved to another page) 195 if ($correctPageID !== $page) { 196 $this->errorParams['errorType']['content'] = self::MOVED; 197 $this->errorParams['content']['uid'] = (int)$anchor; 198 $this->errorParams['content']['wrongPage'] = $page; 199 $this->errorParams['content']['rightPage'] = $correctPageID; 200 $this->responseContent = false; 201 } else { 202 // The element is located on the page to which the link is pointing 203 if ($row['deleted'] == '1') { 204 $this->errorParams['errorType']['content'] = self::DELETED; 205 $this->errorParams['content']['title'] = $row['header']; 206 $this->errorParams['content']['uid'] = $row['uid']; 207 $this->responseContent = false; 208 } elseif ($row['hidden'] == '1' || $GLOBALS['EXEC_TIME'] < (int)$row['starttime'] || $row['endtime'] && (int)$row['endtime'] < $GLOBALS['EXEC_TIME']) { 209 $this->errorParams['errorType']['content'] = self::HIDDEN; 210 $this->errorParams['content']['title'] = $row['header']; 211 $this->errorParams['content']['uid'] = $row['uid']; 212 $this->responseContent = false; 213 } 214 } 215 } else { 216 // The content element does not exist 217 $this->errorParams['errorType']['content'] = self::NOTEXISTING; 218 $this->errorParams['content']['uid'] = (int)$anchor; 219 $this->responseContent = false; 220 } 221 return $this->responseContent; 222 } 223 224 /** 225 * Generates the localized error message from the error params saved from the parsing 226 * 227 * @param array $errorParams All parameters needed for the rendering of the error message 228 * @return string Validation error message 229 */ 230 public function getErrorMessage($errorParams) 231 { 232 $errorPage = null; 233 $errorContent = null; 234 $lang = $this->getLanguageService(); 235 $errorType = $errorParams['errorType']; 236 if (is_array($errorParams['page'] ?? false)) { 237 switch ($errorType['page']) { 238 case self::DELETED: 239 $errorPage = str_replace( 240 [ 241 '###title###', 242 '###uid###', 243 ], 244 [ 245 $errorParams['page']['title'], 246 $errorParams['page']['uid'], 247 ], 248 $lang->getLL('list.report.pagedeleted') 249 ); 250 break; 251 case self::HIDDEN: 252 $errorPage = str_replace( 253 [ 254 '###title###', 255 '###uid###', 256 ], 257 [ 258 $errorParams['page']['title'], 259 $errorParams['page']['uid'], 260 ], 261 $lang->getLL('list.report.pagenotvisible') 262 ); 263 break; 264 default: 265 $errorPage = str_replace( 266 '###uid###', 267 $errorParams['page']['uid'], 268 $lang->getLL('list.report.pagenotexisting') 269 ); 270 } 271 } 272 if (is_array($errorParams['content'] ?? false)) { 273 switch ($errorType['content']) { 274 case self::DELETED: 275 $errorContent = str_replace( 276 [ 277 '###title###', 278 '###uid###', 279 ], 280 [ 281 $errorParams['content']['title'], 282 $errorParams['content']['uid'], 283 ], 284 $lang->getLL('list.report.contentdeleted') 285 ); 286 break; 287 case self::HIDDEN: 288 $errorContent = str_replace( 289 [ 290 '###title###', 291 '###uid###', 292 ], 293 [ 294 $errorParams['content']['title'], 295 $errorParams['content']['uid'], 296 ], 297 $lang->getLL('list.report.contentnotvisible') 298 ); 299 break; 300 case self::MOVED: 301 $errorContent = str_replace( 302 [ 303 '###title###', 304 '###uid###', 305 '###wrongpage###', 306 '###rightpage###', 307 ], 308 [ 309 $errorParams['content']['title'], 310 $errorParams['content']['uid'], 311 $errorParams['content']['wrongPage'], 312 $errorParams['content']['rightPage'], 313 ], 314 $lang->getLL('list.report.contentmoved') 315 ); 316 break; 317 default: 318 $errorContent = str_replace('###uid###', $errorParams['content']['uid'], $lang->getLL('list.report.contentnotexisting')); 319 } 320 } 321 if (isset($errorPage) && isset($errorContent)) { 322 $response = $errorPage . LF . $errorContent; 323 } elseif (isset($errorPage)) { 324 $response = $errorPage; 325 } elseif (isset($errorContent)) { 326 $response = $errorContent; 327 } else { 328 // This should not happen 329 $response = $lang->getLL('list.report.noinformation'); 330 } 331 return $response; 332 } 333 334 /** 335 * Constructs a valid Url for browser output 336 * 337 * @param array $row Broken link record 338 * @return string Parsed broken url 339 */ 340 public function getBrokenUrl($row) 341 { 342 $domain = rtrim($GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getSiteUrl(), '/'); 343 return $domain . '/index.php?id=' . $row['url']; 344 } 345} 346