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\Install\FolderStructure;
17
18use TYPO3\CMS\Core\Messaging\FlashMessage;
19use TYPO3\CMS\Install\FolderStructure\Exception\InvalidArgumentException;
20
21/**
22 * A link
23 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
24 */
25class LinkNode extends AbstractNode implements NodeInterface
26{
27    /**
28     * @var string Optional link target
29     */
30    protected $target = '';
31
32    /**
33     * Implement constructor
34     *
35     * @param array $structure Structure array
36     * @param NodeInterface $parent Parent object
37     * @throws Exception\InvalidArgumentException
38     */
39    public function __construct(array $structure, NodeInterface $parent = null)
40    {
41        if ($parent === null) {
42            throw new InvalidArgumentException(
43                'Link node must have parent',
44                1380485700
45            );
46        }
47        $this->parent = $parent;
48
49        // Ensure name is a single segment, but not a path like foo/bar or an absolute path /foo
50        if (str_contains($structure['name'], '/')) {
51            throw new InvalidArgumentException(
52                'File name must not contain forward slash',
53                1380546061
54            );
55        }
56        $this->name = $structure['name'];
57
58        if (isset($structure['target']) && $structure['target'] !== '') {
59            $this->target = $structure['target'];
60        }
61    }
62
63    /**
64     * Get own status
65     * Returns information status if running on Windows
66     * Returns OK status if is link and possible target is correct
67     * Else returns error (not fixable)
68     *
69     * @return FlashMessage[]
70     */
71    public function getStatus(): array
72    {
73        if ($this->isWindowsOs()) {
74            return [
75                new FlashMessage(
76                    'This node is not handled for Windows OS and should be checked manually.',
77                    $this->getRelativePathBelowSiteRoot() . ' should be a link, but this support is incomplete for Windows.',
78                    FlashMessage::INFO
79                ),
80            ];
81        }
82
83        if (!$this->exists()) {
84            return [
85                new FlashMessage(
86                    'Links cannot be fixed by this system',
87                    $this->getRelativePathBelowSiteRoot() . ' should be a link, but it does not exist',
88                    FlashMessage::ERROR
89                ),
90            ];
91        }
92
93        if (!$this->isLink()) {
94            $type = @filetype($this->getAbsolutePath());
95            if ($type) {
96                $messageBody =
97                    'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a link,' .
98                    ' but is of type ' . $type . '. This cannot be fixed automatically. Please investigate.'
99                ;
100            } else {
101                $messageBody =
102                    'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,' .
103                    ' but is of unknown type, probably because an upper level directory does not exist. Please investigate.'
104                ;
105            }
106            return [
107                new FlashMessage(
108                    $messageBody,
109                    'Path ' . $this->getRelativePathBelowSiteRoot() . ' is not a link',
110                    FlashMessage::WARNING
111                ),
112            ];
113        }
114
115        if (!$this->isTargetCorrect()) {
116            return [
117                new FlashMessage(
118                    'Link target should be ' . $this->getTarget() . ' but is ' . $this->getCurrentTarget(),
119                    $this->getRelativePathBelowSiteRoot() . ' is a link, but link target is not as specified',
120                    FlashMessage::ERROR
121                ),
122            ];
123        }
124        $message = 'Is a link';
125        if ($this->getTarget() !== '') {
126            $message .= ' and correctly points to target ' . $this->getTarget();
127        }
128        return [
129            new FlashMessage(
130                $message,
131                $this->getRelativePathBelowSiteRoot()
132            ),
133        ];
134    }
135
136    /**
137     * Fix structure
138     *
139     * If there is nothing to fix, returns an empty array
140     *
141     * @return FlashMessage[]
142     */
143    public function fix(): array
144    {
145        return [];
146    }
147
148    /**
149     * Get link target
150     *
151     * @return string Link target
152     */
153    protected function getTarget()
154    {
155        return $this->target;
156    }
157
158    /**
159     * Find out if node is a link
160     *
161     * @throws Exception\InvalidArgumentException
162     * @return bool TRUE if node is a link
163     */
164    protected function isLink()
165    {
166        if (!$this->exists()) {
167            throw new InvalidArgumentException(
168                'Link does not exist',
169                1380556246
170            );
171        }
172        return @is_link($this->getAbsolutePath());
173    }
174
175    /**
176     * Checks if the real link target is identical to given target
177     *
178     * @throws Exception\InvalidArgumentException
179     * @return bool TRUE if target is correct
180     */
181    protected function isTargetCorrect()
182    {
183        if (!$this->exists()) {
184            throw new InvalidArgumentException(
185                'Link does not exist',
186                1380556245
187            );
188        }
189        if (!$this->isLink()) {
190            throw new InvalidArgumentException(
191                'Node is not a link',
192                1380556247
193            );
194        }
195
196        $result = false;
197        $expectedTarget = $this->getTarget();
198        if (empty($expectedTarget)) {
199            $result = true;
200        } else {
201            $actualTarget = $this->getCurrentTarget();
202            if ($expectedTarget === rtrim((string)$actualTarget, '/')) {
203                $result = true;
204            }
205        }
206        return $result;
207    }
208
209    /**
210     * Return current target of link
211     *
212     * @return false|string target
213     */
214    protected function getCurrentTarget()
215    {
216        return readlink($this->getAbsolutePath());
217    }
218}
219