Updated all the contrib modules to their latest versions.
[yaffs-website] / web / modules / contrib / metatag / src / MetatagManager.php
1 <?php
2
3 namespace Drupal\metatag;
4
5 use Drupal\Component\Render\PlainTextOutput;
6 use Drupal\Core\Entity\ContentEntityInterface;
7 use Drupal\Core\Entity\EntityTypeManagerInterface;
8 use Drupal\Core\Language\LanguageInterface;
9 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
10 use Drupal\views\ViewEntityInterface;
11
12 /**
13  * Class MetatagManager.
14  *
15  * @package Drupal\metatag
16  */
17 class MetatagManager implements MetatagManagerInterface {
18
19   /**
20    * The group plugin manager.
21    *
22    * @var \Drupal\metatag\MetatagGroupPluginManager
23    */
24   protected $groupPluginManager;
25
26   /**
27    * The tag plugin manager.
28    *
29    * @var \Drupal\metatag\MetatagTagPluginManager
30    */
31   protected $tagPluginManager;
32
33   /**
34    * The Metatag defaults.
35    *
36    * @var array
37    */
38   protected $metatagDefaults;
39
40   /**
41    * The Metatag token.
42    *
43    * @var \Drupal\metatag\MetatagToken
44    */
45   protected $tokenService;
46
47   /**
48    * The Metatag logging channel.
49    *
50    * @var \Drupal\Core\Logger\LoggerChannelInterface
51    */
52   protected $logger;
53
54   /**
55    * Constructor for MetatagManager.
56    *
57    * @param \Drupal\metatag\MetatagGroupPluginManager $groupPluginManager
58    *   The MetatagGroupPluginManager object.
59    * @param \Drupal\metatag\MetatagTagPluginManager $tagPluginManager
60    *   The MetatagTagPluginMÏ€anager object.
61    * @param \Drupal\metatag\MetatagToken $token
62    *   The MetatagToken object.
63    * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $channelFactory
64    *   The LoggerChannelFactoryInterface object.
65    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
66    *   The EntityTypeManagerInterface object.
67    */
68   public function __construct(MetatagGroupPluginManager $groupPluginManager,
69       MetatagTagPluginManager $tagPluginManager,
70       MetatagToken $token,
71       LoggerChannelFactoryInterface $channelFactory,
72       EntityTypeManagerInterface $entityTypeManager) {
73     $this->groupPluginManager = $groupPluginManager;
74     $this->tagPluginManager = $tagPluginManager;
75     $this->tokenService = $token;
76     $this->logger = $channelFactory->get('metatag');
77     $this->metatagDefaults = $entityTypeManager->getStorage('metatag_defaults');
78   }
79
80   /**
81    * Returns the list of protected defaults.
82    *
83    * @return array
84    *   Th protected defaults.
85    */
86   public static function protectedDefaults() {
87     return [
88       'global',
89       '403',
90       '404',
91       'node',
92       'front',
93       'taxonomy_term',
94       'user',
95     ];
96   }
97
98   /**
99    * {@inheritdoc}
100    */
101   public function tagsFromEntity(ContentEntityInterface $entity) {
102     $tags = [];
103
104     $fields = $this->getFields($entity);
105
106     /* @var \Drupal\field\Entity\FieldConfig $field_info */
107     foreach ($fields as $field_name => $field_info) {
108       // Get the tags from this field.
109       $tags = $this->getFieldTags($entity, $field_name);
110     }
111
112     return $tags;
113   }
114
115   /**
116    * {@inheritdoc}
117    */
118   public function tagsFromEntityWithDefaults(ContentEntityInterface $entity) {
119     return $this->tagsFromEntity($entity) + $this->defaultTagsFromEntity($entity);
120   }
121
122   /**
123    * {@inheritdoc}
124    */
125   public function defaultTagsFromEntity(ContentEntityInterface $entity) {
126     /** @var \Drupal\metatag\Entity\MetatagDefaults $metatags */
127     $metatags = $this->metatagDefaults->load('global');
128     if (!$metatags) {
129       return NULL;
130     }
131     // Add/overwrite with tags set on the entity type.
132     $entity_type_tags = $this->metatagDefaults->load($entity->getEntityTypeId());
133     if (!is_null($entity_type_tags)) {
134       $metatags->overwriteTags($entity_type_tags->get('tags'));
135     }
136     // Add/overwrite with tags set on the entity bundle.
137     $bundle_metatags = $this->metatagDefaults->load($entity->getEntityTypeId() . '__' . $entity->bundle());
138     if (!is_null($bundle_metatags)) {
139       $metatags->overwriteTags($bundle_metatags->get('tags'));
140     }
141     return $metatags->get('tags');
142   }
143
144   /**
145    * Gets the group plugin definitions.
146    *
147    * @return array
148    *   Group definitions.
149    */
150   protected function groupDefinitions() {
151     return $this->groupPluginManager->getDefinitions();
152   }
153
154   /**
155    * Gets the tag plugin definitions.
156    *
157    * @return array
158    *   Tag definitions
159    */
160   protected function tagDefinitions() {
161     return $this->tagPluginManager->getDefinitions();
162   }
163
164   /**
165    * {@inheritdoc}
166    */
167   public function sortedGroups() {
168     $metatag_groups = $this->groupDefinitions();
169
170     // Pull the data from the definitions into a new array.
171     $groups = [];
172     foreach ($metatag_groups as $group_name => $group_info) {
173       $groups[$group_name]['id'] = $group_info['id'];
174       $groups[$group_name]['label'] = $group_info['label']->render();
175       $groups[$group_name]['description'] = $group_info['description'];
176       $groups[$group_name]['weight'] = $group_info['weight'];
177     }
178
179     // Create the 'sort by' array.
180     $sort_by = [];
181     foreach ($groups as $group) {
182       $sort_by[] = $group['weight'];
183     }
184
185     // Sort the groups by weight.
186     array_multisort($sort_by, SORT_ASC, $groups);
187
188     return $groups;
189   }
190
191   /**
192    * {@inheritdoc}
193    */
194   public function sortedTags() {
195     $metatag_tags = $this->tagDefinitions();
196
197     // Pull the data from the definitions into a new array.
198     $tags = [];
199     foreach ($metatag_tags as $tag_name => $tag_info) {
200       $tags[$tag_name]['id'] = $tag_info['id'];
201       $tags[$tag_name]['label'] = $tag_info['label']->render();
202       $tags[$tag_name]['group'] = $tag_info['group'];
203       $tags[$tag_name]['weight'] = $tag_info['weight'];
204     }
205
206     // Create the 'sort by' array.
207     $sort_by = [];
208     foreach ($tags as $key => $tag) {
209       $sort_by['group'][$key] = $tag['group'];
210       $sort_by['weight'][$key] = $tag['weight'];
211     }
212
213     // Sort the tags by weight.
214     array_multisort($sort_by['group'], SORT_ASC, $sort_by['weight'], SORT_ASC, $tags);
215
216     return $tags;
217   }
218
219   /**
220    * {@inheritdoc}
221    */
222   public function sortedGroupsWithTags() {
223     $groups = $this->sortedGroups();
224     $tags = $this->sortedTags();
225
226     foreach ($tags as $tag_name => $tag) {
227       $tag_group = $tag['group'];
228
229       if (!isset($groups[$tag_group])) {
230         // If the tag is claiming a group that has no matching plugin, log an
231         // error and force it to the basic group.
232         $this->logger->error("Undefined group '%group' on tag '%tag'", ['%group' => $tag_group, '%tag' => $tag_name]);
233         $tag['group'] = 'basic';
234         $tag_group = 'basic';
235       }
236
237       $groups[$tag_group]['tags'][$tag_name] = $tag;
238     }
239
240     return $groups;
241   }
242
243   /**
244    * {@inheritdoc}
245    */
246   public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL) {
247     // Add the outer fieldset.
248     $element += [
249       '#type' => 'details',
250     ];
251
252     $element += $this->tokenService->tokenBrowser($token_types);
253
254     $groups_and_tags = $this->sortedGroupsWithTags();
255
256     foreach ($groups_and_tags as $group_name => $group) {
257       // Only act on groups that have tags and are in the list of included
258       // groups (unless that list is null).
259       if (isset($group['tags']) && (is_null($included_groups) || in_array($group_name, $included_groups) || in_array($group['id'], $included_groups))) {
260         // Create the fieldset.
261         $element[$group_name]['#type'] = 'details';
262         $element[$group_name]['#title'] = $group['label'];
263         $element[$group_name]['#description'] = $group['description'];
264         $element[$group_name]['#open'] = FALSE;
265
266         foreach ($group['tags'] as $tag_name => $tag) {
267           // Only act on tags in the included tags list, unless that is null.
268           if (is_null($included_tags) || in_array($tag_name, $included_tags) || in_array($tag['id'], $included_tags)) {
269             // Make an instance of the tag.
270             $tag = $this->tagPluginManager->createInstance($tag_name);
271
272             // Set the value to the stored value, if any.
273             $tag_value = isset($values[$tag_name]) ? $values[$tag_name] : NULL;
274             $tag->setValue($tag_value);
275
276             // Open any groups that have non-empty values.
277             if (!empty($tag_value)) {
278               $element[$group_name]['#open'] = TRUE;
279             }
280
281             // Create the bit of form for this tag.
282             $element[$group_name][$tag_name] = $tag->form($element);
283           }
284         }
285       }
286     }
287
288     return $element;
289   }
290
291   /**
292    * Returns a list of the Metatag fields on an entity.
293    *
294    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
295    *   The entity to examine.
296    *
297    * @return array
298    *   The fields from the entity which are Metatag fields.
299    */
300   protected function getFields(ContentEntityInterface $entity) {
301     $field_list = [];
302
303     if ($entity instanceof ContentEntityInterface) {
304       // Get a list of the metatag field types.
305       $field_types = $this->fieldTypes();
306
307       // Get a list of the field definitions on this entity.
308       $definitions = $entity->getFieldDefinitions();
309
310       // Iterate through all the fields looking for ones in our list.
311       foreach ($definitions as $field_name => $definition) {
312         // Get the field type, ie: metatag.
313         $field_type = $definition->getType();
314
315         // Check the field type against our list of fields.
316         if (isset($field_type) && in_array($field_type, $field_types)) {
317           $field_list[$field_name] = $definition;
318         }
319       }
320     }
321
322     return $field_list;
323   }
324
325   /**
326    * Returns a list of the meta tags with values from a field.
327    *
328    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
329    *   The ContentEntityInterface object.
330    * @param string $field_name
331    *   The name of the field to work on.
332    *
333    * @return array
334    *   Array of field tags.
335    */
336   protected function getFieldTags(ContentEntityInterface $entity, $field_name) {
337     $tags = [];
338     foreach ($entity->{$field_name} as $item) {
339       // Get serialized value and break it into an array of tags with values.
340       $serialized_value = $item->get('value')->getValue();
341       if (!empty($serialized_value)) {
342         $tags += unserialize($serialized_value);
343       }
344     }
345
346     return $tags;
347   }
348
349   /**
350    * Returns default meta tags for an entity.
351    *
352    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
353    *   The entity to work on.
354    *
355    * @return array
356    *   The default meta tags appropriate for this entity.
357    */
358   public function getDefaultMetatags(ContentEntityInterface $entity = NULL) {
359     // Get general global metatags.
360     $metatags = $this->getGlobalMetatags();
361     // If that is empty something went wrong.
362     if (!$metatags) {
363       return;
364     }
365
366     // Check if this is a special page.
367     $special_metatags = $this->getSpecialMetatags();
368
369     // Merge with all globals defaults.
370     if ($special_metatags) {
371       $metatags->set('tags', array_merge($metatags->get('tags'), $special_metatags->get('tags')));
372     }
373
374     // Next check if there is this page is an entity that has meta tags.
375     // @todo Think about using other defaults, e.g. views. Maybe use plugins?
376     else {
377       if (is_null($entity)) {
378         $entity = metatag_get_route_entity();
379       }
380
381       if (!empty($entity)) {
382         // Get default meta tags for a given entity.
383         $entity_defaults = $this->getEntityDefaultMetatags($entity);
384         if ($entity_defaults != NULL) {
385           $metatags->set('tags', array_merge($metatags->get('tags'), $entity_defaults));
386         }
387       }
388     }
389
390     return $metatags->get('tags');
391   }
392
393   /**
394    * Returns global meta tags.
395    *
396    * @return array
397    *   The global meta tags.
398    */
399   public function getGlobalMetatags() {
400     return $this->metatagDefaults->load('global');
401   }
402
403   /**
404    * Returns special meta tags.
405    *
406    * @return array
407    *   The defaults for this page, if it's a special page.
408    */
409   public function getSpecialMetatags() {
410     $metatags = NULL;
411
412     if (\Drupal::service('path.matcher')->isFrontPage()) {
413       $metatags = $this->metatagDefaults->load('front');
414     }
415     elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.403') {
416       $metatags = $this->metatagDefaults->load('403');
417     }
418     elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.404') {
419       $metatags = $this->metatagDefaults->load('404');
420     }
421
422     return $metatags;
423   }
424
425   /**
426    * Returns default meta tags for an entity.
427    *
428    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
429    *   The entity to work with.
430    *
431    * @return array
432    *   The appropriate default meta tags.
433    */
434   public function getEntityDefaultMetatags(ContentEntityInterface $entity) {
435     $entity_metatags = $this->metatagDefaults->load($entity->getEntityTypeId());
436     $metatags = [];
437     if ($entity_metatags != NULL) {
438       // Merge with global defaults.
439       $metatags = array_merge($metatags, $entity_metatags->get('tags'));
440     }
441
442     // Finally, check if we should apply bundle overrides.
443     $bundle_metatags = $this->metatagDefaults->load($entity->getEntityTypeId() . '__' . $entity->bundle());
444     if ($bundle_metatags != NULL) {
445       // Merge with existing defaults.
446       $metatags = array_merge($metatags, $bundle_metatags->get('tags'));
447     }
448
449     return $metatags;
450   }
451
452   /**
453    * Generate the elements that go in the hook_page_attachments attached array.
454    *
455    * @param array $tags
456    *   The array of tags as plugin_id => value.
457    * @param object $entity
458    *   Optional entity object to use for token replacements.
459    *
460    * @return array
461    *   Render array with tag elements.
462    */
463   public function generateElements(array $tags, $entity = NULL) {
464     $elements = [];
465     $tags = $this->generateRawElements($tags, $entity);
466
467     foreach ($tags as $name => $tag) {
468       if (!empty($tag)) {
469         $elements['#attached']['html_head'][] = [
470           $tag,
471           $name,
472         ];
473       }
474     }
475
476     return $elements;
477   }
478
479   /**
480    * Generate the actual meta tag values.
481    *
482    * @param array $tags
483    *   The array of tags as plugin_id => value.
484    * @param object $entity
485    *   Optional entity object to use for token replacements.
486    *
487    * @return array
488    *   Render array with tag elements.
489    */
490   public function generateRawElements(array $tags, $entity = NULL) {
491     // Ignore the update.php path.
492     $request = \Drupal::request();
493     if ($request->getBaseUrl() == '/update.php') {
494       return [];
495     }
496
497     $rawTags = [];
498
499     $metatag_tags = $this->tagPluginManager->getDefinitions();
500
501     // Order the elements by weight first, as some systems like Facebook care.
502     uksort($tags, function ($tag_name_a, $tag_name_b) use ($metatag_tags) {
503       $weight_a = isset($metatag_tags[$tag_name_a]['weight']) ? $metatag_tags[$tag_name_a]['weight'] : 0;
504       $weight_b = isset($metatag_tags[$tag_name_b]['weight']) ? $metatag_tags[$tag_name_b]['weight'] : 0;
505
506       return ($weight_a < $weight_b) ? -1 : 1;
507     });
508
509     // Each element of the $values array is a tag with the tag plugin name as
510     // the key.
511     foreach ($tags as $tag_name => $value) {
512       // Check to ensure there is a matching plugin.
513       if (isset($metatag_tags[$tag_name])) {
514         // Get an instance of the plugin.
515         $tag = $this->tagPluginManager->createInstance($tag_name);
516
517         // Render any tokens in the value.
518         $token_replacements = [];
519         if ($entity) {
520           // @todo This needs a better way of discovering the context.
521           if ($entity instanceof ViewEntityInterface) {
522             // Views tokens require the ViewExecutable, not the config entity.
523             // @todo Can we move this into metatag_views somehow?
524             $token_replacements = ['view' => $entity->getExecutable()];
525           }
526           elseif ($entity instanceof ContentEntityInterface) {
527             $token_replacements = [$entity->getEntityTypeId() => $entity];
528           }
529         }
530
531         // Set the value as sometimes the data needs massaging, such as when
532         // field defaults are used for the Robots field, which come as an array
533         // that needs to be filtered and converted to a string.
534         // @see Robots::setValue()
535         $tag->setValue($value);
536         $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
537
538         $processed_value = PlainTextOutput::renderFromHtml(htmlspecialchars_decode($this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode])));
539
540         // Now store the value with processed tokens back into the plugin.
541         $tag->setValue($processed_value);
542
543         // Have the tag generate the output based on the value we gave it.
544         $output = $tag->output();
545
546         if (!empty($output)) {
547           $output = $tag->multiple() ? $output : [$output];
548
549           // Backwards compatibility for modules which don't support this logic.
550           if (isset($output['#tag'])) {
551             $output = [$output];
552           }
553
554           foreach ($output as $index => $element) {
555             // Add index to tag name as suffix to avoid having same key.
556             $index_tag_name = $tag->multiple() ? $tag_name . '_' . $index : $tag_name;
557             $rawTags[$index_tag_name] = $element;
558           }
559         }
560       }
561     }
562
563     return $rawTags;
564   }
565
566   /**
567    * Returns a list of fields handled by Metatag.
568    *
569    * @return array
570    *   A list of supported field types.
571    */
572   protected function fieldTypes() {
573     // @todo Either get this dynamically from field plugins or forget it and
574     // just hardcode metatag where this is called.
575     return ['metatag'];
576   }
577
578 }