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 file
23 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
24 */
25class FileNode extends AbstractNode implements NodeInterface
26{
27    /**
28     * @var string Default for files is octal 0664 == decimal 436
29     */
30    protected $targetPermission = '0664';
31
32    /**
33     * @var string|null Target content of file. If NULL, target content is ignored
34     */
35    protected $targetContent;
36
37    /**
38     * Implement constructor
39     *
40     * @param array $structure Structure array
41     * @param NodeInterface $parent Parent object
42     * @throws Exception\InvalidArgumentException
43     */
44    public function __construct(array $structure, NodeInterface $parent = null)
45    {
46        if ($parent === null) {
47            throw new InvalidArgumentException(
48                'File node must have parent',
49                1366927513
50            );
51        }
52        $this->parent = $parent;
53
54        // Ensure name is a single segment, but not a path like foo/bar or an absolute path /foo
55        if (str_contains($structure['name'], '/')) {
56            throw new InvalidArgumentException(
57                'File name must not contain forward slash',
58                1366222207
59            );
60        }
61        $this->name = $structure['name'];
62
63        if (isset($structure['targetPermission'])) {
64            $this->setTargetPermission($structure['targetPermission']);
65        }
66
67        if (isset($structure['targetContent']) && isset($structure['targetContentFile'])) {
68            throw new InvalidArgumentException(
69                'Either targetContent or targetContentFile can be set, but not both',
70                1380364361
71            );
72        }
73
74        if (isset($structure['targetContent'])) {
75            $this->targetContent = $structure['targetContent'];
76        }
77        if (isset($structure['targetContentFile'])) {
78            if (!is_readable($structure['targetContentFile'])) {
79                throw new InvalidArgumentException(
80                    'targetContentFile ' . $structure['targetContentFile'] . ' does not exist or is not readable',
81                    1380364362
82                );
83            }
84            $fileContent = file_get_contents($structure['targetContentFile']);
85            if ($fileContent === false) {
86                throw new InvalidArgumentException(
87                    'Error while reading targetContentFile ' . $structure['targetContentFile'],
88                    1380364363
89                );
90            }
91            $this->targetContent = $fileContent;
92        }
93    }
94
95    /**
96     * Get own status
97     * Returns warning if file not exists
98     * Returns error if file exists but content is not as expected (can / shouldn't be fixed)
99     *
100     * @return FlashMessage[]
101     */
102    public function getStatus(): array
103    {
104        $result = [];
105        if (!$this->exists()) {
106            $result[] = new FlashMessage(
107                'By using "Try to fix errors" we can try to create it',
108                'File ' . $this->getRelativePathBelowSiteRoot() . ' does not exist',
109                FlashMessage::WARNING
110            );
111        } else {
112            $result = $this->getSelfStatus();
113        }
114        return $result;
115    }
116
117    /**
118     * Fix structure
119     *
120     * If there is nothing to fix, returns an empty array
121     *
122     * @return FlashMessage[]
123     */
124    public function fix(): array
125    {
126        $result = $this->fixSelf();
127        return $result;
128    }
129
130    /**
131     * Fix this node: create if not there, fix permissions
132     *
133     * @return FlashMessage[]
134     */
135    protected function fixSelf(): array
136    {
137        $result = [];
138        if (!$this->exists()) {
139            $resultCreateFile = $this->createFile();
140            $result[] = $resultCreateFile;
141            if ($resultCreateFile->getSeverity() === FlashMessage::OK
142                && $this->targetContent !== null
143            ) {
144                $result[] = $this->setContent();
145                if (!$this->isPermissionCorrect()) {
146                    $result[] = $this->fixPermission();
147                }
148            }
149        } elseif (!$this->isFile()) {
150            $fileType = @filetype($this->getAbsolutePath());
151            if ($fileType) {
152                $messageBody =
153                    'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,' .
154                    ' but is of type ' . $fileType . '. This cannot be fixed automatically. Please investigate.'
155                ;
156            } else {
157                $messageBody =
158                    'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,' .
159                    ' but is of unknown type, probably because an upper level directory does not exist. Please investigate.'
160                ;
161            }
162            $result[] = new FlashMessage(
163                $messageBody,
164                'Path ' . $this->getRelativePathBelowSiteRoot() . ' is not a file',
165                FlashMessage::ERROR
166            );
167        } elseif (!$this->isPermissionCorrect()) {
168            $result[] = $this->fixPermission();
169        }
170        return $result;
171    }
172
173    /**
174     * Create file if not exists
175     *
176     * @throws Exception
177     * @return FlashMessage
178     */
179    protected function createFile(): FlashMessage
180    {
181        if ($this->exists()) {
182            throw new Exception(
183                'File ' . $this->getRelativePathBelowSiteRoot() . ' already exists',
184                1367048077
185            );
186        }
187        $result = @touch($this->getAbsolutePath());
188        if ($result === true) {
189            return new FlashMessage(
190                '',
191                'File ' . $this->getRelativePathBelowSiteRoot() . ' successfully created.'
192            );
193        }
194        return new FlashMessage(
195            'The target file could not be created. There is probably a'
196                . ' group or owner permission problem on the parent directory.',
197            'File ' . $this->getRelativePathBelowSiteRoot() . ' not created!',
198            FlashMessage::ERROR
199        );
200    }
201
202    /**
203     * Get status of file
204     *
205     * @return FlashMessage[]
206     */
207    protected function getSelfStatus(): array
208    {
209        $result = [];
210        if (!$this->isFile()) {
211            $result[] = new FlashMessage(
212                'Path ' . $this->getAbsolutePath() . ' should be a file,'
213                    . ' but is of type ' . filetype($this->getAbsolutePath()),
214                $this->getRelativePathBelowSiteRoot() . ' is not a file',
215                FlashMessage::ERROR
216            );
217        } elseif (!$this->isWritable()) {
218            $result[] = new FlashMessage(
219                'File ' . $this->getRelativePathBelowSiteRoot() . ' exists, but is not writable.',
220                'File ' . $this->getRelativePathBelowSiteRoot() . ' is not writable',
221                FlashMessage::NOTICE
222            );
223        } elseif (!$this->isPermissionCorrect()) {
224            $result[] = new FlashMessage(
225                'Default configured permissions are ' . $this->getTargetPermission()
226                    . ' but file permissions are ' . $this->getCurrentPermission(),
227                'File ' . $this->getRelativePathBelowSiteRoot() . ' permissions mismatch',
228                FlashMessage::NOTICE
229            );
230        }
231        if ($this->isFile() && !$this->isContentCorrect()) {
232            $result[] = new FlashMessage(
233                'File content is not identical to default content. This file may have been changed manually.'
234                    . ' The Install Tool will not overwrite the current version!',
235                'File ' . $this->getRelativePathBelowSiteRoot() . ' content differs',
236                FlashMessage::NOTICE
237            );
238        } else {
239            $result[] = new FlashMessage(
240                'Is a file with the default content and configured permissions of ' . $this->getTargetPermission(),
241                'File ' . $this->getRelativePathBelowSiteRoot()
242            );
243        }
244        return $result;
245    }
246
247    /**
248     * Compare current file content with target file content
249     *
250     * @throws Exception If file does not exist
251     * @return bool TRUE if current and target file content are identical
252     */
253    protected function isContentCorrect()
254    {
255        $absolutePath = $this->getAbsolutePath();
256        if (is_link($absolutePath) || !is_file($absolutePath)) {
257            throw new Exception(
258                'File ' . $absolutePath . ' must exist',
259                1367056363
260            );
261        }
262        $result = false;
263        if ($this->targetContent === null) {
264            $result = true;
265        } else {
266            $targetContentHash = md5($this->targetContent);
267            $currentContentHash = md5((string)file_get_contents($absolutePath));
268            if ($targetContentHash === $currentContentHash) {
269                $result = true;
270            }
271        }
272        return $result;
273    }
274
275    /**
276     * Sets content of file to target content
277     *
278     * @throws Exception If file does not exist
279     * @return FlashMessage
280     */
281    protected function setContent(): FlashMessage
282    {
283        $absolutePath = $this->getAbsolutePath();
284        if (is_link($absolutePath) || !is_file($absolutePath)) {
285            throw new Exception(
286                'File ' . $absolutePath . ' must exist',
287                1367060201
288            );
289        }
290        if ($this->targetContent === null) {
291            throw new Exception(
292                'Target content not defined for ' . $absolutePath,
293                1367060202
294            );
295        }
296        $result = @file_put_contents($absolutePath, $this->targetContent);
297        if ($result !== false) {
298            return new FlashMessage(
299                '',
300                'Set content to ' . $this->getRelativePathBelowSiteRoot()
301            );
302        }
303        return new FlashMessage(
304            'Setting content of the file failed for unknown reasons.',
305            'Setting content to ' . $this->getRelativePathBelowSiteRoot() . ' failed',
306            FlashMessage::ERROR
307        );
308    }
309
310    /**
311     * Checks if not is a file
312     *
313     * @return bool
314     */
315    protected function isFile()
316    {
317        $path = $this->getAbsolutePath();
318        return !is_link($path) && is_file($path);
319    }
320}
321