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