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