Commit 4f36dae5 authored by Robert Wohleb's avatar Robert Wohleb Committed by Damien McKenna
Browse files

Issue #2862747 by JeroenT, DanielVeza, rwohleb, joshua.boltz, henrikakselsen,...

Issue #2862747 by JeroenT, DanielVeza, rwohleb, joshua.boltz, henrikakselsen, Vitalyos, Phil Wolstenholme: Tokens to access individual meta tag values.
parent 28c317d4
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -33,6 +33,8 @@ Metatag 8.x-1.x-dev, xxxx-xx-xx
  assertCount().
#3108052 by Berdir, SpadXIII: metatag_get_default_tags() does not revert config
  override language.
#2862747 by JeroenT, DanielVeza, rwohleb, joshua.boltz, henrikakselsen,
  Vitalyos, Phil Wolstenholme: Tokens to access individual meta tag values.


Metatag 8.x-1.14, 2020-08-11

metatag.tokens.inc

0 → 100644
+228 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Metatag token integration.
 */

use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Render\BubbleableMetadata;

/**
 * Implements hook_token_info().
 */
function metatag_token_info() {
  $info = [];

  $group_manager = \Drupal::service('plugin.manager.metatag.group');
  $tag_manager = \Drupal::service('plugin.manager.metatag.tag');
  $tag_definitions = $tag_manager->getDefinitions();

  $info['types']['metatag'] = [
    'name' => t('Metatags'),
    'description' => t('Tokens related to Metatags.'),
    'needs-data' => 'metatag',
  ];

  foreach ($tag_definitions as $tag_id => $tag_definition) {
    $label = $tag_definition['label'];
    $description = $tag_definition['description'];
    $multiple = $tag_definition['multiple'];
    $metatag_token_name = 'metatag-' . $tag_id;

    $group = $group_manager->getDefinition($tag_definition['group']);
    if ($group) {
      $label = $group['label'] . ': ' . $label;
    }

    $info['tokens']['current-page']['metatag'] = [
      'name' => t('Metatags'),
      'description' => t('Metatag values for the current page.'),
      'type' => 'metatag',
    ];
    $info['tokens']['metatag'][$tag_id] = [
      'name' => Html::escape($label),
      'description' => $description,
      'type' => $multiple ? "list<$metatag_token_name>" : $metatag_token_name,
    ];

    $info['types'][$metatag_token_name] = [
      'name' => Html::escape($label),
      'description' => t('@label tokens.', ['@label' => Html::escape($label)]),
      'needs-data' => $metatag_token_name,
      'nested' => TRUE,
    ];

    // Tag list token type.
    if ($multiple) {
      $info['types']["list<$metatag_token_name>"] = [
        'name' => t('List of @type values', ['@type' => Html::escape($label)]),
        'description' => t('Tokens for lists of @type values.', ['@type' => Html::escape($label)]),
        'needs-data' => "list<$metatag_token_name>",
        'nested' => TRUE,
      ];

      // Show a different token for each tag delta.
      // Since we don't know how many there will be, we will just show 3.
      for ($delta = 0; $delta < 3; $delta++) {
        $info['tokens']["list<$metatag_token_name>"][$delta] = [
          'name' => t('@type type with delta @delta', [
            '@type' => Html::escape($label),
            '@delta' => $delta,
          ]),
          'module' => 'token',
          'type' => $metatag_token_name,
        ];
      }
    }
  }

  return $info;
}

/**
 * Implements hook_token_info_alter().
 */
function metatag_token_info_alter(&$info) {
  foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
    if (!$entity_type->entityClassImplements(ContentEntityInterface::class)) {
      continue;
    }

    // Make sure a token type exists for this entity.
    $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity_type_id);
    if (empty($token_type) || !isset($info['types'][$token_type])) {
      continue;
    }

    $fields = \Drupal::entityTypeManager()->getStorage('field_storage_config')->loadByProperties([
      'entity_type' => $entity_type_id,
      'type' => 'metatag',
    ]);
    foreach ($fields as $field) {
      $field_token_name = $token_type . '-' . $field->getName();
      $info['types'][$field_token_name] = [
        'name' => Html::escape($field->getName()),
        'description' => t('@label tokens.', ['@label' => Html::escape($field->getName())]),
        'needs-data' => $field_token_name,
        'nested' => TRUE,
        'type' => 'metatag',
        'module' => 'metatag',
      ];
    }
  }
}

/**
 * Implements hook_tokens().
 */
