2732fcc068b42577fa5dff2743882dcbf825f495
[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 $key => &$outer) {
84       if (is_array($outer)) {
85         foreach ($outer as $key => &$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   // If any Metatag items were found, append them.
121   if (!empty($metatag_attachments['#attached']['html_head'])) {
122     if (empty($attachments['#attached'])) {
123       $attachments['#attached'] = [];
124     }
125     if (empty($attachments['#attached']['html_head'])) {
126       $attachments['#attached']['html_head'] = [];
127     }
128
129     $head_links = [];
130     foreach ($metatag_attachments['#attached']['html_head'] as $item) {
131       $attachments['#attached']['html_head'][] = $item;
132
133       // Also add a HTTP header "Link:" for canonical URLs and shortlinks.
134       // See HtmlResponseAttachmentsProcessor::processHtmlHeadLink() for the
135       // implementation of the functionality in core.
136       if (in_array($item[1], ['canonical_url', 'shortlink'])) {
137         $attributes = $item[0]['#attributes'];
138
139         $href = '<' . Html::escape($attributes['href']) . '>';
140         unset($attributes['href']);
141         if ($param = drupal_http_header_attributes($attributes)) {
142           $href .= ';' . $param;
143         }
144         $head_links[] = $href;
145       }
146     }
147
148     // If any HTTP Header items were found, add them too.
149     if (!empty($head_links)) {
150       $attachments['#attached']['http_header'][] = [
151         'Link',
152         implode(', ', $head_links),
153         FALSE,
154       ];
155     }
156   }
157 }
158
159 /**
160  * Implements hook_module_implements_alter().
161  */
162 function metatag_module_implements_alter(&$implementations, $hook) {
163   if ($hook == 'page_attachments_alter') {
164     // Move metatag_page_attachments_alter() to the end of the list. This is so
165     // the canonical and shortlink tags can be removed that are added by
166     // taxonomy_page_attachments_alter().
167     // @todo Remove once https://www.drupal.org/node/2282029 is fixed.
168     $group = $implementations['metatag'];
169     unset($implementations['metatag']);
170     $implementations['metatag'] = $group;
171   }
172 }
173
174 /**
175  * Implements hook_page_attachments_alter().
176  */
177 function metatag_page_attachments_alter(array &$attachments) {
178   $route_match = \Drupal::routeMatch();
179   // Can be removed once https://www.drupal.org/node/2282029 is fixed.
180   if ($route_match->getRouteName() == 'entity.taxonomy_term.canonical' && ($term = $route_match->getParameter('taxonomy_term')) && $term instanceof TermInterface) {
181     _metatag_remove_duplicate_entity_tags($attachments);
182   }
183 }
184
185 /**
186  * Implements hook_entity_view_alter().
187  */
188 function metatag_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
189   // If this is a 403 or 404 page then don't output these meta tags.
190   // @todo Make the default meta tags load properly so this is unnecessary.
191   if ($display->getOriginalId() == 'node.403.default' || $display->getOriginalId() == 'node.404.default') {
192     $build['#attached']['html_head_link'] = [];
193     return;
194   }
195
196   _metatag_remove_duplicate_entity_tags($build);
197 }
198
199 /**
200  * Remove duplicate entity tags from a build.
201  *
202  * @param array $build
203  *   The build.
204  */
205 function _metatag_remove_duplicate_entity_tags(array &$build) {
206   // Some entities are built with a link rel="canonical" and/or link
207   // rel="shortlink" tag attached.
208   // If metatag provides them, remove the ones built with the entity.
209   if (isset($build['#attached']['html_head_link'])) {
210     $metatag_attachments = &drupal_static('metatag_attachments');
211     if (is_null($metatag_attachments)) {
212       // Load the meta tags from the route.
213       $metatag_attachments = metatag_get_tags_from_route();
214     }
215
216     // Check to see if the page currently outputs a canonical and/or shortlink
217     // tag.
218     if (isset($metatag_attachments['#attached']['html_head'])) {
219       foreach ($metatag_attachments['#attached']['html_head'] as $metatag_item) {
220         if (in_array($metatag_item[1], ['canonical_url', 'shortlink'])) {
221           // Metatag provides rel="canonical" and/or rel="shortlink" tags.
222           foreach ($build['#attached']['html_head_link'] as $key => $item) {
223             if (isset($item[0]['rel']) && in_array($item[0]['rel'], ['canonical', 'shortlink'])) {
224               // Remove the link rel="canonical" or link rel="shortlink" tag
225               // from the entity's build array.
226               unset($build['#attached']['html_head_link'][$key]);
227             }
228           }
229         }
230       }
231     }
232   }
233 }
234
235 /**
236  * Identify whether the current route is supported by the module.
237  *
238  * @return bool
239  *   TRUE if the current route is supported.
240  */
241 function metatag_is_current_route_supported() {
242   // If upgrading, we need to wait for database updates to complete.
243   $is_ready = \Drupal::service('entity_type.manager')
244     ->getDefinition('metatag_defaults', FALSE);
245   if (!$is_ready) {
246     return FALSE;
247   }
248
249   // Ignore admin paths.
250   if (\Drupal::service('router.admin_context')->isAdminRoute()) {
251     return FALSE;
252   }
253
254   return TRUE;
255 }
256
257 /**
258  * Returns the entity of the current route.
259  *
260  * @return Drupal\Core\Entity\EntityInterface
261  *   The entity or NULL if this is not an entity route.
262  */
263 function metatag_get_route_entity() {
264   $route_match = \Drupal::routeMatch();
265   $route_name = $route_match->getRouteName();
266
267   // Look for a canonical entity view page, e.g. node/{nid}, user/{uid}, etc.
268   $matches = [];
269   preg_match('/entity\.(.*)\.(latest[_-]version|canonical)/', $route_name, $matches);
270   if (!empty($matches[1])) {
271     $entity_type = $matches[1];
272     return $route_match->getParameter($entity_type);
273   }
274
275   // Look for a rest entity view page, e.g. "node/{nid}?_format=json", etc.
276   $matches = [];
277   // Matches e.g. "rest.entity.node.GET.json".
278   preg_match('/rest\.entity\.(.*)\.(.*)\.(.*)/', $route_name, $matches);
279   if (!empty($matches[1])) {
280     $entity_type = $matches[1];
281     return $route_match->getParameter($entity_type);
282   }
283
284   // Look for entity object 'add' pages, e.g. "node/add/{bundle}".
285   $route_name_matches = [];
286   preg_match('/(entity\.)?(.*)\.add(_form)?/', $route_name, $route_name_matches);
287   if (!empty($route_name_matches[2])) {
288     $entity_type = $route_name_matches[2];
289     $definition = Drupal::entityTypeManager()->getDefinition($entity_type, FALSE);
290     if (!empty($definition)) {
291       $type = $route_match->getRawParameter($definition->get('bundle_entity_type'));
292       if (!empty($type)) {
293         return \Drupal::entityTypeManager()
294           ->getStorage($entity_type)
295           ->create([
296             $definition->get('entity_keys')['bundle'] => $type,
297           ]);
298       }
299     }
300   }
301
302   // Look for entity object 'edit' pages, e.g. "node/{entity_id}/edit".
303   $route_name_matches = [];
304   preg_match('/entity\.(.*)\.edit_form/', $route_name, $route_name_matches);
305   if (!empty($route_name_matches[1])) {
306     $entity_type = $route_name_matches[1];
307     $entity_id = $route_match->getRawParameter($entity_type);
308
309     if (!empty($entity_id)) {
310       return \Drupal::entityTypeManager()
311         ->getStorage($entity_type)
312         ->load($entity_id);
313     }
314   }
315
316   // Look for entity object 'add content translation' pages, e.g.
317   // "node/{nid}/translations/add/{source_lang}/{translation_lang}".
318   $route_name_matches = [];
319   preg_match('/(entity\.)?(.*)\.content_translation_add/', $route_name, $route_name_matches);
320   if (!empty($route_name_matches[2])) {
321     $entity_type = $route_name_matches[2];
322     $definition = Drupal::entityTypeManager()->getDefinition($entity_type, FALSE);
323     if (!empty($definition)) {
324       $node = $route_match->getParameter($entity_type);
325       $type = $node->bundle();
326       if (!empty($type)) {
327         return \Drupal::entityTypeManager()
328           ->getStorage($entity_type)
329           ->create([
330             $definition->get('entity_keys')['bundle'] => $type,
331           ]);
332       }
333     }
334   }
335
336   // Special handling for the admin user_create page. In this case, there's only
337   // one bundle and it's named the same as the entity type, so some shortcuts
338   // can be used.
339   if ($route_name == 'user.admin_create') {
340     $entity_type = $type = 'user';
341     $definition = Drupal::entityTypeManager()->getDefinition($entity_type);
342     if (!empty($type)) {
343       return \Drupal::entityTypeManager()
344         ->getStorage($entity_type)
345         ->create([
346           $definition->get('entity_keys')['bundle'] => $type,
347         ]);
348     }
349   }
350
351   // Trigger hook_metatag_route_entity().
352   if ($entities = \Drupal::moduleHandler()->invokeAll('metatag_route_entity', [$route_match])) {
353     return reset($entities);
354   }
355
356   return NULL;
357 }
358
359 /**
360  * Implements template_preprocess_html().
361  */
362 function metatag_preprocess_html(&$variables) {
363   if (!metatag_is_current_route_supported()) {
364     return NULL;
365   }
366
367   $attachments = &drupal_static('metatag_attachments');
368   if (is_null($attachments)) {
369     $attachments = metatag_get_tags_from_route();
370   }
371
372   if (!$attachments) {
373     return NULL;
374   }
375
376   // Load the page title.
377   if (!empty($attachments['#attached']['html_head'])) {
378     foreach ($attachments['#attached']['html_head'] as $key => $attachment) {
379       if (!empty($attachment[1]) && $attachment[1] == 'title') {
380         // It's safe to access the value directly because it was already
381         // processed in MetatagManager::generateElements().
382         $variables['head_title_array'] = [];
383         // Empty head_title to avoid the site name and slogan to be appended to
384         // the meta title.
385         $variables['head_title'] = [];
386         $variables['head_title']['title'] = html_entity_decode($attachment[0]['#attributes']['content'], ENT_QUOTES);
387         // Original:
388         // $variables['head_title_array']['title'] =
389         //   $attachment[0]['#attributes']['content'];
390         // $variables['head_title'] = implode(' | ',
391         //   $variables['head_title_array']);
392         break;
393       }
394     }
395   }
396 }
397
398 /**
399  * Load the meta tags by processing the route parameters.
400  *
401  * @return mixed
402  *   Array of meta tags or NULL.
403  */
404 function metatag_get_tags_from_route($entity = NULL) {
405   $metatag_manager = \Drupal::service('metatag.manager');
406
407   // First, get defaults.
408   $metatags = metatag_get_default_tags($entity);
409   if (!$metatags) {
410     return NULL;
411   }
412
413   // Then, set tag overrides for this particular entity.
414   if (!$entity) {
415     $entity = metatag_get_route_entity();
416   }
417
418   if (!empty($entity) && $entity instanceof ContentEntityInterface) {
419     // If content entity does not have an ID the page is likely an "Add" page,
420     // so do not generate meta tags for entity which has not been created yet.
421     if (!$entity->id()) {
422       return NULL;
423     }
424
425     foreach ($metatag_manager->tagsFromEntity($entity) as $tag => $data) {
426       $metatags[$tag] = $data;
427     }
428   }
429
430   // Trigger hook_metatags_alter().
431   // Allow modules to override tags or the entity used for token replacements.
432   $context = [
433     'entity' => $entity,
434   ];
435   \Drupal::service('module_handler')->alter('metatags', $metatags, $context);
436
437   return $metatag_manager->generateElements($metatags, $entity);
438 }
439
440 /**
441  * Returns default tags for the current route.
442  *
443  * @return mixed
444  *   Array of tags or NULL;
445  */
446 function metatag_get_default_tags($entity = NULL) {
447   /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $global_metatag_manager */
448   $global_metatag_manager = \Drupal::entityTypeManager()->getStorage('metatag_defaults');
449   // First we load global defaults.
450   $metatags = $global_metatag_manager->load('global');
451   if (!$metatags) {
452     return NULL;
453   }
454
455   // Check if this is a special page.
456   $special_metatags = \Drupal::service('metatag.manager')->getSpecialMetatags();
457   if (isset($special_metatags)) {
458     $metatags->overwriteTags($special_metatags->get('tags'));
459   }
460
461   // Next check if there is this page is an entity that has meta tags.
462   else {
463     if (!$entity) {
464       $entity = metatag_get_route_entity();
465     }
466
467     if (!empty($entity) && $entity instanceof ContentEntityInterface) {
468       $entity_metatags = $global_metatag_manager->load($entity->getEntityTypeId());
469       if ($entity_metatags != NULL) {
470         // Merge with global defaults.
471         $metatags->overwriteTags($entity_metatags->get('tags'));
472       }
473
474       // Finally, check if bundle overrides should be added.
475       $bundle_metatags = $global_metatag_manager->load($entity->getEntityTypeId() . '__' . $entity->bundle());
476       if ($bundle_metatags != NULL) {
477         // Merge with existing defaults.
478         $metatags->overwriteTags($bundle_metatags->get('tags'));
479       }
480     }
481   }
482
483   return $metatags->get('tags');
484 }
485
486 /**
487  * Implements hook_entity_base_field_info().
488  */
489 function metatag_entity_base_field_info(EntityTypeInterface $entity_type) {
490   $fields = [];
491   $base_table = $entity_type->getBaseTable();
492   $canonical_template_exists = $entity_type->hasLinkTemplate('canonical');
493   // Certain classes are just not supported.
494   $original_class = $entity_type->getOriginalClass();
495   $classes_to_skip = [
496     'Drupal\comment\Entity\Comment',
497   ];
498
499   // If the entity type doesn't have a base table, has no link template then
500   // there's no point in supporting it.
501   if (!empty($base_table) && $canonical_template_exists && !in_array($original_class, $classes_to_skip)) {
502     $fields['metatag'] = BaseFieldDefinition::create('map')
503       ->setLabel(t('Metatags'))
504       ->setDescription(t('The meta tags for the entity.'))
505       ->setClass('\Drupal\metatag\Plugin\Field\MetatagEntityFieldItemList')
506       ->setQueryable(FALSE)
507       ->setComputed(TRUE)
508       ->setTargetEntityTypeId($entity_type->id());
509   }
510
511   return $fields;
512 }
513
514 /**
515  * Implements hook_entity_diff_options().
516  */
517 function metatag_entity_diff_options($entity_type) {
518   if (metatag_entity_supports_metatags($entity_type)) {
519     $options = [
520       'metatag' => t('Metatags'),
521     ];
522     return $options;
523   }
524 }
525
526 /**
527  * Implements hook_entity_diff().
528  */
529 function metatag_entity_diff($old_entity, $new_entity, $context) {
530   $result = [];
531   $entity_type = $context['entity_type'];
532   $options = variable_get('diff_additional_options_' . $entity_type, []);
533   if (!empty($options['metatag']) && metatag_entity_supports_metatags($entity_type)) {
534     // Find meta tags that are set on either the new or old entity.
535     $tags = [];
536     foreach (['old' => $old_entity, 'new' => $new_entity] as $entity_key => $entity) {
537       $language = metatag_entity_get_language($entity_type, $entity);
538       if (isset($entity->metatags[$language])) {
539         foreach ($entity->metatags[$language] as $key => $value) {
540           $tags[$key][$entity_key] = $value['value'];
541         }
542       }
543     }
544
545     $init_weight = 100;
546     foreach ($tags as $key => $values) {
547       $id = ucwords('Meta ' . $key);
548       // @todo Find the default values and show these if not set.
549       $result[$id] = [
550         '#name' => $id,
551         '#old' => [empty($values['old']) ? '' : $values['old']],
552         '#new' => [empty($values['new']) ? '' : $values['new']],
553         '#weight' => $init_weight++,
554         '#settings' => [
555           'show_header' => TRUE,
556         ],
557       ];
558     }
559   }
560   return $result;
561 }
562
563 /**
564  * Turn the meta tags for an entity into a human readable structure.
565  *
566  * @param object $entity
567  *   The entity object.
568  *
569  * @return array
570  *   All of the meta tags in a nested structure.
571  */
572 function metatag_generate_entity_metatags($entity) {
573   $values = [];
574   $raw = metatag_get_tags_from_route($entity);
575   if (!empty($raw['#attached']['html_head'])) {
576     foreach ($raw['#attached']['html_head'] as $tag) {
577       $values[$tag[1]] = $tag[0];
578     }
579   }
580   return $values;
581 }