1<?php
2namespace TYPO3\CMS\Install\FolderStructure;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Core\Messaging\FlashMessage;
18use TYPO3\CMS\Core\Utility\StringUtility;
19
20/**
21 * A directory
22 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
23 */
24class DirectoryNode extends AbstractNode implements NodeInterface
25{
26    /**
27     * @var int|null Default for directories is octal 02775 == decimal 1533
28     */
29    protected $targetPermission = '2775';
30
31    /**
32     * Implement constructor
33     *
34     * @param array $structure Structure array
35     * @param NodeInterface $parent Parent object
36     * @throws Exception\InvalidArgumentException
37     */
38    public function __construct(array $structure, NodeInterface $parent = null)
39    {
40        if ($parent === null) {
41            throw new Exception\InvalidArgumentException(
42                'Node must have parent',
43                1366222203
44            );
45        }
46        $this->parent = $parent;
47
48        // Ensure name is a single segment, but not a path like foo/bar or an absolute path /foo
49        if (strstr($structure['name'], '/') !== false) {
50            throw new Exception\InvalidArgumentException(
51                'Directory name must not contain forward slash',
52                1366226639
53            );
54        }
55        $this->name = $structure['name'];
56
57        if (isset($structure['targetPermission'])) {
58            $this->setTargetPermission($structure['targetPermission']);
59        }
60
61        if (array_key_exists('children', $structure)) {
62            $this->createChildren($structure['children']);
63        }
64    }
65
66    /**
67     * Get own status and status of child objects
68     *
69     * @return FlashMessage[]
70     */
71    public function getStatus(): array
72    {
73        $result = [];
74        if (!$this->exists()) {
75            $status = new FlashMessage(
76                'The Install Tool can try to create it',
77                'Directory ' . $this->getRelativePathBelowSiteRoot() . ' does not exist',
78                FlashMessage::WARNING
79            );
80            $result[] = $status;
81        } else {
82            $result = $this->getSelfStatus();
83        }
84        $result = array_merge($result, $this->getChildrenStatus());
85        return $result;
86    }
87
88    /**
89     * Create a test file and delete again if directory exists
90     *
91     * @return bool TRUE if test file creation was successful
92     */
93    public function isWritable()
94    {
95        $result = true;
96        if (!$this->exists()) {
97            $result = false;
98        } elseif (!$this->canFileBeCreated()) {
99            $result = false;
100        }
101        return $result;
102    }
103
104    /**
105     * Fix structure
106     *
107     * If there is nothing to fix, returns an empty array
108     *
109     * @return FlashMessage[]
110     */
111    public function fix(): array
112    {
113        $result = $this->fixSelf();
114        foreach ($this->children as $child) {
115            /** @var NodeInterface $child */
116            $result = array_merge($result, $child->fix());
117        }
118        return $result;
119    }
120
121    /**
122     * Fix this directory:
123     *
124     * - create with correct permissions if it was not existing
125     * - if there is no "write" permissions, try to fix it
126     * - leave it alone otherwise
127     *
128     * @return FlashMessage[]
129     */
130    protected function fixSelf()
131    {
132        $result = [];
133        if (!$this->exists()) {
134            $resultCreateDirectory = $this->createDirectory();
135            $result[] = $resultCreateDirectory;
136            if ($resultCreateDirectory->getSeverity() === FlashMessage::OK &&
137                !$this->isPermissionCorrect()
138            ) {
139                $result[] = $this->fixPermission();
140            }
141        } elseif (!$this->isWritable()) {
142            // If directory is not writable, we might have permissions to fix that
143            // Try it:
144            $result[] = $this->fixPermission();
145        } elseif (!$this->isDirectory()) {
146            $fileType = @filetype($this->getAbsolutePath());
147            if ($fileType) {
148                $messageBody =
149                    'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a directory,' .
150                    ' but is of type ' . $fileType . '. This cannot be fixed automatically. Please investigate.'
151                ;
152            } else {
153                $messageBody =
154                    'The target ' . $this->getRelativePathBelowSiteRoot() . ' should be a directory,' .
155                    ' but is of unknown type, probably because an upper level directory does not exist. Please investigate.'
156                ;
157            }
158            $result[] = new FlashMessage(
159                $messageBody,
160                'Path ' . $this->getRelativePathBelowSiteRoot() . ' is not a directory',
161                FlashMessage::ERROR
162            );
163        }
164        return $result;
165    }
166
167    /**
168     * Create directory if not exists
169     *
170     * @throws Exception
171     * @return FlashMessage
172     */
173    protected function createDirectory(): FlashMessage
174    {
175        if ($this->exists()) {
176            throw new Exception(
177                'Directory ' . $this->getAbsolutePath() . ' already exists',
178                1366740091
179            );
180        }
181        $result = @mkdir($this->getAbsolutePath());
182        if ($result === true) {
183            return new FlashMessage(
184                '',
185                'Directory ' . $this->getRelativePathBelowSiteRoot() . ' successfully created.'
186            );
187        }
188        return new FlashMessage(
189            'The target directory could not be created. There is probably a'
190                . ' group or owner permission problem on the parent directory.',
191            'Directory ' . $this->getRelativePathBelowSiteRoot() . ' not created!',
192            FlashMessage::ERROR
193        );
194    }
195
196    /**
197     * Get status of directory - used in root and directory node
198     *
199     * @return FlashMessage[]
200     */
201    protected function getSelfStatus(): array
202    {
203        $result = [];
204        if (!$this->isDirectory()) {
205            $result[] = new FlashMessage(
206                'Directory ' . $this->getRelativePathBelowSiteRoot() . ' should be a directory,'
207                    . ' but is of type ' . filetype($this->getAbsolutePath()),
208                $this->getRelativePathBelowSiteRoot() . ' is not a directory',
209                FlashMessage::ERROR
210            );
211        } elseif (!$this->isWritable()) {
212            $result[] = new FlashMessage(
213                'Path ' . $this->getAbsolutePath() . ' exists, but no file underneath it'
214                    . ' can be created.',
215                'Directory ' . $this->getRelativePathBelowSiteRoot() . ' is not writable',
216                FlashMessage::ERROR
217            );
218        } elseif (!$this->isPermissionCorrect()) {
219            $result[] = new FlashMessage(
220                'Default configured permissions are ' . $this->getTargetPermission()
221                    . ' but current permissions are ' . $this->getCurrentPermission(),
222                'Directory ' . $this->getRelativePathBelowSiteRoot() . ' permissions mismatch',
223                FlashMessage::NOTICE
224            );
225        } else {
226            $result[] = new FlashMessage(
227                'Is a directory with the configured permissions of ' . $this->getTargetPermission(),
228                'Directory ' . $this->getRelativePathBelowSiteRoot()
229            );
230        }
231        return $result;
232    }
233
234    /**
235     * Get status of children
236     *
237     * @return FlashMessage[]
238     */
239    protected function getChildrenStatus(): array
240    {
241        $result = [];
242        foreach ($this->children as $child) {
243            /** @var NodeInterface $child */
244            $result = array_merge($result, $child->getStatus());
245        }
246        return $result;
247    }
248
249    /**
250     * Create a test file and delete again - helper for isWritable
251     *
252     * @return bool TRUE if test file creation was successful
253     */
254    protected function canFileBeCreated()
255    {
256        $testFileName = StringUtility::getUniqueId('installToolTest_');
257        $result = @touch($this->getAbsolutePath() . '/' . $testFileName);
258        if ($result === true) {
259            unlink($this->getAbsolutePath() . '/' . $testFileName);
260        }
261        return $result;
262    }
263
264    /**
265     * Checks if not is a directory
266     *
267     * @return bool True if node is a directory
268     */
269    protected function isDirectory()
270    {
271        $path = $this->getAbsolutePath();
272        return !@is_link($path) && @is_dir($path);
273    }
274
275    /**
276     * Create children nodes - done in directory and root node
277     *
278     * @param array $structure Array of children
279     * @throws Exception\InvalidArgumentException
280     */
281    protected function createChildren(array $structure)
282    {
283        foreach ($structure as $child) {
284            if (!array_key_exists('type', $child)) {
285                throw new Exception\InvalidArgumentException(
286                    'Child must have type',
287                    1366222204
288                );
289            }
290            if (!array_key_exists('name', $child)) {
291                throw new Exception\InvalidArgumentException(
292                    'Child must have name',
293                    1366222205
294                );
295            }
296            $name = $child['name'];
297            foreach ($this->children as $existingChild) {
298                /** @var NodeInterface $existingChild */
299                if ($existingChild->getName() === $name) {
300                    throw new Exception\InvalidArgumentException(
301                        'Child name must be unique',
302                        1366222206
303                    );
304                }
305            }
306            $this->children[] = new $child['type']($child, $this);
307        }
308    }
309}
310