Updated all the contrib modules to their latest versions.
[yaffs-website] / web / modules / contrib / metatag / metatag.module
1 <?php
2
3 /**
4  * @file
5  * Contains metatag.module.
6  */
7
8 use Drupal\Core\Entity\ContentEntityInterface;
9 use Drupal\Core\Entity\EntityInterface;
10 use Drupal\Core\Entity\EntityTypeInterface;
11 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
12 use Drupal\Core\Field\BaseFieldDefinition;
13 use Drupal\Core\Form\FormStateInterface;
14 use Drupal\Core\Routing\RouteMatchInterface;
15 use Drupal\Core\Url;
16 use Drupal\taxonomy\TermInterface;
17 use Drupal\Component\Utility\Html;
18
19 /**
20  * Implements hook_help().
21  */
22 function metatag_help($route_name, RouteMatchInterface $route_match) {
23   switch ($route_name) {
24     // Main module help for the Metatag module.
25     case 'help.page.metatag':
26       $output = '<h2>' . t('About') . '</h2>';
27       $output .= '<p>' . t('This module allows a site to automatically provide structured metadata, aka "meta tags", about the site and individual pages.');
28       $output .= '<p>' . t('In the context of search engine optimization, providing an extensive set of meta tags may help improve the site\'s and pages\' rankings, thus may aid with achieving a more prominent display of the content within search engine results. They can also be used to tailor how content is displayed when shared on social networks. For additional information, see the <a href=":online">online documentation for Metatag</a>.', [':online' => 'https://www.drupal.org/node/1774342']) . '</p>';
29       $output .= '<h3>' . t('Intended worflow') . '</h3>';
30       $output .= '<p>' . t('The module uses <a href=":tokens">"tokens"</a> to automatically fill in values for different meta tags. Specific values may also be filled in.', [':tokens' => Url::fromRoute('help.page', ['name' => 'token'])->toString()]) . '</p>';
31       $output .= '<p>' . t('The best way of using Metatag is as follows:') . '</p>';
32       $output .= '<ol>';
33       $output .= '<li>' . t('Customize the <a href=":defaults">global defaults</a>, fill in the specific values and tokens that every page should have.', [':defaults' => Url::fromRoute('entity.metatag_defaults.edit_form', ['metatag_defaults' => 'global'])->toString()]) . '</li>';
34       $output .= '<li>' . t('Override each of the <a href=":defaults">other defaults</a>, fill in specific values and tokens that each item should have by default. This allows e.g. for all nodes to have different values than taxonomy terms.', [':defaults' => Url::fromRoute('entity.metatag_defaults.collection')->toString()]) . '</li>';
35       $output .= '<li>' . t('<a href=":add">Add more default configurations</a> as necessary for different entity types and entity bundles, e.g. for different content types or different vocabularies.', [':add' => Url::fromRoute('entity.metatag_defaults.add_form')->toString()]) . '</li>';
36       $output .= '<li>' . t('To override the meta tags for individual entities, e.g. for individual nodes, add the "Metatag" field via the field settings for that entity or bundle type.') . '</li>';
37       $output .= '</ol>';
38       return $output;
39
40     // The main configuration page.
41     case 'entity.metatag_defaults.collection':
42       $output = '<p>' . t('Configure global meta tag default values below. Meta tags may be left as the default.') . '</p>';
43       $output .= '<p>' . t('Meta tag patterns are passed down from one level to the next unless they are overridden. To view a summary of the individual meta tags and the pattern for a specific configuration, click on its name below.') . '</p>';
44       $output .= '<p>' . t('If the top-level configuration is not specific enough, additional default meta tag configurations can be added for a specific entity type or entity bundle, e.g. for a specific content type.') . '</p>';
45       $output .= '<p>' . t('Meta tags can be further refined on a per-entity basis, e.g. for individual nodes, by adding the "Metatag" field to that entity type through its normal field settings pages.') . '</p>';
46       return $output;
47
48     // The 'add default meta tags' configuration page.
49     case 'entity.metatag_defaults.add_form':
50       $output = '<p>' . t('Use the following form to override the global default meta tags for a specific entity type or entity bundle. In practical terms, this allows the meta tags to be customized for a specific content type or taxonomy vocabulary, so that its content will have different meta tags <em>default values</em> than others.') . '</p>';
51       $output .= '<p>' . t('As a reminder, if the "Metatag" field is added to the entity type through its normal field settings, the meta tags can be further refined on a per entity basis; this allows each node to have its meta tags  customized on an individual basis.') . '</p>';
52       return $output;
53   }
54 }
55
56 /**
57  * Implements hook_form_FORM_ID_alter() for 'field_storage_config_edit_form'.
58  */
59 function metatag_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state) {
60   if ($form_state->getFormObject()->getEntity()->getType() == 'metatag') {
61     // Hide the cardinality field.
62     $form['cardinality_container']['#access'] = FALSE;
63     $form['cardinality_container']['#disabled'] = TRUE;
64   }
65 }
66
67 /**
68  * Implements hook_form_FORM_ID_alter() for 'field_config_edit_form'.
69  *
70  * Configuration defaults are handled via a different mechanism, so do not allow
71  * any values to be saved.
72  */
73 function metatag_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state) {
74   if ($form_state->getFormObject()->getEntity()->getType() == 'metatag') {
75     // Hide the required and default value fields.
76     $form['required']['#access'] = FALSE;
77     $form['required']['#disabled'] = TRUE;
78     $form['default_value']['#access'] = FALSE;
79     $form['default_value']['#disabled'] = TRUE;
80
81     // Step through the default value structure and erase any '#default_value'
82     // items that are found.
83     foreach ($form['default_value']['widget'][0] as &$outer) {
84       if (is_array($outer)) {
85         foreach ($outer as &$inner) {
86           if (is_array($inner) && isset($inner['#default_value'])) {
87             if (is_array($inner['#default_value'])) {
88               $inner['#default_value'] = [];
89             }
90             else {
91               $inner['#default_value'] = NULL;
92             }
93           }
94         }
95       }
96     }
97   }
98 }
99
100 /**
101  * Implements hook_page_attachments().
102  *
103  * Load all meta tags for this page.
104  */
105 function metatag_page_attachments(array &$attachments) {
106   if (!metatag_is_current_route_supported()) {
107     return NULL;
108   }
109
110   $metatag_attachments = &drupal_static('metatag_attachments');
111
112   if (is_null($metatag_attachments)) {
113     // Load the meta tags from the route.
114     $metatag_attachments = metatag_get_tags_from_route();
115   }
116   if (!$metatag_attachments) {
117     return NULL;
118   }
119
120   // Trigger hook_metatags_attachments_alter().
121   // Allow modules to rendered metatags prior to attaching.
122   \Drupal::service('module_handler')->alter('metatags_attachments', $metatag_attachments);
123
124   // If any Metatag items were found, append them.
125   if (!empty($metatag_attachments['#attached']['html_head'])) {
126     if (empty($attachments['#attached'])) {
127       $attachments['#attached'] = [];
128     }
129     if (empty($attachments['#attached']['html_head'])) {
130       $attachments['#attached']['html_head'] = [];
131     }
132
133     $head_links = [];
134     foreach ($metatag_attachments['#attached']['html_head'] as $item) {
135       $attachments['#attached']['html_head'][] = $item;
136
137       // Also add a HTTP header "Link:" for canonical URLs and shortlinks.
138       // See HtmlResponseAttachmentsProcessor::processHtmlHeadLink() for the
139       // implementation of the functionality in core.
140       if (in_array($item[1], ['canonical_url', 'shortlink'])) {
141         $attributes = $item[0]['#attributes'];
142
143         $href = '<' . Html::escape($attributes['href']) . '>';
144         unset($attributes['href']);
145         if ($param = drupal_http_header_attributes($attributes)) {
146           $href .= ';' . $param;
147         }
148         $head_links[] = $href;
149       }
150     }
151
152     // If any HTTP Header items were found, add them too.
153     if (!empty($head_links)) {
154       $attachments['#attached']['http_header'][] = [
155         'Link',
156         implode(', ', $head_links),
157         FALSE,
158       ];
159     }
160   }
161 }
162
163 /**
164  * Implements hook_module_implements_alter().
165  */
166 function metatag_module_implements_alter(&$implementations, $hook) {
167   if ($hook == 'page_attachments_alter') {
168     // Move metatag_page_attachments_alter() to the end of the list. This is so
169     // the canonical and shortlink tags can be removed that are added by
170     // taxonomy_page_attachments_alter().
171     // @todo Remove once https://www.drupal.org/node/2282029 is fixed.
172     $group = $implementations['metatag'];
173     unset($implementations['metatag']);
174     $implementations['metatag'] = $group;
175   }
176 }
177
178 /**
179  * Implements hook_page_attachments_alter().
180  */
181 function metatag_page_attachments_alter(array &$attachments) {
182   $route_match = \Drupal::routeMatch();
183   // Can be removed once https://www.drupal.org/node/2282029 is fixed.
184   if ($route_match->getRouteName() == 'entity.taxonomy_term.canonical' && ($term = $route_match->getParameter('taxonomy_term')) && $term instanceof TermInterface) {
185     _metatag_remove_duplicate_entity_tags($attachments);
186   }
187 }
188
189 /**
190  * Implements hook_entity_view_alter().
191  */
192 function metatag_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
193   // If this is a 403 or 404 page then don't output these meta tags.
194   // @todo Make the default meta tags load properly so this is unnecessary.
195   if ($display->getOriginalId() == 'node.403.default' || $display->getOriginalId() == 'node.404.default') {
196     $build['#attached']['html_head_link'] = [];
197     return;
198   }
199
200   // Panelized entities still use the original entity's controller, but with
201   // custom built entities. In those cases hook_entity_view_alter might be
202   // called too early, where meta links are not yet set.
203   // @see \Drupal\node\Controller\NodeController::view
204   if ($display->getThirdPartySetting('panelizer', 'enable', FALSE)) {
205     $build['#pre_render'][] = '_metatag_panelizer_pre_render';
206     return;
207   }
208
209   _metatag_remove_duplicate_entity_tags($build);
210 }
211
212 /**
213  * Pre render callback for entities processed by Panelizer.
214  *
215  * @param array $element
216  *   The render array being processed.
217  *
218  * @return array
219  *   The filtered render array.
220  */
221 function _metatag_panelizer_pre_render(array $element) {
222   _metatag_remove_duplicate_entity_tags($element);
223   return $element;
224 }
225
226 /**
227  * Remove duplicate entity tags from a build.
228  *
229  * @param array $build
230  *   The build.
231  */
232 function _metatag_remove_duplicate_entity_tags(array &$build) {
233   // Some entities are built with a link rel="canonical" and/or link
234   // rel="shortlink" tag attached.
235   // If metatag provides them, remove the ones built with the entity.
236   if (isset($build['#attached']['html_head_link'])) {
237     $metatag_attachments = &drupal_static('metatag_attachments');
238     if (is_null($metatag_attachments)) {
239       // Load the meta tags from the route.
240       $metatag_attachments = metatag_get_tags_from_route();
241     }
242
243     // Check to see if the page currently outputs a canonical and/or shortlink
244     // tag.
245     if (isset($metatag_attachments['#attached']['html_head'])) {
246       foreach ($metatag_attachments['#attached']['html_head'] as $metatag_item) {
247         if (in_array($metatag_item[1], ['canonical_url', 'shortlink'])) {
248           // Metatag provides rel="canonical" and/or rel="shortlink" tags.
249           foreach ($build['#attached']['html_head_link'] as $key => $item) {
250             if (isset($item[0]['rel']) && in_array($item[0]['rel'], ['canonical', 'shortlink'])) {
251               // Remove the link rel="canonical" or link rel="shortlink" tag
252               // from the entity's build array.
253               unset($build['#attached']['html_head_link'][$key]);
254             }
255           }
256         }
257       }
258     }
259   }
260 }
261
262 /**
263  * Identify whether the current route is supported by the module.
264  *
265  * @return bool
266  *   TRUE if the current route is supported.
267  */
268 function metatag_is_current_route_supported() {
269   // If upgrading, we need to wait for database updates to complete.
270   $is_ready = \Drupal::service('entity_type.manager')
271     ->getDefinition('metatag_defaults', FALSE);
272   if (!$is_ready) {
273     return FALSE;
274   }
275
276   // Ignore admin paths.
277   if (\Drupal::service('router.admin_context')->isAdminRoute()) {
278     return FALSE;
279   }
280
281   return TRUE;
282 }
283
284 /**
285  * Returns the entity of the current route.
286  *
287  * @return Drupal\Core\Entity\EntityInterface
288  *   The entity or NULL if this is not an entity route.
289  */
290 function metatag_get_route_entity() {
291   $route_match = \Drupal::routeMatch();
292   $route_name = $route_match->getRouteName();
293
294   // Look for a canonical entity view page, e.g. node/{nid}, user/{uid}, etc.
295   $matches = [];
296   preg_match('/entity\.(.*)\.(latest[_-]version|canonical)/', $route_name, $matches);
297   if (!empty($matches[1])) {
298     $entity_type = $matches[1];
299     return $route_match->getParameter($entity_type);
300   }
301
302   // Look for a rest entity view page, e.g. "node/{nid}?_format=json", etc.
303   $matches = [];
304   // Matches e.g. "rest.entity.node.GET.json".
305   preg_match('/rest\.entity\.(.*)\.(.*)\.(.*)/', $route_name, $matches);
306   if (!empty($matches[1])) {
307     $entity_type = $matches[1];
308     return $route_match->getParameter($entity_type);
309   }
310
311   // Look for entity object 'add' pages, e.g. "node/add/{bundle}".
312   $route_name_matches = [];
313   preg_match('/(entity\.)?(.*)\.add(_form)?/', $route_name, $route_name_matches);
314   if (!empty($route_name_matches[2])) {
315     $entity_type = $route_name_matches[2];
316     $definition = Drupal::entityTypeManager()->getDefinition($entity_type, FALSE);
317     if (!empty($definition)) {
318       $type = $route_match->getRawParameter($definition->get('bundle_entity_type'));
319       if (!empty($type)) {
320         return \Drupal::entityTypeManager()
321           ->getStorage($entity_type)
322           ->create([
323             $definition->get('entity_keys')['bundle'] => $type,
324           ]);
325       }
326     }
327   }
328
329   // Look for entity object 'edit' pages, e.g. "node/{entity_id}/edit".
330   $route_name_matches = [];
331   preg_match('/entity\.(.*)\.edit_form/', $route_name, $route_name_matches);
332   if (!empty($route_name_matches[1])) {
333     $entity_type = $route_name_matches[1];
334     $entity_id = $route_match->getRawParameter($entity_type);
335
336     if (!empty($entity_id)) {
337       return \Drupal::entityTypeManager()
338         ->getStorage($entity_type)
339         ->load($entity_id);
340     }
341   }
342
343   // Look for entity object 'add content translation' pages, e.g.
344   // "node/{nid}/translations/add/{source_lang}/{translation_lang}".
345   $route_name_matches = [];
346   preg_match('/(entity\.)?(.*)\.content_translation_add/', $route_name, $route_name_matches);
347   if (!empty($route_name_matches[2])) {
348     $entity_type = $route_name_matches[2];
349     $definition = Drupal::entityTypeManager()->getDefinition($entity_type, FALSE);
350     if (!empty($definition)) {
351       $node = $route_match->getParameter($entity_type);
352       $type = $node->bundle();
353       if (!empty($type)) {
354         return \Drupal::entityTypeManager()
355           ->getStorage($entity_type)
356           ->create([
357             $definition->get('entity_keys')['bundle'] => $type,
358           ]);
359       }
360     }
361   }
362
363   // Special handling for the admin user_create page. In this case, there's only
364   // one bundle and it's named the same as the entity type, so some shortcuts
365   // can be used.
366   if ($route_name == 'user.admin_create') {
367     $entity_type = $type = 'user';
368     $definition = Drupal::entityTypeManager()->getDefinition($entity_type);
369     if (!empty($type)) {
370       return \Drupal::entityTypeManager()
371         ->getStorage($entity_type)
372         ->create([
373           $definition->get('entity_keys')['bundle'] => $type,
374         ]);
375     }
376   }
377
378   // Trigger hook_metatag_route_entity().
379   if ($entities = \Drupal::moduleHandler()->invokeAll('metatag_route_entity', [$route_match])) {
380     return reset($entities);
381   }
382
383   return NULL;
384 }
385
386 /**
387  * Implements template_preprocess_html().
388  */
389 function metatag_preprocess_html(&$variables) {
390   if (!metatag_is_current_route_supported()) {
391     return NULL;
392   }
393
394   $attachments = &drupal_static('metatag_attachments');
395   if (is_null($attachments)) {
396     $attachments = metatag_get_tags_from_route();
397   }
398
399   if (!$attachments) {
400     return NULL;
401   }
402
403   // Load the page title.
404   if (!empty($attachments['#attached']['html_head'])) {
405     foreach ($attachments['#attached']['html_head'] as $key => $attachment) {
406       if (!empty($attachment[1]) && $attachment[1] == 'title') {
407         // Empty head_title to avoid the site name and slogan to be appended to
408         // the meta title.
409         $variables['head_title'] = [];
410         $variables['head_title']['title'] = html_entity_decode($attachment[0]['#attributes']['content'], ENT_QUOTES);
411         break;
412       }
413     }
414   }
415 }
416
417 /**
418  * Load the meta tags by processing the route parameters.
419  *
420  * @return mixed
421  *   Array of meta tags or NULL.
422  */
423 function metatag_get_tags_from_route($entity = NULL) {
424   $metatag_manager = \Drupal::service('metatag.manager');
425
426   // First, get defaults.
427   $metatags = metatag_get_default_tags($entity);
428   if (!$metatags) {
429     return NULL;
430   }
431
432   // Then, set tag overrides for this particular entity.
433   if (!$entity) {
434     $entity = metatag_get_route_entity();
435   }
436
437   if (!empty($entity) && $entity instanceof ContentEntityInterface) {
438     // If content entity does not have an ID the page is likely an "Add" page,
439     // so do not generate meta tags for entity which has not been created yet.
440     if (!$entity->id()) {
441       return NULL;
442     }
443
444     foreach ($metatag_manager->tagsFromEntity($entity) as $tag => $data) {
445       $metatags[$tag] = $data;
446     }
447   }
448
449   // Trigger hook_metatags_alter().
450   // Allow modules to override tags or the entity used for token replacements.
451   $context = [
452     'entity' => &$entity,
453   ];
454   \Drupal::service('module_handler')->alter('metatags', $metatags, $context);
455
456   // If the entity was changed above, use that for generating the meta tags.
457   if (isset($context['entity'])) {
458     $entity = $context['entity'];
459   }
460
461   return $metatag_manager->generateElements($metatags, $entity);
462 }
463
464 /**
465  * Returns default tags for the current route.
466  *
467  * @return mixed
468  *   Array of tags or NULL;
469  */
470 function metatag_get_default_tags($entity = NULL) {
471   /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $global_metatag_manager */
472   $global_metatag_manager = \Drupal::entityTypeManager()->getStorage('metatag_defaults');
473   // First we load global defaults.
474   $metatags = $global_metatag_manager->load('global');
475   if (!$metatags) {
476     return NULL;
477   }
478
479   // Check if this is a special page.
480   $special_metatags = \Drupal::service('metatag.manager')->getSpecialMetatags();
481   if (isset($special_metatags)) {
482     $metatags->overwriteTags($special_metatags->get('tags'));
483   }
484
485   // Next check if there is this page is an entity that has meta tags.
486   else {
487     if (!$entity) {
488       $entity = metatag_get_route_entity();
489     }
490
491     if (!empty($entity) && $entity instanceof ContentEntityInterface) {
492       $entity_metatags = $global_metatag_manager->load($entity->getEntityTypeId());
493       if ($entity_metatags != NULL) {
494         // Merge with global defaults.
495         $metatags->overwriteTags($entity_metatags->get('tags'));
496       }
497
498       // Finally, check if bundle overrides should be added.
499       $bundle_metatags = $global_metatag_manager->load($entity->getEntityTypeId() . '__' . $entity->bundle());
500       if ($bundle_metatags != NULL) {
501         // Merge with existing defaults.
502         $metatags->overwriteTags($bundle_metatags->get('tags'));
503       }
504     }
505   }
506
507   return $metatags->get('tags');
508 }
509
510 /**
511  * Implements hook_entity_base_field_info().
512  */
513 function metatag_entity_base_field_info(EntityTypeInterface $entity_type) {
514   $fields = [];
515   $base_table = $entity_type->getBaseTable();
516   $canonical_template_exists = $entity_type->hasLinkTemplate('canonical');
517   // Certain classes are just not supported.
518   $original_class = $entity_type->getOriginalClass();
519   $classes_to_skip = [
520     'Drupal\comment\Entity\Comment',
521   ];
522
523   // If the entity type doesn't have a base table, has no link template then
524   // there's no point in supporting it.
525   if (!empty($base_table) && $canonical_template_exists && !in_array($original_class, $classes_to_skip)) {
526     $fields['metatag'] = BaseFieldDefinition::create('map')
527       ->setLabel(t('Metatags'))
528       ->setDescription(t('The meta tags for the entity.'))
529       ->setClass('\Drupal\metatag\Plugin\Field\MetatagEntityFieldItemList')
530       ->setComputed(TRUE)
531       ->setTranslatable(TRUE)
532       ->setTargetEntityTypeId($entity_type->id());
533   }
534
535   return $fields;
536 }
537
538 /**
539  * Implements hook_entity_diff_options().
540  */
541 function metatag_entity_diff_options($entity_type) {
542   if (metatag_entity_supports_metatags($entity_type)) {
543     $options = [
544       'metatag' => t('Metatags'),
545     ];
546     return $options;
547   }
548 }
549
550 /**
551  * Implements hook_entity_diff().
552  */
553 function metatag_entity_diff($old_entity, $new_entity, $context) {
554   $result = [];
555   $entity_type = $context['entity_type'];
556   $options = variable_get('diff_additional_options_' . $entity_type, []);
557   if (!empty($options['metatag']) && metatag_entity_supports_metatags($entity_type)) {
558     // Find meta tags that are set on either the new or old entity.
559     $tags = [];
560     foreach (['old' => $old_entity, 'new' => $new_entity] as $entity_key => $entity) {
561       $language = metatag_entity_get_language($entity_type, $entity);
562       if (isset($entity->metatags[$language])) {
563         foreach ($entity->metatags[$language] as $key => $value) {
564           $tags[$key][$entity_key] = $value['value'];
565         }
566       }
567     }
568
569     $init_weight = 100;
570     foreach ($tags as $key => $values) {
571       $id = ucwords('Meta ' . $key);
572       // @todo Find the default values and show these if not set.
573       $result[$id] = [
574         '#name' => $id,
575         '#old' => [empty($values['old']) ? '' : $values['old']],
576         '#new' => [empty($values['new']) ? '' : $values['new']],
577         '#weight' => $init_weight++,
578         '#settings' => [
579           'show_header' => TRUE,
580         ],
581       ];
582     }
583   }
584   return $result;
585 }
586
587 /**
588  * Turn the meta tags for an entity into a human readable structure.
589  *
590  * @param object $entity
591  *   The entity object.
592  *
593  * @return array
594  *   All of the meta tags in a nested structure.
595  */
596 function metatag_generate_entity_metatags($entity) {
597   $values = [];
598   $raw = metatag_get_tags_from_route($entity);
599   if (!empty($raw['#attached']['html_head'])) {
600     foreach ($raw['#attached']['html_head'] as $tag) {
601       $values[$tag[1]] = $tag[0];
602     }
603   }
604   return $values;
605 }