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 (strpos($structure['name'], '/') !== false) {
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            $this->targetContent = file_get_contents($structure['targetContentFile']);
85        }
86    }
87
88    /**
89     * Get own status
90     * Returns warning if file not exists
91     * Returns error if file exists but content is not as expected (can / shouldn't be fixed)
92     *
93     * @return FlashMessage[]
94     */
95    public function getStatus(): array
96    {
97        $result = [];
98        if (!$this->exists()) {
99            $result[] = new FlashMessage(
100                'By using "Try to fix errors" we can try to create it',
101                'File ' . $this->getRelativePathBelowSiteRoot() . ' does not exist',
102                FlashMessage::WARNING
103            );
104        } else {
105            $result = $this->getSelfStatus();
106        }
107        return $result;
108    }
109
110    /**
111     * Fix structure
112     *
113     * If there is nothing to fix, returns an empty array
114     *
115     * @return FlashMessage[]
116     */
117    public function fix(): array
118    {
119        $result = $this->fixSelf();
120        return $result;
121    }
122
123    /**
124     * Fix this node: create if not there, fix permissions
125     *
126     * @return FlashMessage[]
127     */
128    protected function fixSelf(): array
129    {
130        $result = [];
131        if (!$this->exists()) {
132            $resultCreateFile = $this->createFile();
133            $result[] = $resultCreateFile;
134            if ($resultCreateFile->getSeverity() === FlashMessage::OK
135                && $this->targetContent !== null
136            ) {
137                $result[] = $this->setContent();
138                if (!$this->isPermissionCorrect()) {
139                    $result[] = $this->fixPermission();
140                }
141            }
142        } elseif (!$this->isFile()) {
143            $fileType = @filetype($this->getAbsolutePath());
144            if ($fileType) {
145                $messageBody =
146                    'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,' .
147                    ' but is of type ' . $fileType . '. This cannot be fixed automatically. Please investigate.'
148                ;
149            } else {
150                $messageBody =
151                    'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a file,' .
152                    ' but is of unknown type, probably because an upper level directory does not exist. Please investigate.'
153                ;
154            }
155            $result[] = new FlashMessage(
156                $messageBody,
157                'Path ' . $this->getRelativePathBelowSiteRoot() . ' is not a file',
158                FlashMessage::ERROR
159            );
160        } elseif (!$this->isPermissionCorrect()) {
161            $result[] = $this->fixPermission();
162        }
163        return $result;
164    }
165
166    /**
167     * Create file if not exists
168     *
169     * @throws Exception
170     * @return FlashMessage
171     */
172    protected function createFile(): FlashMessage
173    {
174        if ($this->exists()) {
175            throw new Exception(
176                'File ' . $this->getRelativePathBelowSiteRoot() . ' already exists',
177                1367048077
178            );
179        }
180        $result = @touch($this->getAbsolutePath());
181        if ($result === true) {
182            return new FlashMessage(
183                '',
184                'File ' . $this->getRelativePathBelowSiteRoot() . ' successfully created.'
185            );
186        }
187        return new FlashMessage(
188            'The target file could not be created. There is probably a'
189                . ' group or owner permission problem on the parent directory.',
190            'File ' . $this->getRelativePathBelowSiteRoot() . ' not created!',
191            FlashMessage::ERROR
192        );
193    }
194
195    /**
196     * Get status of file
197     *
198     * @return FlashMessage[]
199     */
200    protected function getSelfStatus(): array
201    {
202        $result = [];
203        if (!$this->isFile()) {
204            $result[] = new FlashMessage(
205                'Path ' . $this->getAbsolutePath() . ' should be a file,'
206                    . ' but is of type ' . filetype($this->getAbsolutePath()),
207                $this->getRelativePathBelowSiteRoot() . ' is not a file',
208                FlashMessage::ERROR
209            );
210        } elseif (!$this->isWritable()) {
211            $result[] = new FlashMessage(
212                'File ' . $this->getRelativePathBelowSiteRoot() . ' exists, but is not writable.',
213                'File ' . $this->getRelativePathBelowSiteRoot() . ' is not writable',
214                FlashMessage::NOTICE
215            );
216        } elseif (!$this->isPermissionCorrect()) {
217            $result[] = new FlashMessage(
218                'Default configured permissions are ' . $this->getTargetPermission()
219                    . ' but file permissions are ' . $this->getCurrentPermission(),
220                'File ' . $this->getRelativePathBelowSiteRoot() . ' permissions mismatch',
221                FlashMessage::NOTICE
222            );
223        }
224        if ($this->isFile() && !$this->isContentCorrect()) {
225            $result[] = new FlashMessage(
226                'File content is not identical to default content. This file may have been changed manually.'
227                    . ' The Install Tool will not overwrite the current version!',
228                'File ' . $this->getRelativePathBelowSiteRoot() . ' content differs',
229                FlashMessage::NOTICE
230            );
231        } else {
232            $result[] = new FlashMessage(
233                'Is a file with the default content and configured permissions of ' . $this->getTargetPermission(),
234                'File ' . $this->getRelativePathBelowSiteRoot()
235            );
236        }
237        return $result;
238    }
239
240    /**
241     * Compare current file content with target file content
242     *
243     * @throws Exception If file does not exist
244     * @return bool TRUE if current and target file content are identical
245     */
246    protected function isContentCorrect()
247    {
248        $absolutePath = $this->getAbsolutePath();
249        if (is_link($absolutePath) || !is_file($absolutePath)) {
250            throw new Exception(
251                'File ' . $absolutePath . ' must exist',
252                1367056363
253            );
254        }
255        $result = false;
256        if ($this->targetContent === null) {
257            $result = true;
258        } else {
259            $targetContentHash = md5($this->targetContent);
260            $currentContentHash = md5((string)file_get_contents($absolutePath));
261            if ($targetContentHash === $currentContentHash) {
262                $result = true;
263            }
264        }
265        return $result;
266    }
267
268    /**
269     * Sets content of file to target content
270     *
271     * @throws Exception If file does not exist
272     * @return FlashMessage
273     */
274    protected function setContent(): FlashMessage
275    {
276        $absolutePath = $this->getAbsolutePath();
277        if (is_link($absolutePath) || !is_file($absolutePath)) {
278            throw new Exception(
279                'File ' . $absolutePath . ' must exist',
280                1367060201
281            );
282        }
283        if ($this->targetContent === null) {
284            throw new Exception(
285                'Target content not defined for ' . $absolutePath,
286                1367060202
287            );
288        }
289        $result = @file_put_contents($absolutePath, $this->targetContent);
290        if ($result !== false) {
291            return new FlashMessage(
292                '',
293                'Set content to ' . $this->getRelativePathBelowSiteRoot()
294            );
295        }
296        return new FlashMessage(
297            'Setting content of the file failed for unknown reasons.',
298            'Setting content to ' . $this->getRelativePathBelowSiteRoot() . ' failed',
299            FlashMessage::ERROR
300        );
301    }
302
303    /**
304     * Checks if not is a file
305     *
306     * @return bool
307     */
308    protected function isFile()
309    {
310        $path = $this->getAbsolutePath();
311        return !is_link($path) && is_file($path);
312    }
313}
314