[SECURITY][TASK] Remove support for native PHP yaml extension
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Mvc / Configuration / YamlSource.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Form\Mvc\Configuration;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It originated from the Neos.Form package (www.neos.io)
9 *
10 * It is free software; you can redistribute it and/or modify it under
11 * the terms of the GNU General Public License, either version 2
12 * of the License, or any later version.
13 *
14 * For the full copyright and license information, please read the
15 * LICENSE.txt file that was distributed with this source code.
16 *
17 * The TYPO3 project - inspiring people to share!
18 */
19
20 use Symfony\Component\Yaml\Exception\ParseException;
21 use Symfony\Component\Yaml\Yaml;
22 use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
23 use TYPO3\CMS\Core\Resource\File;
24 use TYPO3\CMS\Core\Resource\FolderInterface;
25 use TYPO3\CMS\Core\Utility\ArrayUtility;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Form\Mvc\Configuration\Exception\FileWriteException;
28 use TYPO3\CMS\Form\Mvc\Configuration\Exception\NoSuchFileException;
29 use TYPO3\CMS\Form\Mvc\Configuration\Exception\ParseErrorException;
30 use TYPO3\CMS\Form\Slot\FilePersistenceSlot;
31
32 /**
33 * Configuration source based on YAML files
34 *
35 * Scope: frontend / backend
36 * @internal
37 */
38 class YamlSource
39 {
40 /**
41 * @var FilePersistenceSlot
42 */
43 protected $filePersistenceSlot;
44
45 /**
46 * @param FilePersistenceSlot $filePersistenceSlot
47 */
48 public function injectFilePersistenceSlot(FilePersistenceSlot $filePersistenceSlot)
49 {
50 $this->filePersistenceSlot = $filePersistenceSlot;
51 }
52
53 /**
54 * Loads the specified configuration files and returns its merged content
55 * as an array.
56 *
57 * @param array $filesToLoad
58 * @return array
59 * @throws ParseErrorException
60 * @throws NoSuchFileException
61 * @internal
62 */
63 public function load(array $filesToLoad): array
64 {
65 $configuration = [];
66 foreach ($filesToLoad as $fileToLoad) {
67 if ($fileToLoad instanceof File) {
68 $fileIdentifier = $fileToLoad->getIdentifier();
69 $rawYamlContent = $fileToLoad->getContents();
70 if ($rawYamlContent === false) {
71 throw new NoSuchFileException(
72 'The file "' . $fileIdentifier . '" does not exist.',
73 1498802253
74 );
75 }
76 } else {
77 $fileIdentifier = $fileToLoad;
78 $fileToLoad = GeneralUtility::getFileAbsFileName($fileToLoad);
79 if (is_file($fileToLoad)) {
80 $rawYamlContent = file_get_contents($fileToLoad);
81 } else {
82 throw new NoSuchFileException(
83 'The file "' . $fileToLoad . '" does not exist.',
84 1471473378
85 );
86 }
87 }
88
89 try {
90 $loadedConfiguration = Yaml::parse($rawYamlContent);
91
92 if (is_array($loadedConfiguration)) {
93 $configuration = array_replace_recursive($configuration, $loadedConfiguration);
94 }
95 } catch (ParseException $exception) {
96 throw new ParseErrorException(
97 'An error occurred while parsing file "' . $fileIdentifier . '": ' . $exception->getMessage(),
98 1480195405
99 );
100 }
101 }
102
103 $configuration = ArrayUtility::convertBooleanStringsToBooleanRecursive($configuration);
104 return $configuration;
105 }
106
107 /**
108 * Save the specified configuration array to the given file in YAML format.
109 *
110 * @param File|string $fileToSave The file to write to.
111 * @param array $configuration The configuration to save
112 * @throws FileWriteException if the file could not be written
113 * @internal
114 */
115 public function save($fileToSave, array $configuration)
116 {
117 try {
118 $header = $this->getHeaderFromFile($fileToSave);
119 } catch (InsufficientFileAccessPermissionsException $e) {
120 throw new FileWriteException($e->getMessage(), 1512584488, $e);
121 }
122
123 $yaml = Yaml::dump($configuration, 99, 2);
124
125 if ($fileToSave instanceof File) {
126 try {
127 $this->filePersistenceSlot->allowInvocation(
128 FilePersistenceSlot::COMMAND_FILE_SET_CONTENTS,
129 $this->buildCombinedIdentifier(
130 $fileToSave->getParentFolder(),
131 $fileToSave->getName()
132 ),
133 $this->filePersistenceSlot->getContentSignature(
134 $header . LF . $yaml
135 )
136 );
137 $fileToSave->setContents($header . LF . $yaml);
138 } catch (InsufficientFileAccessPermissionsException $e) {
139 throw new FileWriteException($e->getMessage(), 1512582753, $e);
140 }
141 } else {
142 $byteCount = @file_put_contents($fileToSave, $header . LF . $yaml);
143
144 if ($byteCount === false) {
145 $error = error_get_last();
146 throw new FileWriteException($error['message'], 1512582929);
147 }
148 }
149
150 return $return;
151 }
152
153 /**
154 * Read the header part from the given file. That means, every line
155 * until the first non comment line is found.
156 *
157 * @param File|string $file
158 * @return string The header of the given YAML file
159 */
160 protected function getHeaderFromFile($file): string
161 {
162 $header = '';
163 if ($file instanceof File) {
164 $fileLines = explode(LF, $file->getContents());
165 } elseif (is_file($file)) {
166 $fileLines = file($file);
167 } else {
168 return '';
169 }
170
171 foreach ($fileLines as $line) {
172 if (preg_match('/^#/', $line)) {
173 $header .= $line;
174 } else {
175 break;
176 }
177 }
178 return $header;
179 }
180
181 /**
182 * @param FolderInterface $folder
183 * @param string $fileName
184 * @return string
185 */
186 protected function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string
187 {
188 return sprintf(
189 '%d:%s%s',
190 $folder->getStorage()->getUid(),
191 $folder->getIdentifier(),
192 $fileName
193 );
194 }
195 }