function metatag_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
  $replacements = [];

  switch ($type) {
    case 'current-page':
      /** @var \Drupal\token\TokenInterface $token_service */
      $token_service = \Drupal::token();
      $metatag_tokens = $token_service->findWithPrefix($tokens, 'metatag');
      if (!empty($metatag_tokens) && metatag_is_current_route_supported()) {
        // Add cache contexts to ensure this token functions on a per-path
        // basis.
        $bubbleable_metadata->addCacheContexts(['url.site']);
        $replacements += $token_service->generate('metatag', $metatag_tokens, [], $options, $bubbleable_metadata);
      }
      break;

    case 'entity':
      if (!empty($data['entity_type']) && !empty($data['entity']) && !empty($data['token_type'])) {
        /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
        $entity = $data['entity'];
        if (!($entity instanceof ContentEntityInterface)) {
          return $replacements;
        }

        $metatag_fields = [];
        foreach ($tokens as $name => $original) {
          $field_name = explode(':', $name)[0];

          if ($entity->hasField($field_name) && $entity->get($field_name)->getFieldDefinition()->getType() === 'metatag') {
            $metatag_fields[] = $field_name;
          }
        }

        if (!empty($metatag_fields)) {
          /** @var \Drupal\token\TokenInterface $token_service */
          $token_service = \Drupal::token();
          $metatag_tokens = [];
          foreach ($metatag_fields as $metatag_field) {
            $metatag_tokens += $token_service->findWithPrefix($tokens, $metatag_field);
          }
          $replacements += $token_service->generate('metatag', $metatag_tokens, ['entity' => $entity], $options, $bubbleable_metadata);
        }
      }
      break;

    case 'metatag':
      $metatag_manager = \Drupal::service('metatag.manager');

      $entity = $options['entity'] ?? metatag_get_route_entity();
      $tags = metatag_get_default_tags($entity);
      if ($entity instanceof ContentEntityInterface) {
        // If content entity does not have an ID the page is likely an "Add"
        // page, so skip processing for entity which has not been created yet.
        if (!$entity->id()) {
          return NULL;
        }

        $tags += $metatag_manager->tagsFromEntity($entity);
      }

      // Trigger hook_metatags_alter().
      // Allow modules to override tags or the entity used for token
      // replacements.
      $context = [
        'entity' => &$entity,
      ];
      \Drupal::service('module_handler')->alter('metatags', $tags, $context);

      // If the entity was changed above, use that for generating the meta tags.
      if (isset($context['entity'])) {
        $entity = $context['entity'];
      }

      $processed_tags = $metatag_manager->generateTokenValues($tags, $entity);

      foreach ($tokens as $name => $original) {
        // For the [metatag:tag_name] token.
        if (strpos($name, ':') === FALSE) {
          $tag_name = $name;
        }
        // For [metatag:tag_name:0], [metatag:tag_name:0:value] and
        // [metatag:tag_name:value] tokens.
        else {
          [$tag_name, $delta] = explode(':', $name, 2);
          if (!is_numeric($delta)) {
            unset($delta);
          }
        }

        // Replace dashes (-) with underscores (_) for e.g. canonical-url.
        $tag_name = str_replace('-', '_', $tag_name);

        if (empty($processed_tags[$tag_name])) {
          continue;
        }

        // Render only one delta.
        if (isset($delta)) {
          $replacements[$original] = $processed_tags[$tag_name][$delta];
        }
        else {
          $replacements[$original] = is_array($processed_tags[$tag_name]) ? implode(',', $processed_tags[$tag_name]) : $processed_tags[$tag_name];
        }
      }
      break;
  }

  return $replacements;
}
+76 −4
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

namespace Drupal\metatag;

