Patched to Drupal 8.4.8 level. See https://www.drupal.org/sa-core-2018-004 and patch...
[yaffs-website] / web / core / lib / Drupal / Core / Config / FileStorage.php
1 <?php
2
3 namespace Drupal\Core\Config;
4
5 use Drupal\Component\FileCache\FileCacheFactory;
6 use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
7 use Drupal\Core\Serialization\Yaml;
8
9 /**
10  * Defines the file storage.
11  */
12 class FileStorage implements StorageInterface {
13
14   /**
15    * The storage collection.
16    *
17    * @var string
18    */
19   protected $collection;
20
21   /**
22    * The filesystem path for configuration objects.
23    *
24    * @var string
25    */
26   protected $directory = '';
27
28   /**
29    * The file cache object.
30    *
31    * @var \Drupal\Component\FileCache\FileCacheInterface
32    */
33   protected $fileCache;
34
35   /**
36    * Constructs a new FileStorage.
37    *
38    * @param string $directory
39    *   A directory path to use for reading and writing of configuration files.
40    * @param string $collection
41    *   (optional) The collection to store configuration in. Defaults to the
42    *   default collection.
43    */
44   public function __construct($directory, $collection = StorageInterface::DEFAULT_COLLECTION) {
45     $this->directory = $directory;
46     $this->collection = $collection;
47
48     // Use a NULL File Cache backend by default. This will ensure only the
49     // internal statc caching of FileCache is used and thus avoids blowing up
50     // the APCu cache.
51     $this->fileCache = FileCacheFactory::get('config', ['cache_backend_class' => NULL]);
52   }
53
54   /**
55    * Returns the path to the configuration file.
56    *
57    * @return string
58    *   The path to the configuration file.
59    */
60   public function getFilePath($name) {
61     return $this->getCollectionDirectory() . '/' . $name . '.' . static::getFileExtension();
62   }
63
64   /**
65    * Returns the file extension used by the file storage for all configuration files.
66    *
67    * @return string
68    *   The file extension.
69    */
70   public static function getFileExtension() {
71     return 'yml';
72   }
73
74   /**
75    * Check if the directory exists and create it if not.
76    */
77   protected function ensureStorage() {
78     $dir = $this->getCollectionDirectory();
79     $success = file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
80     // Only create .htaccess file in root directory.
81     if ($dir == $this->directory) {
82       $success = $success && file_save_htaccess($this->directory, TRUE, TRUE);
83     }
84     if (!$success) {
85       throw new StorageException('Failed to create config directory ' . $dir);
86     }
87     return $this;
88   }
89
90   /**
91    * {@inheritdoc}
92    */
93   public function exists($name) {
94     return file_exists($this->getFilePath($name));
95   }
96
97   /**
98    * Implements Drupal\Core\Config\StorageInterface::read().
99    *
100    * @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException
101    */
102   public function read($name) {
103     if (!$this->exists($name)) {
104       return FALSE;
105     }
106
107     $filepath = $this->getFilePath($name);
108     if ($data = $this->fileCache->get($filepath)) {
109       return $data;
110     }
111
112     $data = file_get_contents($filepath);
113     try {
114       $data = $this->decode($data);
115     }
116     catch (InvalidDataTypeException $e) {
117       throw new UnsupportedDataTypeConfigException('Invalid data type in config ' . $name . ', found in file' . $filepath . ' : ' . $e->getMessage());
118     }
119     $this->fileCache->set($filepath, $data);
120
121     return $data;
122   }
123
124   /**
125    * {@inheritdoc}
126    */
127   public function readMultiple(array $names) {
128     $list = [];
129     foreach ($names as $name) {
130       if ($data = $this->read($name)) {
131         $list[$name] = $data;
132       }
133     }
134     return $list;
135   }
136
137   /**
138    * {@inheritdoc}
139    */
140   public function write($name, array $data) {
141     try {
142       $encoded_data = $this->encode($data);
143     }
144     catch (InvalidDataTypeException $e) {
145       throw new StorageException("Invalid data type in config $name: {$e->getMessage()}");
146     }
147
148     $target = $this->getFilePath($name);
149     $status = @file_put_contents($target, $encoded_data);
150     if ($status === FALSE) {
151       // Try to make sure the directory exists and try writing again.
152       $this->ensureStorage();
153       $status = @file_put_contents($target, $encoded_data);
154     }
155     if ($status === FALSE) {
156       throw new StorageException('Failed to write configuration file: ' . $this->getFilePath($name));
157     }
158     else {
159       drupal_chmod($target);
160     }
161
162     $this->fileCache->set($target, $data);
163
164     return TRUE;
165   }
166
167   /**
168    * {@inheritdoc}
169    */
170   public function delete($name) {
171     if (!$this->exists($name)) {
172       $dir = $this->getCollectionDirectory();
173       if (!file_exists($dir)) {
174         throw new StorageException($dir . '/ not found.');
175       }
176       return FALSE;
177     }
178     $this->fileCache->delete($this->getFilePath($name));
179     return drupal_unlink($this->getFilePath($name));
180   }
181
182   /**
183    * {@inheritdoc}
184    */
185   public function rename($name, $new_name) {
186     $status = @rename($this->getFilePath($name), $this->getFilePath($new_name));
187     if ($status === FALSE) {
188       throw new StorageException('Failed to rename configuration file from: ' . $this->getFilePath($name) . ' to: ' . $this->getFilePath($new_name));
189     }
190     $this->fileCache->delete($this->getFilePath($name));
191     $this->fileCache->delete($this->getFilePath($new_name));
192     return TRUE;
193   }
194
195   /**
196    * {@inheritdoc}
197    */
198   public function encode($data) {
199     return Yaml::encode($data);
200   }
201
202   /**
203    * {@inheritdoc}
204    */
205   public function decode($raw) {
206     $data = Yaml::decode($raw);
207     // A simple string is valid YAML for any reason.
208     if (!is_array($data)) {
209       return FALSE;
210     }
211     return $data;
212   }
213
214   /**
215    * {@inheritdoc}
216    */
217   public function listAll($prefix = '') {
218     $dir = $this->getCollectionDirectory();
219     if (!is_dir($dir)) {
220       return [];
221     }
222     $extension = '.' . static::getFileExtension();
223
224     // glob() directly calls into libc glob(), which is not aware of PHP stream
225     // wrappers. Same for \GlobIterator (which additionally requires an absolute
226     // realpath() on Windows).
227     // @see https://github.com/mikey179/vfsStream/issues/2
228     $files = scandir($dir);
229
230     $names = [];
231     $pattern = '/^' . preg_quote($prefix, '/') . '.*' . preg_quote($extension, '/') . '$/';
232     foreach ($files as $file) {
233       if ($file[0] !== '.' && preg_match($pattern, $file)) {
234         $names[] = basename($file, $extension);
235       }
236     }
237
238     return $names;
239   }
240
241   /**
242    * {@inheritdoc}
243    */
244   public function deleteAll($prefix = '') {
245     $success = TRUE;
246     $files = $this->listAll($prefix);
247     foreach ($files as $name) {
248       if (!$this->delete($name) && $success) {
249         $success = FALSE;
250       }
251     }
252     if ($success && $this->collection != StorageInterface::DEFAULT_COLLECTION) {
253       // Remove empty directories.
254       if (!(new \FilesystemIterator($this->getCollectionDirectory()))->valid()) {
255         drupal_rmdir($this->getCollectionDirectory());
256       }
257     }
258     return $success;
259   }
260
261   /**
262    * {@inheritdoc}
263    */
264   public function createCollection($collection) {
265     return new static(
266       $this->directory,
267       $collection
268     );
269   }
270
271   /**
272    * {@inheritdoc}
273    */
274   public function getCollectionName() {
275     return $this->collection;
276   }
277
278   /**
279    * {@inheritdoc}
280    */
281   public function getAllCollectionNames() {
282     $collections = $this->getAllCollectionNamesHelper($this->directory);
283     sort($collections);
284     return $collections;
285   }
286
287   /**
288    * Helper function for getAllCollectionNames().
289    *
290    * If the file storage has the following subdirectory structure:
291    *   ./another_collection/one
292    *   ./another_collection/two
293    *   ./collection/sub/one
294    *   ./collection/sub/two
295    * this function will return:
296    * @code
297    *   array(
298    *     'another_collection.one',
299    *     'another_collection.two',
300    *     'collection.sub.one',
301    *     'collection.sub.two',
302    *   );
303    * @endcode
304    *
305    * @param string $directory
306    *   The directory to check for sub directories. This allows this
307    *   function to be used recursively to discover all the collections in the
308    *   storage.
309    *
310    * @return array
311    *   A list of collection names contained within the provided directory.
312    */
313   protected function getAllCollectionNamesHelper($directory) {
314     $collections = [];
315     $pattern = '/\.' . preg_quote($this->getFileExtension(), '/') . '$/';
316     foreach (new \DirectoryIterator($directory) as $fileinfo) {
317       if ($fileinfo->isDir() && !$fileinfo->isDot()) {
318         $collection = $fileinfo->getFilename();
319         // Recursively call getAllCollectionNamesHelper() to discover if there
320         // are subdirectories. Subdirectories represent a dotted collection
321         // name.
322         $sub_collections = $this->getAllCollectionNamesHelper($directory . '/' . $collection);
323         if (!empty($sub_collections)) {
324           // Build up the collection name by concatenating the subdirectory
325           // names with the current directory name.
326           foreach ($sub_collections as $sub_collection) {
327             $collections[] = $collection . '.' . $sub_collection;
328           }
329         }
330         // Check that the collection is valid by searching it for configuration
331         // objects. A directory without any configuration objects is not a valid
332         // collection.
333         // @see \Drupal\Core\Config\FileStorage::listAll()
334         foreach (scandir($directory . '/' . $collection) as $file) {
335           if ($file[0] !== '.' && preg_match($pattern, $file)) {
336             $collections[] = $collection;
337             break;
338           }
339         }
340       }
341     }
342     return $collections;
343   }
344
345   /**
346    * Gets the directory for the collection.
347    *
348    * @return string
349    *   The directory for the collection.
350    */
351   protected function getCollectionDirectory() {
352     if ($this->collection == StorageInterface::DEFAULT_COLLECTION) {
353       $dir = $this->directory;
354     }
355     else {
356       $dir = $this->directory . '/' . str_replace('.', '/', $this->collection);
357     }
358     return $dir;
359   }
360
361 }