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