use Drupal\Component\Render\PlainTextOutput;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageInterface;
@@ -54,6 +55,13 @@ class MetatagManager implements MetatagManagerInterface {
   */
  protected $logger;

  /**
   * Caches processed strings, keyed by tag name.
   *
   * @var array
   */
  protected $processedTokenCache = [];

  /**
   * Constructor for MetatagManager.
   *
@@ -72,7 +80,8 @@ class MetatagManager implements MetatagManagerInterface {
    MetatagTagPluginManager $tagPluginManager,
    MetatagToken $token,
    LoggerChannelFactoryInterface $channelFactory,
      EntityTypeManagerInterface $entityTypeManager) {
    EntityTypeManagerInterface $entityTypeManager
  ) {
    $this->groupPluginManager = $groupPluginManager;
    $this->tagPluginManager = $tagPluginManager;
    $this->tokenService = $token;
@@ -593,6 +602,69 @@ class MetatagManager implements MetatagManagerInterface {
    return $rawTags;
  }

  /**
   * Generate the actual meta tag values for use as tokens.
   *
   * @param array $tags
   *   The array of tags as plugin_id => value.
   * @param object $entity
   *   Optional entity object to use for token replacements.
   *
   * @return array
   *   Array of MetatagTag plugin instances.
   */
  public function generateTokenValues(array $tags, $entity = NULL) {
    // Ignore the update.php path.
    $request = \Drupal::request();
    if ($request->getBaseUrl() == '/update.php') {
      return [];
    }

    $entity_identifier = '_none';
    if ($entity) {
      $entity_identifier = $entity->getEntityTypeId() . ':' . ($entity->uuid() ?: $entity->id());
    }

    if (!isset($this->processedTokenCache[$entity_identifier])) {
      $metatag_tags = $this->tagPluginManager->getDefinitions();

      // Each element of the $values array is a tag with the tag plugin name as
      // the key.
      foreach ($tags as $tag_name => $value) {
        // Check to ensure there is a matching plugin.
        if (isset($metatag_tags[$tag_name])) {
          // Get an instance of the plugin.
          $tag = $this->tagPluginManager->createInstance($tag_name);

          // Render any tokens in the value.
          $token_replacements = [];
          if ($entity) {
            // @todo This needs a better way of discovering the context.
            if ($entity instanceof ViewEntityInterface) {
              // Views tokens require the ViewExecutable, not the config entity.
              // @todo Can we move this into metatag_views somehow?
              $token_replacements = ['view' => $entity->getExecutable()];
            }
            elseif ($entity instanceof ContentEntityInterface) {
              $token_replacements = [$entity->getEntityTypeId() => $entity];
            }
          }

          // Set the value as sometimes the data needs massaging, such as when
          // field defaults are used for the Robots field, which come as an
          // array that needs to be filtered and converted to a string.
          // @see Robots::setValue()
          $tag->setValue($value);
          $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
          $value = PlainTextOutput::renderFromHtml(htmlspecialchars_decode($this->tokenService->replace($value, $token_replacements, ['langcode' => $langcode])));
          $this->processedTokenCache[$entity_identifier][$tag_name] = $tag->multiple() ? explode(',', $value) : $value;
        }
      }
    }

    return $this->processedTokenCache[$entity_identifier];
  }

  /**
   * Returns a list of fields handled by Metatag.
   *
+113 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\metatag\Functional;

use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
use Drupal\Tests\token\Functional\TokenTestTrait;

/**
 * Verify that metatag token generation is working.
 *
 * @group metatag
 */
class MetatagTokenTest extends BrowserTestBase {

  use TokenTestTrait;
  use FieldUiTestTrait;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'block',
    'field_ui',
    'user',
    'token',
    'token_module_test',
    'metatag',
    'metatag_open_graph',
  ];

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * {@inheritdoc}
   */
  public function setUp(): void {
    parent::setUp();

    $this->drupalPlaceBlock('system_breadcrumb_block');
    $this->drupalPlaceBlock('local_tasks_block');
    $this->drupalPlaceBlock('page_title_block');

    $this->drupalLogin($this->rootUser);
    $this->fieldUIAddNewField('/admin/config/people/accounts', 'metatags', 'Metatags', 'metatag');
  }

  /**
   * Test current-page metatag token generation.
   */
  public function testMetatagCurrentPageTokens() {
    $user = $this->createUser([]);
    $this->drupalGet($user->toUrl('edit-form'));
    $this->submitForm([
      'field_metatags[0][basic][abstract]' => 'My abstract',
      'field_metatags[0][open_graph][og_title]' => 'My OG Title',
      'field_metatags[0][open_graph][og_image]' => 'Image 1,Image 2',
    ], 'Save');

    $tokens = [
      // Test globally configured metatags.
      '[current-page:metatag:title]' => sprintf('%s | %s', $user->getAccountName(), $this->config('system.site')
        ->get('name')),
      '[current-page:metatag:description]' => $this->config('system.site')
        ->get('name'),
      '[current-page:metatag:canonical-url]' => $user->toUrl('canonical', ['absolute' => TRUE])
        ->toString(),
      // Test entity overridden metatags.
      '[current-page:metatag:abstract]' => 'My abstract',
      // Test metatags provided by a submodule.
      '[current-page:metatag:og-title]' => 'My OG Title',
      // Test metatags that can contain multiple values.
      '[current-page:metatag:og_image]' => 'Image 1,Image 2',
      '[current-page:metatag:og_image:0]' => 'Image 1',
      '[current-page:metatag:og_image:1]' => 'Image 2',
    ];
    $this->assertPageTokens($user->toUrl(), $tokens);
  }

  /**
   * Test entity token generation.
   */
  public function testMetatagEntityTokens() {
    $user = $this->createUser();
    $this->drupalGet($user->toUrl('edit-form'));
    $this->submitForm([
      'field_metatags[0][basic][abstract]' => 'My abstract',
      'field_metatags[0][open_graph][og_title]' => 'My OG Title',
      'field_metatags[0][open_graph][og_image]' => 'Image 1,Image 2',
    ], 'Save');

    $tokens = [
      // Test globally configured metatags.
      '[user:field_metatags:title]' => sprintf('%s | %s', $user->getAccountName(), $this->config('system.site')->get('name')),
      '[user:field_metatags:description]' => $this->config('system.site')->get('name'),
      '[user:field_metatags:canonical-url]' => $user->toUrl('canonical', ['absolute' => TRUE])->toString(),
      // Test entity overridden metatags.
      '[user:field_metatags:abstract]' => 'My abstract',
      // Test metatags provided by a submodule.
      '[user:field_metatags:og-title]' => 'My OG Title',
      // Test metatags that can contain multiple values.
      '[user:field_metatags:og_image]' => 'Image 1,Image 2',
      '[user:field_metatags:og_image:0]' => 'Image 1',
      '[user:field_metatags:og_image:1]' => 'Image 2',
    ];

    $this->assertPageTokens($user->toUrl(), $tokens, ['user' => $user]);
  }

}