Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Extension / ModuleHandler.php
1 <?php
2
3 namespace Drupal\Core\Extension;
4
5 use Drupal\Component\Graph\Graph;
6 use Drupal\Component\Utility\NestedArray;
7 use Drupal\Core\Cache\CacheBackendInterface;
8
9 /**
10  * Class that manages modules in a Drupal installation.
11  */
12 class ModuleHandler implements ModuleHandlerInterface {
13
14   /**
15    * List of loaded files.
16    *
17    * @var array
18    *   An associative array whose keys are file paths of loaded files, relative
19    *   to the application's root directory.
20    */
21   protected $loadedFiles;
22
23   /**
24    * List of installed modules.
25    *
26    * @var \Drupal\Core\Extension\Extension[]
27    */
28   protected $moduleList;
29
30   /**
31    * Boolean indicating whether modules have been loaded.
32    *
33    * @var bool
34    */
35   protected $loaded = FALSE;
36
37   /**
38    * List of hook implementations keyed by hook name.
39    *
40    * @var array
41    */
42   protected $implementations;
43
44   /**
45    * List of hooks where the implementations have been "verified".
46    *
47    * @var true[]
48    *   Associative array where keys are hook names.
49    */
50   protected $verified;
51
52   /**
53    * Information returned by hook_hook_info() implementations.
54    *
55    * @var array
56    */
57   protected $hookInfo;
58
59   /**
60    * Cache backend for storing module hook implementation information.
61    *
62    * @var \Drupal\Core\Cache\CacheBackendInterface
63    */
64   protected $cacheBackend;
65
66   /**
67    * Whether the cache needs to be written.
68    *
69    * @var bool
70    */
71   protected $cacheNeedsWriting = FALSE;
72
73   /**
74    * List of alter hook implementations keyed by hook name(s).
75    *
76    * @var array
77    */
78   protected $alterFunctions;
79
80   /**
81    * The app root.
82    *
83    * @var string
84    */
85   protected $root;
86
87   /**
88    * A list of module include file keys.
89    *
90    * @var array
91    */
92   protected $includeFileKeys = [];
93
94   /**
95    * Constructs a ModuleHandler object.
96    *
97    * @param string $root
98    *   The app root.
99    * @param array $module_list
100    *   An associative array whose keys are the names of installed modules and
101    *   whose values are Extension class parameters. This is normally the
102    *   %container.modules% parameter being set up by DrupalKernel.
103    * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
104    *   Cache backend for storing module hook implementation information.
105    *
106    * @see \Drupal\Core\DrupalKernel
107    * @see \Drupal\Core\CoreServiceProvider
108    */
109   public function __construct($root, array $module_list, CacheBackendInterface $cache_backend) {
110     $this->root = $root;
111     $this->moduleList = [];
112     foreach ($module_list as $name => $module) {
113       $this->moduleList[$name] = new Extension($this->root, $module['type'], $module['pathname'], $module['filename']);
114     }
115     $this->cacheBackend = $cache_backend;
116   }
117
118   /**
119    * {@inheritdoc}
120    */
121   public function load($name) {
122     if (isset($this->loadedFiles[$name])) {
123       return TRUE;
124     }
125
126     if (isset($this->moduleList[$name])) {
127       $this->moduleList[$name]->load();
128       $this->loadedFiles[$name] = TRUE;
129       return TRUE;
130     }
131     return FALSE;
132   }
133
134   /**
135    * {@inheritdoc}
136    */
137   public function loadAll() {
138     if (!$this->loaded) {
139       foreach ($this->moduleList as $name => $module) {
140         $this->load($name);
141       }
142       $this->loaded = TRUE;
143     }
144   }
145
146   /**
147    * {@inheritdoc}
148    */
149   public function reload() {
150     $this->loaded = FALSE;
151     $this->loadAll();
152   }
153
154   /**
155    * {@inheritdoc}
156    */
157   public function isLoaded() {
158     return $this->loaded;
159   }
160
161   /**
162    * {@inheritdoc}
163    */
164   public function getModuleList() {
165     return $this->moduleList;
166   }
167
168   /**
169    * {@inheritdoc}
170    */
171   public function getModule($name) {
172     if (isset($this->moduleList[$name])) {
173       return $this->moduleList[$name];
174     }
175     throw new \InvalidArgumentException(sprintf('The module %s does not exist.', $name));
176   }
177
178   /**
179    * {@inheritdoc}
180    */
181   public function setModuleList(array $module_list = []) {
182     $this->moduleList = $module_list;
183     // Reset the implementations, so a new call triggers a reloading of the
184     // available hooks.
185     $this->resetImplementations();
186   }
187
188   /**
189    * {@inheritdoc}
190    */
191   public function addModule($name, $path) {
192     $this->add('module', $name, $path);
193   }
194
195   /**
196    * {@inheritdoc}
197    */
198   public function addProfile($name, $path) {
199     $this->add('profile', $name, $path);
200   }
201
202   /**
203    * Adds a module or profile to the list of currently active modules.
204    *
205    * @param string $type
206    *   The extension type; either 'module' or 'profile'.
207    * @param string $name
208    *   The module name; e.g., 'node'.
209    * @param string $path
210    *   The module path; e.g., 'core/modules/node'.
211    */
212   protected function add($type, $name, $path) {
213     $pathname = "$path/$name.info.yml";
214     $filename = file_exists($this->root . "/$path/$name.$type") ? "$name.$type" : NULL;
215     $this->moduleList[$name] = new Extension($this->root, $type, $pathname, $filename);
216     $this->resetImplementations();
217   }
218
219   /**
220    * {@inheritdoc}
221    */
222   public function buildModuleDependencies(array $modules) {
223     foreach ($modules as $module) {
224       $graph[$module->getName()]['edges'] = [];
225       if (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) {
226         foreach ($module->info['dependencies'] as $dependency) {
227           $dependency_data = static::parseDependency($dependency);
228           $graph[$module->getName()]['edges'][$dependency_data['name']] = $dependency_data;
229         }
230       }
231     }
232     $graph_object = new Graph($graph);
233     $graph = $graph_object->searchAndSort();
234     foreach ($graph as $module_name => $data) {
235       $modules[$module_name]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : [];
236       $modules[$module_name]->requires = isset($data['paths']) ? $data['paths'] : [];
237       $modules[$module_name]->sort = $data['weight'];
238     }
239     return $modules;
240   }
241
242   /**
243    * {@inheritdoc}
244    */
245   public function moduleExists($module) {
246     return isset($this->moduleList[$module]);
247   }
248
249   /**
250    * {@inheritdoc}
251    */
252   public function loadAllIncludes($type, $name = NULL) {
253     foreach ($this->moduleList as $module => $filename) {
254       $this->loadInclude($module, $type, $name);
255     }
256   }
257
258   /**
259    * {@inheritdoc}
260    */
261   public function loadInclude($module, $type, $name = NULL) {
262     if ($type == 'install') {
263       // Make sure the installation API is available
264       include_once $this->root . '/core/includes/install.inc';
265     }
266
267     $name = $name ?: $module;
268     $key = $type . ':' . $module . ':' . $name;
269     if (isset($this->includeFileKeys[$key])) {
270       return $this->includeFileKeys[$key];
271     }
272     if (isset($this->moduleList[$module])) {
273       $file = $this->root . '/' . $this->moduleList[$module]->getPath() . "/$name.$type";
274       if (is_file($file)) {
275         require_once $file;
276         $this->includeFileKeys[$key] = $file;
277         return $file;
278       }
279       else {
280         $this->includeFileKeys[$key] = FALSE;
281       }
282     }
283     return FALSE;
284   }
285
286   /**
287    * {@inheritdoc}
288    */
289   public function getHookInfo() {
290     if (!isset($this->hookInfo)) {
291       if ($cache = $this->cacheBackend->get('hook_info')) {
292         $this->hookInfo = $cache->data;
293       }
294       else {
295         $this->buildHookInfo();
296         $this->cacheBackend->set('hook_info', $this->hookInfo);
297       }
298     }
299     return $this->hookInfo;
300   }
301
302   /**
303    * Builds hook_hook_info() information.
304    *
305    * @see \Drupal\Core\Extension\ModuleHandler::getHookInfo()
306    */
307   protected function buildHookInfo() {
308     $this->hookInfo = [];
309     // Make sure that the modules are loaded before checking.
310     $this->reload();
311     // $this->invokeAll() would cause an infinite recursion.
312     foreach ($this->moduleList as $module => $filename) {
313       $function = $module . '_hook_info';
314       if (function_exists($function)) {
315         $result = $function();
316         if (isset($result) && is_array($result)) {
317           $this->hookInfo = NestedArray::mergeDeep($this->hookInfo, $result);
318         }
319       }
320     }
321   }
322
323   /**
324    * {@inheritdoc}
325    */
326   public function getImplementations($hook) {
327     $implementations = $this->getImplementationInfo($hook);
328     return array_keys($implementations);
329   }
330
331   /**
332    * {@inheritdoc}
333    */
334   public function writeCache() {
335     if ($this->cacheNeedsWriting) {
336       $this->cacheBackend->set('module_implements', $this->implementations);
337       $this->cacheNeedsWriting = FALSE;
338     }
339   }
340
341   /**
342    * {@inheritdoc}
343    */
344   public function resetImplementations() {
345     $this->implementations = NULL;
346     $this->hookInfo = NULL;
347     $this->alterFunctions = NULL;
348     // We maintain a persistent cache of hook implementations in addition to the
349     // static cache to avoid looping through every module and every hook on each
350     // request. Benchmarks show that the benefit of this caching outweighs the
351     // additional database hit even when using the default database caching
352     // backend and only a small number of modules are enabled. The cost of the
353     // $this->cacheBackend->get() is more or less constant and reduced further
354     // when non-database caching backends are used, so there will be more
355     // significant gains when a large number of modules are installed or hooks
356     // invoked, since this can quickly lead to
357     // \Drupal::moduleHandler()->implementsHook() being called several thousand
358     // times per request.
359     $this->cacheBackend->set('module_implements', []);
360     $this->cacheBackend->delete('hook_info');
361   }
362
363   /**
364    * {@inheritdoc}
365    */
366   public function implementsHook($module, $hook) {
367     $function = $module . '_' . $hook;
368     if (function_exists($function)) {
369       return TRUE;
370     }
371     // If the hook implementation does not exist, check whether it lives in an
372     // optional include file registered via hook_hook_info().
373     $hook_info = $this->getHookInfo();
374     if (isset($hook_info[$hook]['group'])) {
375       $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
376       if (function_exists($function)) {
377         return TRUE;
378       }
379     }
380     return FALSE;
381   }
382
383   /**
384    * {@inheritdoc}
385    */
386   public function invoke($module, $hook, array $args = []) {
387     if (!$this->implementsHook($module, $hook)) {
388       return;
389     }
390     $function = $module . '_' . $hook;
391     return call_user_func_array($function, $args);
392   }
393
394   /**
395    * {@inheritdoc}
396    */
397   public function invokeAll($hook, array $args = []) {
398     $return = [];
399     $implementations = $this->getImplementations($hook);
400     foreach ($implementations as $module) {
401       $function = $module . '_' . $hook;
402       $result = call_user_func_array($function, $args);
403       if (isset($result) && is_array($result)) {
404         $return = NestedArray::mergeDeep($return, $result);
405       }
406       elseif (isset($result)) {
407         $return[] = $result;
408       }
409     }
410
411     return $return;
412   }
413
414   /**
415    * {@inheritdoc}
416    */
417   public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
418     // Most of the time, $type is passed as a string, so for performance,
419     // normalize it to that. When passed as an array, usually the first item in
420     // the array is a generic type, and additional items in the array are more
421     // specific variants of it, as in the case of array('form', 'form_FORM_ID').
422     if (is_array($type)) {
423       $cid = implode(',', $type);
424       $extra_types = $type;
425       $type = array_shift($extra_types);
426       // Allow if statements in this function to use the faster isset() rather
427       // than !empty() both when $type is passed as a string, or as an array
428       // with one item.
429       if (empty($extra_types)) {
430         unset($extra_types);
431       }
432     }
433     else {
434       $cid = $type;
435     }
436
437     // Some alter hooks are invoked many times per page request, so store the
438     // list of functions to call, and on subsequent calls, iterate through them
439     // quickly.
440     if (!isset($this->alterFunctions[$cid])) {
441       $this->alterFunctions[$cid] = [];
442       $hook = $type . '_alter';
443       $modules = $this->getImplementations($hook);
444       if (!isset($extra_types)) {
445         // For the more common case of a single hook, we do not need to call
446         // function_exists(), since $this->getImplementations() returns only
447         // modules with implementations.
448         foreach ($modules as $module) {
449           $this->alterFunctions[$cid][] = $module . '_' . $hook;
450         }
451       }
452       else {
453         // For multiple hooks, we need $modules to contain every module that
454         // implements at least one of them.
455         $extra_modules = [];
456         foreach ($extra_types as $extra_type) {
457           $extra_modules = array_merge($extra_modules, $this->getImplementations($extra_type . '_alter'));
458         }
459         // If any modules implement one of the extra hooks that do not implement
460         // the primary hook, we need to add them to the $modules array in their
461         // appropriate order. $this->getImplementations() can only return
462         // ordered implementations of a single hook. To get the ordered
463         // implementations of multiple hooks, we mimic the
464         // $this->getImplementations() logic of first ordering by
465         // $this->getModuleList(), and then calling
466         // $this->alter('module_implements').
467         if (array_diff($extra_modules, $modules)) {
468           // Merge the arrays and order by getModuleList().
469           $modules = array_intersect(array_keys($this->moduleList), array_merge($modules, $extra_modules));
470           // Since $this->getImplementations() already took care of loading the
471           // necessary include files, we can safely pass FALSE for the array
472           // values.
473           $implementations = array_fill_keys($modules, FALSE);
474           // Let modules adjust the order solely based on the primary hook. This
475           // ensures the same module order regardless of whether this if block
476           // runs. Calling $this->alter() recursively in this way does not
477           // result in an infinite loop, because this call is for a single
478           // $type, so we won't end up in this code block again.
479           $this->alter('module_implements', $implementations, $hook);
480           $modules = array_keys($implementations);
481         }
482         foreach ($modules as $module) {
483           // Since $modules is a merged array, for any given module, we do not
484           // know whether it has any particular implementation, so we need a
485           // function_exists().
486           $function = $module . '_' . $hook;
487           if (function_exists($function)) {
488             $this->alterFunctions[$cid][] = $function;
489           }
490           foreach ($extra_types as $extra_type) {
491             $function = $module . '_' . $extra_type . '_alter';
492             if (function_exists($function)) {
493               $this->alterFunctions[$cid][] = $function;
494             }
495           }
496         }
497       }
498     }
499
500     foreach ($this->alterFunctions[$cid] as $function) {
501       $function($data, $context1, $context2);
502     }
503   }
504
505   /**
506    * Provides information about modules' implementations of a hook.
507    *
508    * @param string $hook
509    *   The name of the hook (e.g. "help" or "menu").
510    *
511    * @return mixed[]
512    *   An array whose keys are the names of the modules which are implementing
513    *   this hook and whose values are either a string identifying a file in
514    *   which the implementation is to be found, or FALSE, if the implementation
515    *   is in the module file.
516    */
517   protected function getImplementationInfo($hook) {
518     if (!isset($this->implementations)) {
519       $this->implementations = [];
520       $this->verified = [];
521       if ($cache = $this->cacheBackend->get('module_implements')) {
522         $this->implementations = $cache->data;
523       }
524     }
525     if (!isset($this->implementations[$hook])) {
526       // The hook is not cached, so ensure that whether or not it has
527       // implementations, the cache is updated at the end of the request.
528       $this->cacheNeedsWriting = TRUE;
529       // Discover implementations.
530       $this->implementations[$hook] = $this->buildImplementationInfo($hook);
531       // Implementations are always "verified" as part of the discovery.
532       $this->verified[$hook] = TRUE;
533     }
534     elseif (!isset($this->verified[$hook])) {
535       if (!$this->verifyImplementations($this->implementations[$hook], $hook)) {
536         // One or more of the implementations did not exist and need to be
537         // removed in the cache.
538         $this->cacheNeedsWriting = TRUE;
539       }
540       $this->verified[$hook] = TRUE;
541     }
542     return $this->implementations[$hook];
543   }
544
545   /**
546    * Builds hook implementation information for a given hook name.
547    *
548    * @param string $hook
549    *   The name of the hook (e.g. "help" or "menu").
550    *
551    * @return mixed[]
552    *   An array whose keys are the names of the modules which are implementing
553    *   this hook and whose values are either a string identifying a file in
554    *   which the implementation is to be found, or FALSE, if the implementation
555    *   is in the module file.
556    *
557    * @throws \RuntimeException
558    *   Exception thrown when an invalid implementation is added by
559    *   hook_module_implements_alter().
560    *
561    * @see \Drupal\Core\Extension\ModuleHandler::getImplementationInfo()
562    */
563   protected function buildImplementationInfo($hook) {
564     $implementations = [];
565     $hook_info = $this->getHookInfo();
566     foreach ($this->moduleList as $module => $extension) {
567       $include_file = isset($hook_info[$hook]['group']) && $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
568       // Since $this->implementsHook() may needlessly try to load the include
569       // file again, function_exists() is used directly here.
570       if (function_exists($module . '_' . $hook)) {
571         $implementations[$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
572       }
573     }
574     // Allow modules to change the weight of specific implementations, but avoid
575     // an infinite loop.
576     if ($hook != 'module_implements_alter') {
577       // Remember the original implementations, before they are modified with
578       // hook_module_implements_alter().
579       $implementations_before = $implementations;
580       // Verify implementations that were added or modified.
581       $this->alter('module_implements', $implementations, $hook);
582       // Verify new or modified implementations.
583       foreach (array_diff_assoc($implementations, $implementations_before) as $module => $group) {
584         // If an implementation of hook_module_implements_alter() changed or
585         // added a group, the respective file needs to be included.
586         if ($group) {
587           $this->loadInclude($module, 'inc', "$module.$group");
588         }
589         // If a new implementation was added, verify that the function exists.
590         if (!function_exists($module . '_' . $hook)) {
591           throw new \RuntimeException("An invalid implementation {$module}_{$hook} was added by hook_module_implements_alter()");
592         }
593       }
594     }
595     return $implementations;
596   }
597
598   /**
599    * Verifies an array of implementations loaded from the cache, by including
600    * the lazy-loaded $module.$group.inc, and checking function_exists().
601    *
602    * @param string[] $implementations
603    *   Implementation "group" by module name.
604    * @param string $hook
605    *   The hook name.
606    *
607    * @return bool
608    *   TRUE, if all implementations exist.
609    *   FALSE, if one or more implementations don't exist and need to be removed
610    *     from the cache.
611    */
612   protected function verifyImplementations(&$implementations, $hook) {
613     $all_valid = TRUE;
614     foreach ($implementations as $module => $group) {
615       // If this hook implementation is stored in a lazy-loaded file, include
616       // that file first.
617       if ($group) {
618         $this->loadInclude($module, 'inc', "$module.$group");
619       }
620       // It is possible that a module removed a hook implementation without
621       // the implementations cache being rebuilt yet, so we check whether the
622       // function exists on each request to avoid undefined function errors.
623       // Since ModuleHandler::implementsHook() may needlessly try to
624       // load the include file again, function_exists() is used directly here.
625       if (!function_exists($module . '_' . $hook)) {
626         // Clear out the stale implementation from the cache and force a cache
627         // refresh to forget about no longer existing hook implementations.
628         unset($implementations[$module]);
629         // One of the implementations did not exist and needs to be removed in
630         // the cache.
631         $all_valid = FALSE;
632       }
633     }
634     return $all_valid;
635   }
636
637   /**
638    * Parses a dependency for comparison by drupal_check_incompatibility().
639    *
640    * @param $dependency
641    *   A dependency string, which specifies a module dependency, and optionally
642    *   the project it comes from and versions that are supported. Supported
643    *   formats include:
644    *   - 'module'
645    *   - 'project:module'
646    *   - 'project:module (>=version, version)'
647    *
648    * @return
649    *   An associative array with three keys:
650    *   - 'name' includes the name of the thing to depend on (e.g. 'foo').
651    *   - 'original_version' contains the original version string (which can be
652    *     used in the UI for reporting incompatibilities).
653    *   - 'versions' is a list of associative arrays, each containing the keys
654    *     'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<',
655    *     '<=', '>', or '>='. 'version' is one piece like '4.5-beta3'.
656    *   Callers should pass this structure to drupal_check_incompatibility().
657    *
658    * @see drupal_check_incompatibility()
659    */
660   public static function parseDependency($dependency) {
661     $value = [];
662     // Split out the optional project name.
663     if (strpos($dependency, ':') !== FALSE) {
664       list($project_name, $dependency) = explode(':', $dependency);
665       $value['project'] = $project_name;
666     }
667     // We use named subpatterns and support every op that version_compare
668     // supports. Also, op is optional and defaults to equals.
669     $p_op = '(?<operation>!=|==|=|<|<=|>|>=|<>)?';
670     // Core version is always optional: 8.x-2.x and 2.x is treated the same.
671     $p_core = '(?:' . preg_quote(\Drupal::CORE_COMPATIBILITY) . '-)?';
672     $p_major = '(?<major>\d+)';
673     // By setting the minor version to x, branches can be matched.
674     $p_minor = '(?<minor>(?:\d+|x)(?:-[A-Za-z]+\d+)?)';
675     $parts = explode('(', $dependency, 2);
676     $value['name'] = trim($parts[0]);
677     if (isset($parts[1])) {
678       $value['original_version'] = ' (' . $parts[1];
679       foreach (explode(',', $parts[1]) as $version) {
680         if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) {
681           $op = !empty($matches['operation']) ? $matches['operation'] : '=';
682           if ($matches['minor'] == 'x') {
683             // Drupal considers "2.x" to mean any version that begins with
684             // "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(),
685             // on the other hand, treats "x" as a string; so to
686             // version_compare(), "2.x" is considered less than 2.0. This
687             // means that >=2.x and <2.x are handled by version_compare()
688             // as we need, but > and <= are not.
689             if ($op == '>' || $op == '<=') {
690               $matches['major']++;
691             }
692             // Equivalence can be checked by adding two restrictions.
693             if ($op == '=' || $op == '==') {
694               $value['versions'][] = ['op' => '<', 'version' => ($matches['major'] + 1) . '.x'];
695               $op = '>=';
696             }
697           }
698           $value['versions'][] = ['op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']];
699         }
700       }
701     }
702     return $value;
703   }
704
705   /**
706    * {@inheritdoc}
707    */
708   public function getModuleDirectories() {
709     $dirs = [];
710     foreach ($this->getModuleList() as $name => $module) {
711       $dirs[$name] = $this->root . '/' . $module->getPath();
712     }
713     return $dirs;
714   }
715
716   /**
717    * {@inheritdoc}
718    */
719   public function getName($module) {
720     $info = system_get_info('module', $module);
721     return isset($info['name']) ? $info['name'] : $module;
722   }
723
724 }