Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / Extension / ExtensionList.php
1 <?php
2
3 namespace Drupal\Core\Extension;
4
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Database\DatabaseExceptionWrapper;
7 use Drupal\Core\Extension\Exception\UnknownExtensionException;
8 use Drupal\Core\State\StateInterface;
9
10 /**
11  * Provides available extensions.
12  *
13  * The extension list is per extension type, like module, theme and profile.
14  */
15 abstract class ExtensionList {
16
17   /**
18    * The type of the extension: "module", "theme" or "profile".
19    *
20    * @var string
21    */
22   protected $type;
23
24   /**
25    * The app root.
26    *
27    * @var string
28    */
29   protected $root;
30
31   /**
32    * The cache.
33    *
34    * @var \Drupal\Core\Cache\CacheBackendInterface
35    */
36   protected $cache;
37
38   /**
39    * Default values to be merged into *.info.yml file arrays.
40    *
41    * @var mixed[]
42    */
43   protected $defaults = [];
44
45   /**
46    * The info parser.
47    *
48    * @var \Drupal\Core\Extension\InfoParserInterface
49    */
50   protected $infoParser;
51
52   /**
53    * The module handler.
54    *
55    * @var \Drupal\Core\Extension\ModuleHandlerInterface
56    */
57   protected $moduleHandler;
58
59   /**
60    * The cached extensions.
61    *
62    * @var \Drupal\Core\Extension\Extension[]|null
63    */
64   protected $extensions;
65
66   /**
67    * Static caching for extension info.
68    *
69    * Access this property's value through static::getAllInfo().
70    *
71    * @var array[]|null
72    *   Keys are extension names, and values their info arrays (mixed[]).
73    *
74    * @see \Drupal\Core\Extension\ExtensionList::getAllAvailableInfo
75    */
76   protected $extensionInfo;
77
78   /**
79    * A list of extension folder names keyed by extension name.
80    *
81    * @var string[]|null
82    */
83   protected $pathNames;
84
85   /**
86    * A list of extension folder names directly added in code (not discovered).
87    *
88    * It is important to keep a separate list to ensure that it takes priority
89    * over the discovered extension folders.
90    *
91    * @var string[]
92    *
93    * @internal
94    */
95   protected $addedPathNames = [];
96
97   /**
98    * The state store.
99    *
100    * @var \Drupal\Core\State\StateInterface
101    */
102   protected $state;
103
104   /**
105    * The install profile used by the site.
106    *
107    * @var string
108    */
109   protected $installProfile;
110
111   /**
112    * Constructs a new instance.
113    *
114    * @param string $root
115    *   The app root.
116    * @param string $type
117    *   The extension type.
118    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
119    *   The cache.
120    * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
121    *   The info parser.
122    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
123    *   The module handler.
124    * @param \Drupal\Core\State\StateInterface $state
125    *   The state.
126    * @param string $install_profile
127    *   The install profile used by the site.
128    */
129   public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, $install_profile) {
130     $this->root = $root;
131     $this->type = $type;
132     $this->cache = $cache;
133     $this->infoParser = $info_parser;
134     $this->moduleHandler = $module_handler;
135     $this->state = $state;
136     $this->installProfile = $install_profile;
137   }
138
139   /**
140    * Returns the extension discovery.
141    *
142    * @return \Drupal\Core\Extension\ExtensionDiscovery
143    */
144   protected function getExtensionDiscovery() {
145     return new ExtensionDiscovery($this->root);
146   }
147
148   /**
149    * Resets the stored extension list.
150    *
151    * We don't reset statically added filenames, as it is a static cache which
152    * logically can't change. This is done for performance reasons of the
153    * installer.
154    */
155   public function reset() {
156     $this->extensions = NULL;
157     $this->cache->delete($this->getListCacheId());
158     $this->extensionInfo = NULL;
159     $this->cache->delete($this->getInfoCacheId());
160     $this->pathNames = NULL;
161
162     try {
163       $this->state->delete($this->getPathnamesCacheId());
164     }
165     catch (DatabaseExceptionWrapper $e) {
166       // Ignore exceptions caused by a non existing {key_value} table in the
167       // early installer.
168     }
169
170     $this->cache->delete($this->getPathnamesCacheId());
171     // @todo In the long run it would be great to add the reset, but the early
172     //   installer fails due to that. https://www.drupal.org/node/2719315 could
173     //   help to resolve with that.
174     return $this;
175   }
176
177   /**
178    * Returns the extension list cache ID.
179    *
180    * @return string
181    *   The list cache ID.
182    */
183   protected function getListCacheId() {
184     return 'core.extension.list.' . $this->type;
185   }
186
187   /**
188    * Returns the extension info cache ID.
189    *
190    * @return string
191    *   The info cache ID.
192    */
193   protected function getInfoCacheId() {
194     return "system.{$this->type}.info";
195   }
196
197   /**
198    * Returns the extension filenames cache ID.
199    *
200    * @return string
201    *   The filename cache ID.
202    */
203   protected function getPathnamesCacheId() {
204     return "system.{$this->type}.files";
205   }
206
207   /**
208    * Determines if an extension exists in the filesystem.
209    *
210    * @param string $extension_name
211    *   The machine name of the extension.
212    *
213    * @return bool
214    *   TRUE if the extension exists (regardless installed or not) and FALSE if
215    *   not.
216    */
217   public function exists($extension_name) {
218     $extensions = $this->getList();
219     return isset($extensions[$extension_name]);
220   }
221
222   /**
223    * Returns the human-readable name of the extension.
224    *
225    * @param string $extension_name
226    *   The machine name of the extension.
227    *
228    * @return string
229    *   The human-readable name of the extension.
230    *
231    * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
232    *   If there is no extension with the supplied machine name.
233    */
234   public function getName($extension_name) {
235     return $this->get($extension_name)->info['name'];
236   }
237
238   /**
239    * Returns a single extension.
240    *
241    * @param string $extension_name
242    *   The machine name of the extension.
243    *
244    * @return \Drupal\Core\Extension\Extension
245    *   A processed extension object for the extension with the specified machine
246    *   name.
247    *
248    * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
249    *   If there is no extension with the supplied name.
250    */
251   public function get($extension_name) {
252     $extensions = $this->getList();
253     if (isset($extensions[$extension_name])) {
254       return $extensions[$extension_name];
255     }
256
257     throw new UnknownExtensionException("The {$this->type} $extension_name does not exist.");
258   }
259
260   /**
261    * Returns all available extensions.
262    *
263    * @return \Drupal\Core\Extension\Extension[]
264    *   Processed extension objects, keyed by machine name.
265    */
266   public function getList() {
267     if ($this->extensions !== NULL) {
268       return $this->extensions;
269     }
270     if ($cache = $this->cache->get($this->getListCacheId())) {
271       $this->extensions = $cache->data;
272       return $this->extensions;
273     }
274     $extensions = $this->doList();
275     $this->cache->set($this->getListCacheId(), $extensions);
276     $this->extensions = $extensions;
277     return $this->extensions;
278   }
279
280   /**
281    * Scans the available extensions.
282    *
283    * Overriding this method gives other code the chance to add additional
284    * extensions to this raw listing.
285    *
286    * @return \Drupal\Core\Extension\Extension[]
287    *   Unprocessed extension objects, keyed by machine name.
288    */
289   protected function doScanExtensions() {
290     return $this->getExtensionDiscovery()->scan($this->type);
291   }
292
293   /**
294    * Builds the list of extensions.
295    *
296    * @return \Drupal\Core\Extension\Extension[]
297    *   Processed extension objects, keyed by machine name.
298    *
299    * @throws \Drupal\Core\Extension\InfoParserException
300    *   If one of the .info.yml files is incomplete, or causes a parsing error.
301    */
302   protected function doList() {
303     // Find extensions.
304     $extensions = $this->doScanExtensions();
305
306     // Read info files for each extension.
307     foreach ($extensions as $extension_name => $extension) {
308       // Look for the info file.
309       $extension->info = $this->infoParser->parse($extension->getPathname());
310
311       // Add the info file modification time, so it becomes available for
312       // contributed extensions to use for ordering extension lists.
313       $extension->info['mtime'] = $extension->getMTime();
314
315       // Merge extension type-specific defaults.
316       $extension->info += $this->defaults;
317
318       // Invoke hook_system_info_alter() to give installed modules a chance to
319       // modify the data in the .info.yml files if necessary.
320       $this->moduleHandler->alter('system_info', $extension->info, $extension, $this->type);
321     }
322
323     return $extensions;
324   }
325
326   /**
327    * Returns information about a specified extension.
328    *
329    * This function returns the contents of the .info.yml file for the specified
330    * extension.
331    *
332    * @param string $extension_name
333    *   The name of an extension whose information shall be returned.
334    *
335    * @return mixed[]
336    *   An associative array of extension information.
337    *
338    * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
339    *   If there is no extension with the supplied name.
340    */
341   public function getExtensionInfo($extension_name) {
342     $all_info = $this->getAllInstalledInfo();
343     if (isset($all_info[$extension_name])) {
344       return $all_info[$extension_name];
345     }
346     throw new UnknownExtensionException("The {$this->type} $extension_name does not exist or is not installed.");
347   }
348
349   /**
350    * Returns an array of info files information of available extensions.
351    *
352    * This function returns the processed contents (with added defaults) of the
353    * .info.yml files.
354    *
355    * @return array[]
356    *   An associative array of extension information arrays, keyed by extension
357    *   name.
358    */
359   public function getAllAvailableInfo() {
360     if ($this->extensionInfo === NULL) {
361       $cache_id = $this->getInfoCacheId();
362       if ($cache = $this->cache->get($cache_id)) {
363         $info = $cache->data;
364       }
365       else {
366         $info = $this->recalculateInfo();
367         $this->cache->set($cache_id, $info);
368       }
369       $this->extensionInfo = $info;
370     }
371
372     return $this->extensionInfo;
373   }
374
375   /**
376    * Returns a list of machine names of installed extensions.
377    *
378    * @return string[]
379    *   The machine names of all installed extensions of this type.
380    */
381   abstract protected function getInstalledExtensionNames();
382
383   /**
384    * Returns an array of info files information of installed extensions.
385    *
386    * This function returns the processed contents (with added defaults) of the
387    * .info.yml files.
388    *
389    * @return array[]
390    *   An associative array of extension information arrays, keyed by extension
391    *   name.
392    */
393   public function getAllInstalledInfo() {
394     return array_intersect_key($this->getAllAvailableInfo(), array_flip($this->getInstalledExtensionNames()));
395   }
396
397   /**
398    * Generates the information from .info.yml files for extensions of this type.
399    *
400    * @return array[]
401    *   An array of arrays of .info.yml entries keyed by the machine name.
402    */
403   protected function recalculateInfo() {
404     return array_map(function (Extension $extension) {
405       return $extension->info;
406     }, $this->getList());
407   }
408
409   /**
410    * Returns a list of extension file paths keyed by machine name.
411    *
412    * @return string[]
413    */
414   public function getPathnames() {
415     if ($this->pathNames === NULL) {
416       $cache_id = $this->getPathnamesCacheId();
417       if ($cache = $this->cache->get($cache_id)) {
418         $path_names = $cache->data;
419       }
420       // We use $file_names below.
421       elseif (!$path_names = $this->state->get($cache_id)) {
422         $path_names = $this->recalculatePathnames();
423         // Store filenames to allow static::getPathname() to retrieve them
424         // without having to rebuild or scan the filesystem.
425         $this->state->set($cache_id, $path_names);
426         $this->cache->set($cache_id, $path_names);
427       }
428       $this->pathNames = $path_names;
429     }
430     return $this->pathNames;
431   }
432
433   /**
434    * Generates a sorted list of .info.yml file locations for all extensions.
435    *
436    * @return string[]
437    *   An array of .info.yml file locations keyed by the extension machine name.
438    */
439   protected function recalculatePathnames() {
440     $extensions = $this->getList();
441     ksort($extensions);
442
443     return array_map(function (Extension $extension) {
444       return $extension->getPathname();
445     }, $extensions);
446   }
447
448   /**
449    * Sets the pathname for an extension.
450    *
451    * This method is used in the Drupal bootstrapping phase, when the extension
452    * system is not fully initialized, to manually set locations of modules and
453    * profiles needed to complete bootstrapping.
454    *
455    * It is not recommended to call this method except in those rare cases.
456    *
457    * @param string $extension_name
458    *   The machine name of the extension.
459    * @param string $pathname
460    *   The pathname of the extension which is to be set explicitly rather
461    *   than by consulting the dynamic extension listing.
462    *
463    * @internal
464    *
465    * @see ::getPathname
466    */
467   public function setPathname($extension_name, $pathname) {
468     $this->addedPathNames[$extension_name] = $pathname;
469   }
470
471   /**
472    * Gets the info file path for an extension.
473    *
474    * The info path, whether provided, cached, or retrieved from the database, is
475    * only returned if the file exists.
476    *
477    * This function plays a key role in allowing Drupal's extensions (modules,
478    * themes, profiles, theme_engines, etc.) to be located in different places
479    * depending on a site's configuration. For example, a module 'foo' may
480    * legally be located in any of these four places:
481    *
482    * - core/modules/foo/foo.info.yml
483    * - modules/foo/foo.info.yml
484    * - sites/all/modules/foo/foo.info.yml
485    * - sites/example.com/modules/foo/foo.info.yml
486    *
487    * while a theme 'bar' may be located in any of the following four places:
488    *
489    * - core/themes/bar/bar.info.yml
490    * - themes/bar/bar.info.yml
491    * - sites/all/themes/bar/bar.info.yml
492    * - sites/example.com/themes/bar/bar.info.yml
493    *
494    * An installation profile maybe be located in any of the following places:
495    *
496    * - core/profiles/baz/baz.info.yml
497    * - profiles/baz/baz.info.yml
498    *
499    * Calling ExtensionList::getPathname('foo') will give you one of the above,
500    * depending on where the extension is located and what type it is.
501    *
502    * @param string $extension_name
503    *   The machine name of the extension for which the pathname is requested.
504    *
505    * @return string
506    *   The drupal-root relative filename and path of the requested extension's
507    *   .info.yml file.
508    *
509    * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
510    *   If there is no extension with the supplied machine name.
511    */
512   public function getPathname($extension_name) {
513     if (isset($this->addedPathNames[$extension_name])) {
514       return $this->addedPathNames[$extension_name];
515     }
516     elseif (isset($this->pathNames[$extension_name])) {
517       return $this->pathNames[$extension_name];
518     }
519     elseif (($path_names = $this->getPathnames()) && isset($path_names[$extension_name])) {
520       return $path_names[$extension_name];
521     }
522     throw new UnknownExtensionException("The {$this->type} $extension_name does not exist.");
523   }
524
525   /**
526    * Gets the path to an extension of a specific type (module, theme, etc.).
527    *
528    * The path is the directory in which the .info file is located. This name is
529    * coming from \SplFileInfo.
530    *
531    * @param string $extension_name
532    *   The machine name of the extension for which the path is requested.
533    *
534    * @return string
535    *   The Drupal-root-relative path to the specified extension.
536    *
537    * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
538    *   If there is no extension with the supplied name.
539    */
540   public function getPath($extension_name) {
541     return dirname($this->getPathname($extension_name));
542   }
543
544 }