9105fcfc1618c5a7c647c82d2b0ba44686930e75
[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 * Will be set if the PHP YAML Extension is installed.
42 * Having this installed massively improves YAML parsing performance.
43 *
44 * @var bool
45 * @see http://pecl.php.net/package/yaml
46 */
47 protected $usePhpYamlExtension = false;
48
49 /**
50 * @var FilePersistenceSlot
51 */
52 protected $filePersistenceSlot;
53
54 /**
55 * Use PHP YAML Extension if installed.
56 * @internal
57 */
58 public function __construct()
59 {
60 if (extension_loaded('yaml')) {
61 $this->usePhpYamlExtension = true;
62 }
63 }
64
65 /**
66 * @param FilePersistenceSlot $filePersistenceSlot
67 */
68 public function injectFilePersistenceSlot(FilePersistenceSlot $filePersistenceSlot)
69 {
70 $this->filePersistenceSlot = $filePersistenceSlot;
71 }
72
73 /**
74 * Loads the specified configuration files and returns its merged content
75 * as an array.
76 *
77 * @param array $filesToLoad
78 * @return array
79 * @throws ParseErrorException
80 * @throws NoSuchFileException
81 * @internal
82 */
83 public function load(array $filesToLoad): array
84 {
85 $configuration = [];
86 foreach ($filesToLoad as $fileToLoad) {
87 if ($fileToLoad instanceof File) {
88 $fileIdentifier = $fileToLoad->getIdentifier();
89 $rawYamlContent = $fileToLoad->getContents();
90 if ($rawYamlContent === false) {
91 throw new NoSuchFileException(
92 'The file "' . $fileIdentifier . '" does not exist.',
93 1498802253
94 );
95 }
96 } else {
97 $fileIdentifier = $fileToLoad;
98 $fileToLoad = GeneralUtility::getFileAbsFileName($fileToLoad);
99 if (is_file($fileToLoad)) {
100 $rawYamlContent = file_get_contents($fileToLoad);
101 } else {
102 throw new NoSuchFileException(
103 'The file "' . $fileToLoad . '" does not exist.',
104 1471473378
105 );
106 }
107 }
108
109 try {
110 if ($this->usePhpYamlExtension) {
111 $loadedConfiguration = @yaml_parse($rawYamlContent);
112 if ($loadedConfiguration === false) {
113 throw new ParseErrorException(
114 'A parse error occurred while parsing file "' . $fileIdentifier . '".',
115 1391894094
116 );
117 }
118 } else {
119 $loadedConfiguration = Yaml::parse($rawYamlContent);
120 }
121
122 if (is_array($loadedConfiguration)) {
123 $configuration = array_replace_recursive($configuration, $loadedConfiguration);
124 }
125 } catch (ParseException $exception) {
126 throw new ParseErrorException(
127 'An error occurred while parsing file "' . $fileIdentifier . '": ' . $exception->getMessage(),
128 1480195405
129 );
130 }
131 }
132
133 $configuration = ArrayUtility::convertBooleanStringsToBooleanRecursive($configuration);
134 return $configuration;
135 }
136
137 /**
138 * Save the specified configuration array to the given file in YAML format.
139 *
140 * @param File|string $fileToSave The file to write to.
141 * @param array $configuration The configuration to save
142 * @throws FileWriteException if the file could not be written
143 * @internal
144 */
145 public function save($fileToSave, array $configuration)
146 {
147 try {
148 $header = $this->getHeaderFromFile($fileToSave);
149 } catch (InsufficientFileAccessPermissionsException $e) {
150 throw new FileWriteException($e->getMessage(), 1512584488, $e);
151 }
152
153 $yaml = Yaml::dump($configuration, 99, 2);
154
155 if ($fileToSave instanceof File) {
156 try {
157 $this->filePersistenceSlot->allowInvocation(
158 FilePersistenceSlot::COMMAND_FILE_SET_CONTENTS,
159 $this->buildCombinedIdentifier(
160 $fileToSave->getParentFolder(),
161 $fileToSave->getName()
162 ),
163 $this->filePersistenceSlot->getContentSignature(
164 $header . LF . $yaml
165 )
166 );
167 $fileToSave->setContents($header . LF . $yaml);
168 } catch (InsufficientFileAccessPermissionsException $e) {
169 throw new FileWriteException($e->getMessage(), 1512582753, $e);
170 }
171 } else {
172 $byteCount = @file_put_contents($fileToSave, $header . LF . $yaml);
173
174 if ($byteCount === false) {
175 $error = error_get_last();
176 throw new FileWriteException($error['message'], 1512582929);
177 }
178 }
179
180 return $return;
181 }
182
183 /**
184 * Read the header part from the given file. That means, every line
185 * until the first non comment line is found.
186 *
187 * @param File|string $file
188 * @return string The header of the given YAML file
189 */
190 protected function getHeaderFromFile($file): string
191 {
192 $header = '';
193 if ($file instanceof File) {
194 $fileLines = explode(LF, $file->getContents());
195 } elseif (is_file($file)) {
196 $fileLines = file($file);
197 } else {
198 return '';
199 }
200
201 foreach ($fileLines as $line) {
202 if (preg_match('/^#/', $line)) {
203 $header .= $line;
204 } else {
205 break;
206 }
207 }
208 return $header;
209 }
210
211 /**
212 * @param FolderInterface $folder
213 * @param string $fileName
214 * @return string
215 */
216 protected function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string
217 {
218 return sprintf(
219 '%d:%s%s',
220 $folder->getStorage()->getUid(),
221 $folder->getIdentifier(),
222 $fileName
223 );
224 }
225 }