Pull merge.
[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 static 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     if (!is_dir($this->directory)) {
283       return [];
284     }
285     $collections = $this->getAllCollectionNamesHelper($this->directory);
286     sort($collections);
287     return $collections;
288   }
289
290   /**
291    * Helper function for getAllCollectionNames().
292    *
293    * If the file storage has the following subdirectory structure:
294    *   ./another_collection/one
295    *   ./another_collection/two
296    *   ./collection/sub/one
297    *   ./collection/sub/two
298    * this function will return:
299    * @code
300    *   array(
301    *     'another_collection.one',
302    *     'another_collection.two',
303    *     'collection.sub.one',
304    *     'collection.sub.two',
305    *   );
306    * @endcode
307    *
308    * @param string $directory
309    *   The directory to check for sub directories. This allows this
310    *   function to be used recursively to discover all the collections in the
311    *   storage. It is the responsibility of the caller to ensure the directory
312    *   exists.
313    *
314    * @return array
315    *   A list of collection names contained within the provided directory.
316    */
317   protected function getAllCollectionNamesHelper($directory) {
318     $collections = [];
319     $pattern = '/\.' . preg_quote($this->getFileExtension(), '/') . '$/';
320     foreach (new \DirectoryIterator($directory) as $fileinfo) {
321       if ($fileinfo->isDir() && !$fileinfo->isDot()) {
322         $collection = $fileinfo->getFilename();
323         // Recursively call getAllCollectionNamesHelper() to discover if there
324         // are subdirectories. Subdirectories represent a dotted collection
325         // name.
326         $sub_collections = $this->getAllCollectionNamesHelper($directory . '/' . $collection);
327         if (!empty($sub_collections)) {
328           // Build up the collection name by concatenating the subdirectory
329           // names with the current directory name.
330           foreach ($sub_collections as $sub_collection) {
331             $collections[] = $collection . '.' . $sub_collection;
332           }
333         }
334         // Check that the collection is valid by searching it for configuration
335         // objects. A directory without any configuration objects is not a valid
336         // collection.
337         // @see \Drupal\Core\Config\FileStorage::listAll()
338         foreach (scandir($directory . '/' . $collection) as $file) {
339           if ($file[0] !== '.' && preg_match($pattern, $file)) {
340             $collections[] = $collection;
341             break;
342           }
343         }
344       }
345     }
346     return $collections;
347   }
348
349   /**
350    * Gets the directory for the collection.
351    *
352    * @return string
353    *   The directory for the collection.
354    */
355   protected function getCollectionDirectory() {
356     if ($this->collection == StorageInterface::DEFAULT_COLLECTION) {
357       $dir = $this->directory;
358     }
359     else {
360       $dir = $this->directory . '/' . str_replace('.', '/', $this->collection);
361     }
362     return $dir;
363   }
364
365 }