Pathauto and dependencies

This commit is contained in:
Rob Davies 2017-05-22 15:12:47 +01:00
parent 4b1a293d57
commit 24ffcb956b
257 changed files with 29510 additions and 0 deletions

View file

@ -0,0 +1,78 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Controller\ControllerBase;
use Drupal\token\TreeBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns autocomplete responses for tokens.
*/
class TokenAutocompleteController extends ControllerBase {
/**
* @var \Drupal\token\TreeBuilderInterface
*/
protected $treeBuilder;
public function __construct(TreeBuilderInterface $tree_builder) {
$this->treeBuilder = $tree_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('token.tree_builder')
);
}
/**
* Retrieves suggestions for block category autocompletion.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param string $token_type
* The token type.
* @param string $filter
* The autocomplete filter.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing autocomplete suggestions.
*/
public function autocomplete($token_type, $filter, Request $request) {
$filter = substr($filter, strrpos($filter, '['));
$matches = array();
if (!Unicode::strlen($filter)) {
$matches["[{$token_type}:"] = 0;
}
else {
$depth = max(1, substr_count($filter, ':'));
$tree = $this->treeBuilder->buildTree($token_type, ['flat' => TRUE, 'depth' => $depth]);
foreach (array_keys($tree) as $token) {
if (strpos($token, $filter) === 0) {
$matches[$token] = levenshtein($token, $filter);
if (isset($tree[$token]['children'])) {
$token = rtrim($token, ':]') . ':';
$matches[$token] = levenshtein($token, $filter);
}
}
}
}
asort($matches);
$keys = array_keys($matches);
$matches = array_combine($keys, $keys);
return new JsonResponse($matches);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Clears cache for tokens.
*/
class TokenCacheController extends ControllerBase {
/**
* Clear caches and redirect back to the frontpage.
*/
public function flush() {
token_clear_cache();
drupal_set_message(t('Token registry caches cleared.'));
return $this->redirect('<front>');
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\token\TokenEntityMapperInterface;
use Drupal\token\TreeBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Devel integration for tokens.
*/
class TokenDevelController extends ControllerBase {
/**
* @var \Drupal\token\TreeBuilderInterface
*/
protected $treeBuilder;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $entityMapper;
public function __construct(TreeBuilderInterface $tree_builder, TokenEntityMapperInterface $entity_mapper) {
$this->treeBuilder = $tree_builder;
$this->entityMapper = $entity_mapper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('token.tree_builder'),
$container->get('token.entity_mapper')
);
}
/**
* Prints the loaded structure of the current entity.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function entityTokens(RouteMatchInterface $route_match) {
$output = [];
$parameter_name = $route_match->getRouteObject()->getOption('_token_entity_type_id');
$entity = $route_match->getParameter($parameter_name);
if ($entity && $entity instanceof EntityInterface) {
$output = $this->renderTokenTree($entity);
}
return $output;
}
/**
* Render the token tree for the specified entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which the token tree should be rendered.
*
* @return array
* Render array of the token tree for the $entity.
*
* @see static::entityLoad
*/
protected function renderTokenTree(EntityInterface $entity) {
$this->moduleHandler()->loadInclude('token', 'pages.inc');
$entity_type = $entity->getEntityTypeId();
$token_type = $this->entityMapper->getTokenTypeForEntityType($entity_type);
$options = [
'flat' => TRUE,
'values' => TRUE,
'data' => [$token_type => $entity],
];
$token_tree = [
$token_type => [
'tokens' => $this->treeBuilder->buildTree($token_type, $options),
],
];
// foreach ($tree as $token => $token_info) {
// if (!isset($token_info['value']) && !empty($token_info['parent']) && !isset($tree[$token_info['parent']]['value'])) {
// continue;
// }
// }
$build['tokens'] = [
'#type' => 'token_tree_table',
'#show_restricted' => FALSE,
'#show_nested' => FALSE,
'#skip_empty_values' => TRUE,
'#token_tree' => $token_tree,
'#columns' => ['token', 'value'],
'#empty' => $this->t('No tokens available.'),
];
return $build;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\token\TreeBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns tree responses for tokens.
*/
class TokenTreeController extends ControllerBase {
/**
* @var \Drupal\token\TreeBuilderInterface
*/
protected $treeBuilder;
public function __construct(TreeBuilderInterface $tree_builder) {
$this->treeBuilder = $tree_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('token.tree_builder')
);
}
/**
* Page callback to output a token tree as an empty page.
*/
function outputTree(Request $request) {
$options = $request->query->has('options') ? Json::decode($request->query->get('options')) : [];
// The option token_types may only be an array OR 'all'. If it is not set,
// we assume that only global token types are requested.
$token_types = !empty($options['token_types']) ? $options['token_types'] : [];
if ($token_types == 'all') {
$build = $this->treeBuilder->buildAllRenderable($options);
}
else {
$build = $this->treeBuilder->buildRenderable($token_types, $options);
}
$build['#cache']['contexts'][] = 'url.query_args:options';
$build['#title'] = $this->t('Available tokens');
return $build;
}
}

View file

@ -0,0 +1,155 @@
<?php
namespace Drupal\token\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Element\Table;
/**
* Provides a render element for a token tree table.
*
* @RenderElement("token_tree_table")
*/
class TokenTreeTable extends Table {
protected static $cssFilter = [' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '', ':' => '--', '?' => '', '<' => '-', '>' => '-'];
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#header' => [],
'#rows' => [],
'#token_tree' => [],
'#columns' => ['name', 'token', 'description'],
'#empty' => '',
'#show_restricted' => FALSE,
'#show_nested' => FALSE,
'#skip_empty_values' => FALSE,
'#click_insert' => TRUE,
'#sticky' => FALSE,
'#responsive' => TRUE,
'#input' => FALSE,
'#pre_render' => [
[$class, 'preRenderTokenTree'],
[$class, 'preRenderTable'],
],
'#theme' => 'table__token_tree',
'#attached' => [
'library' => [
'token/token',
],
],
];
}
/**
* Pre-render the token tree to transform rows in the token tree.
*
* @param array $element
*
* @return array
* The processed element.
*/
public static function preRenderTokenTree($element) {
$multiple_token_types = count($element['#token_tree']) > 1;
foreach ($element['#token_tree'] as $token_type => $type_info) {
// Do not show nested tokens.
if (!empty($type_info['nested']) && empty($element['#show_nested'])) {
continue;
}
if ($multiple_token_types) {
$row = static::formatRow($token_type, $type_info, $element['#columns'], TRUE);
$element['#rows'][] = $row;
}
foreach ($type_info['tokens'] as $token => $token_info) {
if (!empty($token_info['restricted']) && empty($element['#show_restricted'])) {
continue;
}
if ($element['#skip_empty_values'] && empty($token_info['value']) && !empty($token_info['parent']) && !isset($tree[$token_info['parent']]['value'])) {
continue;
}
if ($multiple_token_types && !isset($token_info['parent'])) {
$token_info['parent'] = $token_type;
}
$row = static::formatRow($token, $token_info, $element['#columns']);
$element['#rows'][] = $row;
}
}
if (!empty($element['#rows'])) {
$element['#attached']['library'][] = 'token/jquery.treeTable';
}
// Fill headers if one is not specified.
if (empty($element['#header'])) {
$column_map = [
'name' => t('Name'),
'token' => t('Token'),
'value' => t('Value'),
'description' => t('Description'),
];
foreach ($element['#columns'] as $col) {
$element['#header'][] = $column_map[$col];
}
}
$element['#attributes']['class'][] = 'token-tree';
if ($element['#click_insert']) {
$element['#caption'] = t('Click a token to insert it into the field you\'ve last clicked.');
$element['#attributes']['class'][] = 'token-click-insert';
}
return $element;
}
protected static function cleanCssIdentifier($id) {
return 'token-' . Html::cleanCssIdentifier(trim($id, '[]'), static::$cssFilter);
}
protected static function formatRow($token, $token_info, $columns, $is_group = FALSE) {
$row = [
'id' => static::cleanCssIdentifier($token),
'data-tt-id' => static::cleanCssIdentifier($token),
'class' => [],
'data' => [],
];
foreach ($columns as $col) {
switch ($col) {
case 'name':
$row['data'][$col] = $token_info['name'];
break;
case 'token':
$row['data'][$col]['data'] = $token;
$row['data'][$col]['class'][] = 'token-key';
break;
case 'description':
$row['data'][$col] = isset($token_info['description']) ? $token_info['description'] : '';
break;
case 'value':
$row['data'][$col] = !$is_group && isset($token_info['value']) ? $token_info['value'] : '';
break;
}
}
if ($is_group) {
// This is a token type/group.
$row['class'][] = 'token-group';
}
elseif (!empty($token_info['parent'])) {
$row['data-tt-parent-id'] = static::cleanCssIdentifier($token_info['parent']);
unset($row['parent']);
}
return $row;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\token\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class DevelLocalTask extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
protected $entityTypeManager;
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->hasLinkTemplate('token-devel')) {
$this->derivatives["$entity_type_id.token_devel_tab"] = [
'route_name' => "entity.$entity_type_id.token_devel",
'weight' => 110,
'title' => $this->t('Tokens'),
'parent_id' => "devel.entities:$entity_type_id.devel_tab",
];
}
}
foreach ($this->derivatives as &$entry) {
$entry += $base_plugin_definition;
}
return $this->derivatives;
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Drupal\token\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for Devel routes.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($devel_render = $entity_type->getLinkTemplate('token-devel')) {
$options = [
'_admin_route' => TRUE,
'_token_entity_type_id' => $entity_type_id,
'parameters' => [
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
],
],
];
$route = new Route(
$devel_render,
[
'_controller' => '\Drupal\token\Controller\TokenDevelController::entityTokens',
'_title' => 'Devel Tokens',
],
[
'_permission' => 'access devel information',
'_module_dependencies' => 'devel',
],
$options
);
$collection->add("entity.$entity_type_id.token_devel", $route);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = parent::getSubscribedEvents();
$events[RoutingEvents::ALTER] = array('onAlterRoutes', 100);
return $events;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Drupal\token\Tests;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
/**
* Tests block tokens.
*
* @group token
*/
class TokenBlockTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['block', 'node', 'views', 'block_content'];
/**
* {@inheritdoc}
*/
public function setUp($modules = array()) {
parent::setUp();
$this->admin_user = $this->drupalCreateUser(array('access content', 'administer blocks'));
$this->drupalLogin($this->admin_user);
}
public function testBlockTitleTokens() {
$label = 'tokenblock';
$bundle = BlockContentType::create(array(
'id' => $label,
'label' => $label,
'revision' => FALSE
));
$bundle->save();
$block_content = BlockContent::create(array(
'type' => $label,
'label' => '[current-page:title] block title',
'info' => 'Test token title block',
'body[value]' => 'This is the test token title block.',
));
$block_content->save();
$block = $this->drupalPlaceBlock('block_content:' . $block_content->uuid(), array(
'label' => '[user:name]',
));
$this->drupalGet($block->urlInfo());
// Ensure that the link to available tokens is present and correctly
// positioned.
$this->assertLink('Browse available tokens.');
$this->assertText('This field supports tokens. Browse available tokens.');
$this->drupalPostForm(NULL, array(), t('Save block'));
// Ensure token validation is working on the block.
$this->assertText('Title is using the following invalid tokens: [user:name].');
// Create the block for real now with a valid title.
$settings = $block->get('settings');
$settings['label'] = '[current-page:title] block title';
$block->set('settings', $settings);
$block->save();
// Ensure that tokens are not double-escaped when output as a block title.
$this->drupalCreateContentType(array('type' => 'page'));
$node = $this->drupalCreateNode(array('title' => "Site's first node"));
$this->drupalGet('node/' . $node->id());
// The apostraphe should only be escaped once.
$this->assertRaw("Site&#039;s first node block title");
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Drupal\token\Tests;
use Drupal\Core\Url;
/**
* Test the [current-page:*] tokens.
*
* @group token
*/
class TokenCurrentPageTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node');
function testCurrentPageTokens() {
$tokens = array(
'[current-page:title]' => t('Log in'),
'[current-page:url]' => Url::fromRoute('user.login', [], array('absolute' => TRUE))->toString(),
'[current-page:url:absolute]' => Url::fromRoute('user.login', [], array('absolute' => TRUE))->toString(),
'[current-page:url:relative]' => Url::fromRoute('user.login')->toString(),
'[current-page:url:path]' => '/user/login',
'[current-page:url:args:value:0]' => 'user',
'[current-page:url:args:value:1]' => 'login',
'[current-page:url:args:value:2]' => NULL,
'[current-page:url:unaliased]' => Url::fromRoute('user.login', [], array('absolute' => TRUE, 'alias' => TRUE))->toString(),
'[current-page:page-number]' => 1,
'[current-page:query:foo]' => NULL,
'[current-page:query:bar]' => NULL,
// Deprecated tokens
'[current-page:arg:0]' => 'user',
'[current-page:arg:1]' => 'login',
'[current-page:arg:2]' => NULL,
);
$this->assertPageTokens('user/login', $tokens);
$this->drupalCreateContentType(array('type' => 'page'));
$node = $this->drupalCreateNode(array('title' => 'Node title', 'path' => array('alias' => '/node-alias')));
$tokens = array(
'[current-page:title]' => 'Node title',
'[current-page:url]' => $node->url('canonical', array('absolute' => TRUE)),
'[current-page:url:absolute]' => $node->url('canonical', array('absolute' => TRUE)),
'[current-page:url:relative]' => $node->url(),
'[current-page:url:alias]' => '/node-alias',
'[current-page:url:args:value:0]' => 'node-alias',
'[current-page:url:args:value:1]' => NULL,
'[current-page:url:unaliased]' => $node->url('canonical', array('absolute' => TRUE, 'alias' => TRUE)),
'[current-page:url:unaliased:args:value:0]' => 'node',
'[current-page:url:unaliased:args:value:1]' => $node->id(),
'[current-page:url:unaliased:args:value:2]' => NULL,
'[current-page:page-number]' => 1,
'[current-page:query:foo]' => 'bar',
'[current-page:query:bar]' => NULL,
// Deprecated tokens
'[current-page:arg:0]' => 'node',
'[current-page:arg:1]' => 1,
'[current-page:arg:2]' => NULL,
);
$this->assertPageTokens("/node/{$node->id()}", $tokens, array(), array('url_options' => array('query' => array('foo' => 'bar'))));
}
}

View file

@ -0,0 +1,280 @@
<?php
namespace Drupal\token\Tests;
use Drupal\node\Entity\NodeType;
use Drupal\node\Entity\Node;
use Drupal\file\Entity\File;
use Drupal\image\Entity\ImageStyle;
/**
* Tests field ui.
*
* @group token
*/
class TokenFieldUiTest extends TokenTestBase {
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['field_ui', 'node', 'image'];
/**
* {@inheritdoc}
*/
public function setUp($modules = []) {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['administer content types', 'administer node fields']);
$this->drupalLogin($this->adminUser);
$node_type = NodeType::create([
'type' => 'article',
'name' => 'Article',
'description' => "Use <em>articles</em> for time-sensitive content like news, press releases or blog posts.",
]);
$node_type->save();
entity_create('field_storage_config', array(
'field_name' => 'field_body',
'entity_type' => 'node',
'type' => 'text_with_summary',
))->save();
entity_create('field_config', array(
'field_name' => 'field_body',
'label' => 'Body',
'entity_type' => 'node',
'bundle' => 'article',
))->save();
entity_create('field_storage_config', array(
'field_name' => 'field_image',
'entity_type' => 'node',
'type' => 'image',
))->save();
entity_create('field_config', array(
'field_name' => 'field_image',
'label' => 'Image',
'entity_type' => 'node',
'bundle' => 'article',
))->save();
entity_create('field_storage_config', array(
'field_name' => 'field_image_2',
'entity_type' => 'node',
'type' => 'image',
))->save();
entity_create('field_config', array(
'field_name' => 'field_image_2',
'label' => 'Image 2',
'entity_type' => 'node',
'bundle' => 'article',
))->save();
entity_create('field_storage_config', array(
'field_name' => 'multivalued_field_image',
'entity_type' => 'node',
'type' => 'image',
))->save();
entity_create('field_config', array(
'field_name' => 'multivalued_field_image',
'label' => 'Multivalued field image',
'entity_type' => 'node',
'bundle' => 'article',
))->save();
entity_get_form_display('node', 'article', 'default')
->setComponent('field_body', [
'type' => 'text_textarea_with_summary',
'settings' => [
'rows' => '9',
'summary_rows' => '3',
],
'weight' => 5,
])
->save();
}
public function testFileFieldUi() {
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image');
// Ensure the 'Browse available tokens' link is present and correct.
$this->assertLink('Browse available tokens.');
$this->assertLinkByHref('token/tree');
// Ensure that the default file directory value validates correctly.
$this->drupalPostForm(NULL, [], t('Save settings'));
$this->assertText(t('Saved Image configuration.'));
}
public function testFieldDescriptionTokens() {
$edit = [
'description' => 'The site is called [site:name].',
];
$this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_body', $edit, 'Save settings');
$this->drupalGet('node/add/article');
$this->assertText('The site is called Drupal.');
}
/**
* Test that tokens are correctly provided and replaced for the image fields.
*/
public function testImageFieldTokens() {
// Generate 2 different test images.
file_unmanaged_copy(\Drupal::root() . '/core/misc/druplicon.png', 'public://example1.png');
file_unmanaged_copy(\Drupal::root() . '/core/misc/loading.gif', 'public://example2.gif');
// Resize the test images so that they will be scaled down during token
// replacement.
$image1 = \Drupal::service('image.factory')->get('public://example1.png');
$image1->resize(500, 500);
$image1->save();
$image2 = \Drupal::service('image.factory')->get('public://example2.gif');
$image2->resize(500, 500);
$image2->save();
/** @var \Drupal\file\Entity\File $image1 */
$image1 = File::create(['uri' => 'public://example1.png']);
$image1->save();
/** @var \Drupal\file\Entity\File $image2 */
$image2 = File::create(['uri' => 'public://example2.gif']);
$image2->save();
$node = Node::create([
'title' => 'Test node title',
'type' => 'article',
'field_image' => [
[
'target_id' => $image1->id(),
],
],
'field_image_2' => [
[
'target_id' => $image2->id(),
],
],
'multivalued_field_image' => [
['target_id' => $image1->id()],
['target_id' => $image2->id()],
],
]);
$node->save();
// Obtain the file size and dimension of the images that will be scaled
// down during token replacement by applying the styles here.
$style_thumbnail = ImageStyle::load('thumbnail');
$style_thumbnail->createDerivative('public://example1.png', 'public://styles/thumbnail/public/example1-test.png');
$style_thumbnail->createDerivative('public://example2.gif', 'public://styles/thumbnail/public/example2-test.gif');
$image_1_thumbnail = \Drupal::service('image.factory')->get('public://styles/thumbnail/public/example1-test.png');
$image_2_thumbnail = \Drupal::service('image.factory')->get('public://styles/thumbnail/public/example2-test.gif');
$style_medium = ImageStyle::load('medium');
$style_medium->createDerivative('public://example1.png', 'public://styles/medium/public/example1-test.png');
$style_medium->createDerivative('public://example2.gif', 'public://styles/medium/public/example2-test.gif');
$image_1_medium = \Drupal::service('image.factory')->get('public://styles/medium/public/example1-test.png');
$image_2_medium = \Drupal::service('image.factory')->get('public://styles/medium/public/example2-test.gif');
$style_large = ImageStyle::load('large');
$style_large->createDerivative('public://example1.png', 'public://styles/large/public/example1-test.png');
$style_large->createDerivative('public://example2.gif', 'public://styles/large/public/example2-test.gif');
$image_1_large = \Drupal::service('image.factory')->get('public://styles/large/public/example1-test.png');
$image_2_large = \Drupal::service('image.factory')->get('public://styles/large/public/example2-test.gif');
// Delete the image derivatives, to make sure they are re-created.
unlink('public://styles/thumbnail/public/example1-test.png');
unlink('public://styles/medium/public/example1-test.png');
unlink('public://styles/large/public/example1-test.png');
unlink('public://styles/thumbnail/public/example2-test.gif');
unlink('public://styles/medium/public/example2-test.gif');
unlink('public://styles/large/public/example2-test.gif');
$tokens = [
// field_image
'field_image:thumbnail:mimetype' => 'image/png',
'field_image:medium:mimetype' => 'image/png',
'field_image:large:mimetype' => 'image/png',
'field_image:thumbnail:filesize' => $image_1_thumbnail->getFileSize(),
'field_image:medium:filesize' => $image_1_medium->getFileSize(),
'field_image:large:filesize' => $image_1_large->getFileSize(),
'field_image:thumbnail:height' => '100',
'field_image:medium:height' => '220',
'field_image:large:height' => '480',
'field_image:thumbnail:width' => '100',
'field_image:medium:width' => '220',
'field_image:large:width' => '480',
'field_image:thumbnail:uri' => 'public://styles/thumbnail/public/example1.png',
'field_image:medium:uri' => 'public://styles/medium/public/example1.png',
'field_image:large:uri' => 'public://styles/large/public/example1.png',
'field_image:thumbnail:url' => $style_thumbnail->buildUrl('public://example1.png'),
'field_image:medium:url' => $style_medium->buildUrl('public://example1.png'),
'field_image:large:url' => $style_large->buildUrl('public://example1.png'),
'field_image:thumbnail' => $style_thumbnail->buildUrl('public://example1.png'),
'field_image:medium' => $style_medium->buildUrl('public://example1.png'),
'field_image:large' => $style_large->buildUrl('public://example1.png'),
// field_image_2
'field_image_2:thumbnail:mimetype' => 'image/gif',
'field_image_2:medium:mimetype' => 'image/gif',
'field_image_2:large:mimetype' => 'image/gif',
'field_image_2:thumbnail:filesize' => $image_2_thumbnail->getFileSize(),
'field_image_2:medium:filesize' => $image_2_medium->getFileSize(),
'field_image_2:large:filesize' => $image_2_large->getFileSize(),
'field_image_2:thumbnail:height' => '100',
'field_image_2:medium:height' => '220',
'field_image_2:large:height' => '480',
'field_image_2:thumbnail:width' => '100',
'field_image_2:medium:width' => '220',
'field_image_2:large:width' => '480',
'field_image_2:thumbnail:uri' => 'public://styles/thumbnail/public/example2.gif',
'field_image_2:medium:uri' => 'public://styles/medium/public/example2.gif',
'field_image_2:large:uri' => 'public://styles/large/public/example2.gif',
'field_image_2:thumbnail:url' => $style_thumbnail->buildUrl('public://example2.gif'),
'field_image_2:medium:url' => $style_medium->buildUrl('public://example2.gif'),
'field_image_2:large:url' => $style_large->buildUrl('public://example2.gif'),
'field_image_2:thumbnail' => $style_thumbnail->buildUrl('public://example2.gif'),
'field_image_2:medium' => $style_medium->buildUrl('public://example2.gif'),
'field_image_2:large' => $style_large->buildUrl('public://example2.gif'),
// multivalued_field_image:0, test for thumbnail image style only.
'multivalued_field_image:0:thumbnail:mimetype' => 'image/png',
'multivalued_field_image:0:thumbnail:filesize' => $image_1_thumbnail->getFileSize(),
'multivalued_field_image:0:thumbnail:height' => '100',
'multivalued_field_image:0:thumbnail:width' => '100',
'multivalued_field_image:0:thumbnail:uri' => 'public://styles/thumbnail/public/example1.png',
'multivalued_field_image:0:thumbnail:url' => $style_thumbnail->buildUrl('public://example1.png'),
'multivalued_field_image:0:thumbnail' => $style_thumbnail->buildUrl('public://example1.png'),
// multivalued_field_image:1, test for medium image style only.
'multivalued_field_image:1:medium:mimetype' => 'image/gif',
'multivalued_field_image:1:medium:filesize' => $image_2_medium->getFileSize(),
'multivalued_field_image:1:medium:height' => '220',
'multivalued_field_image:1:medium:width' => '220',
'multivalued_field_image:1:medium:uri' => 'public://styles/medium/public/example2.gif',
'multivalued_field_image:1:medium:url' => $style_medium->buildUrl('public://example2.gif'),
'multivalued_field_image:1:medium' => $style_medium->buildUrl('public://example2.gif'),
];
$this->assertTokens('node', ['node' => $node], $tokens);
/** @var \Drupal\token\Token $token_service */
$token_service = \Drupal::service('token');
// Test one of the image style's token info for cardinality 1 image field.
$token_info = $token_service->getTokenInfo('node-field_image', 'thumbnail');
$this->assertEqual('Thumbnail (100×100)', $token_info['name']);
$this->assertEqual('Represents the image in the given image style.', $token_info['description']);
// Test one of the image style's token info for a multivalued image field.
$token_info = $token_service->getTokenInfo('node-multivalued_field_image', 'medium');
$this->assertEqual('Medium (220×220)', $token_info['name']);
$this->assertEqual('Represents the image in the given image style.', $token_info['description']);
// Test few of the image styles' properties token info.
$token_info = $token_service->getTokenInfo('image_with_image_style', 'mimetype');
$this->assertEqual('MIME type', $token_info['name']);
$this->assertEqual('The MIME type (image/png, image/bmp, etc.) of the image.', $token_info['description']);
$token_info = $token_service->getTokenInfo('image_with_image_style', 'filesize');
$this->assertEqual('File size', $token_info['name']);
$this->assertEqual('The file size of the image.', $token_info['description']);
}
}

View file

@ -0,0 +1,459 @@
<?php
namespace Drupal\token\Tests;
use Drupal\node\Entity\Node;
use Drupal\Core\Url;
use Drupal\node\Entity\NodeType;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Core\Language\LanguageInterface;
use Drupal\system\Entity\Menu;
use Drupal\menu_link_content\Entity\MenuLinkContent;
/**
* Tests menu tokens.
*
* @group token
*/
class TokenMenuTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'menu_ui',
'node',
'block',
'language',
'block_content',
'content_translation',
];
function testMenuTokens() {
// Make sure we have a body field on the node type.
$this->drupalCreateContentType(['type' => 'page']);
// Add a menu.
$menu = entity_create('menu', array(
'id' => 'main-menu',
'label' => 'Main menu',
'description' => 'The <em>Main</em> menu is used on many sites to show the major sections of the site, often in a top navigation bar.',
));
$menu->save();
// Place the menu block.
$this->drupalPlaceBlock('system_menu_block:main-menu');
// Add a root link.
/** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $root_link */
$root_link = entity_create('menu_link_content', array(
'link' => ['uri' => 'internal:/admin'],
'title' => 'Administration',
'menu_name' => 'main-menu',
));
$root_link->save();
// Add another link with the root link as the parent.
/** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $parent_link */
$parent_link = entity_create('menu_link_content', array(
'link' => ['uri' => 'internal:/admin/config'],
'title' => 'Configuration',
'menu_name' => 'main-menu',
'parent' => $root_link->getPluginId(),
));
$parent_link->save();
// Test menu link tokens.
$tokens = array(
'id' => $parent_link->getPluginId(),
'title' => 'Configuration',
'menu' => 'Main menu',
'menu:name' => 'Main menu',
'menu:machine-name' => $menu->id(),
'menu:description' => 'The <em>Main</em> menu is used on many sites to show the major sections of the site, often in a top navigation bar.',
'menu:menu-link-count' => '2',
'menu:edit-url' => Url::fromRoute('entity.menu.edit_form', ['menu' => 'main-menu'], array('absolute' => TRUE))->toString(),
'url' => Url::fromRoute('system.admin_config', [], array('absolute' => TRUE))->toString(),
'url:absolute' => Url::fromRoute('system.admin_config', [], array('absolute' => TRUE))->toString(),
'url:relative' => Url::fromRoute('system.admin_config', [], array('absolute' => FALSE))->toString(),
'url:path' => '/admin/config',
'url:alias' => '/admin/config',
'edit-url' => Url::fromRoute('entity.menu_link_content.canonical', ['menu_link_content' => $parent_link->id()], array('absolute' => TRUE))->toString(),
'parent' => 'Administration',
'parent:id' => $root_link->getPluginId(),
'parent:title' => 'Administration',
'parent:menu' => 'Main menu',
'parent:parent' => NULL,
'parents' => 'Administration',
'parents:count' => 1,
'parents:keys' => $root_link->getPluginId(),
'root' => 'Administration',
'root:id' => $root_link->getPluginId(),
'root:parent' => NULL,
'root:root' => NULL,
);
$this->assertTokens('menu-link', array('menu-link' => $parent_link), $tokens);
// Add a node.
$node = $this->drupalCreateNode();
// Allow main menu for this node type.
//$this->config('menu.entity.node.' . $node->getType())->set('available_menus', array('main-menu'))->save();
// Add a node menu link.
/** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $node_link */
$node_link = entity_create('menu_link_content', array(
'link' => ['uri' =>'entity:node/' . $node->id()],
'title' => 'Node link',
'parent' => $parent_link->getPluginId(),
'menu_name' => 'main-menu',
));
$node_link->save();
// Test [node:menu] tokens.
$tokens = array(
'menu-link' => 'Node link',
'menu-link:id' => $node_link->getPluginId(),
'menu-link:title' => 'Node link',
'menu-link:menu' => 'Main menu',
'menu-link:url' => $node->url('canonical', ['absolute' => TRUE]),
'menu-link:url:path' => '/node/' . $node->id(),
'menu-link:edit-url' => $node_link->url('edit-form', ['absolute' => TRUE]),
'menu-link:parent' => 'Configuration',
'menu-link:parent:id' => $parent_link->getPluginId(),
'menu-link:parents' => 'Administration, Configuration',
'menu-link:parents:count' => 2,
'menu-link:parents:keys' => $root_link->getPluginId() . ', ' . $parent_link->getPluginId(),
'menu-link:root' => 'Administration',
'menu-link:root:id' => $root_link->getPluginId(),
);
$this->assertTokens('node', array('node' => $node), $tokens);
// Reload the node which will not have $node->menu defined and re-test.
$loaded_node = Node::load($node->id());
$this->assertTokens('node', array('node' => $loaded_node), $tokens);
// Regression test for http://drupal.org/node/1317926 to ensure the
// original node object is not changed when calling menu_node_prepare().
$this->assertTrue(!isset($loaded_node->menu), t('The $node->menu property was not modified during token replacement.'), 'Regression');
// Now add a node with a menu-link from the UI and ensure it works.
$this->drupalLogin($this->drupalCreateUser([
'create page content',
'edit any page content',
'administer menu',
'administer nodes',
'administer content types',
'access administration pages',
]));
// Setup node type menu options.
$edit = array(
'menu_options[main-menu]' => 1,
'menu_options[main]' => 1,
'menu_parent' => 'main-menu:',
);
$this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
// Use a menu-link token in the body.
$this->drupalGet('node/add/page');
$this->drupalPostForm(NULL, [
// This should get replaced on save.
// @see token_module_test_node_presave()
'title[0][value]' => 'Node menu title test',
'body[0][value]' => 'This is a [node:menu-link:title] token to the menu link title',
'menu[enabled]' => 1,
'menu[title]' => 'Test preview',
], t('Save and publish'));
$node = $this->drupalGetNodeByTitle('Node menu title test');
$this->assertEqual('This is a Test preview token to the menu link title', $node->body->value);
// Disable the menu link, save the node and verify that the menu link is
// no longer displayed.
$link = menu_ui_get_menu_link_defaults($node);
$this->drupalPostForm('admin/structure/menu/manage/main-menu', ['links[menu_plugin_id:' . $link['id'] . '][enabled]' => FALSE], t('Save'));
$this->assertText('Menu Main menu has been updated.');
$this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save and keep published'));
$this->assertNoLink('Test preview');
// Now test a parent link and token.
$this->drupalGet('node/add/page');
// Make sure that the previous node save didn't result in two menu-links
// being created by the computed menu-link ER field.
// @see token_entity_base_field_info()
// @see token_node_menu_link_submit()
$selects = $this->cssSelect('select[name="menu[menu_parent]"]');
$select = reset($selects);
$options = $this->getAllOptions($select);
// Filter to items with title containing 'Test preview'.
$options = array_filter($options, function(\SimpleXMLElement $item) {
return strpos((string) $item[0], 'Test preview') !== FALSE;
});
$this->assertEqual(1, count($options));
$this->drupalPostForm(NULL, [
'title[0][value]' => 'Node menu title parent path test',
'body[0][value]' => 'This is a [node:menu-link:parent:url:path] token to the menu link parent',
'menu[enabled]' => 1,
'menu[title]' => 'Child link',
'menu[menu_parent]' => 'main-menu:' . $parent_link->getPluginId(),
], t('Save and publish'));
$node = $this->drupalGetNodeByTitle('Node menu title parent path test');
$this->assertEqual('This is a /admin/config token to the menu link parent', $node->body->value);
// Now edit the node and update the parent and title.
$this->drupalPostForm('node/' . $node->id() . '/edit', [
'menu[menu_parent]' => 'main-menu:' . $node_link->getPluginId(),
'title[0][value]' => 'Node menu title edit parent path test',
'body[0][value]' => 'This is a [node:menu-link:parent:url:path] token to the menu link parent',
], t('Save and keep published'));
$node = $this->drupalGetNodeByTitle('Node menu title edit parent path test', TRUE);
$this->assertEqual(sprintf('This is a /node/%d token to the menu link parent', $loaded_node->id()), $node->body->value);
// Make sure that the previous node edit didn't result in two menu-links
// being created by the computed menu-link ER field.
// @see token_entity_base_field_info()
// @see token_node_menu_link_submit()
$this->drupalGet('node/add/page');
$selects = $this->cssSelect('select[name="menu[menu_parent]"]');
$select = reset($selects);
$options = $this->getAllOptions($select);
// Filter to items with title containing 'Test preview'.
$options = array_filter($options, function(\SimpleXMLElement $item) {
return strpos((string) $item[0], 'Child link') !== FALSE;
});
$this->assertEqual(1, count($options));
// Now add a new node with no menu.
$this->drupalGet('node/add/page');
$this->drupalPostForm(NULL, [
'title[0][value]' => 'Node menu adding menu later test',
'body[0][value]' => 'Going to add a menu link on edit',
'menu[enabled]' => 0,
], t('Save and publish'));
$node = $this->drupalGetNodeByTitle('Node menu adding menu later test');
// Now edit it and add a menu item.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->drupalPostForm(NULL, [
'title[0][value]' => 'Node menu adding menu later test',
'body[0][value]' => 'This is a [node:menu-link:parent:url:path] token to the menu link parent',
'menu[enabled]' => 1,
'menu[title]' => 'Child link',
'menu[menu_parent]' => 'main-menu:' . $parent_link->getPluginId(),
], t('Save and keep published'));
$node = $this->drupalGetNodeByTitle('Node menu adding menu later test', TRUE);
$this->assertEqual('This is a /admin/config token to the menu link parent', $node->body->value);
// And make sure the menu link exists with the right URI.
$link = menu_ui_get_menu_link_defaults($node);
$this->assertTrue(!empty($link['entity_id']));
$query = \Drupal::entityQuery('menu_link_content')
->condition('link.uri', 'entity:node/' . $node->id())
->sort('id', 'ASC')
->range(0, 1);
$result = $query->execute();
$this->assertTrue($result);
// Create a node with a menu link and create 2 menu links linking to this
// node after. Verify that the menu link provided by the node has priority.
$node_title = $this->randomMachineName();
$edit = [
'title[0][value]' => $node_title,
'menu[enabled]' => 1,
'menu[title]' => 'menu link provided by node',
];
$this->drupalPostForm('node/add/page', $edit, t('Save and publish'));
$this->assertText('page ' . $node_title . ' has been created');
$node = $this->drupalGetNodeByTitle($node_title);
$menu_ui_link1 = entity_create('menu_link_content', [
'link' => ['uri' => 'entity:node/' . $node->id()],
'title' => 'menu link 1 provided by menu ui',
'menu_name' => 'main-menu',
]);
$menu_ui_link1->save();
$menu_ui_link2 = entity_create('menu_link_content', [
'link' => ['uri' => 'entity:node/' . $node->id()],
'title' => 'menu link 2 provided by menu ui',
'menu_name' => 'main-menu',
]);
$menu_ui_link2->save();
$tokens = [
'menu-link' => 'menu link provided by node',
'menu-link:title' => 'menu link provided by node',
];
$this->assertTokens('node', ['node' => $node], $tokens);
}
/**
* Tests that the module doesn't affect integrity of the menu, when
* translating them and that menu links tokens are correct.
*/
function testMultilingualMenu() {
// Place the menu block.
$this->drupalPlaceBlock('system_menu_block:main');
// Add a second language.
$language = ConfigurableLanguage::create([
'id' => 'de',
'label' => 'German',
]);
$language->save();
// Create the article content type.
$node_type = NodeType::create([
'type' => 'article',
]);
$node_type->save();
$permissions = array(
'access administration pages',
'administer content translation',
'administer content types',
'administer languages',
'create content translations',
'create article content',
'edit any article content',
'translate any entity',
'administer menu',
);
$this->drupalLogin($this->drupalCreateUser($permissions));
// Enable translation for articles and menu links.
$this->drupalGet('admin/config/regional/content-language');
$edit = array(
'entity_types[node]' => TRUE,
'entity_types[menu_link_content]' => TRUE,
'settings[node][article][translatable]' => TRUE,
'settings[node][article][fields][title]' => TRUE,
'settings[menu_link_content][menu_link_content][translatable]' => TRUE,
);
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$this->assertText('Settings successfully updated.');
// Create an english node with an english menu.
$this->drupalGet('/node/add/article');
$edit = [
'title[0][value]' => 'English test node with menu',
'menu[enabled]' => TRUE,
'menu[title]' => 'English menu title',
];
$this->drupalPostForm('/node/add/article', $edit, t('Save'));
$this->assertText('English test node with menu has been created.');
// Add a german translation.
$this->drupalGet('node/1/translations');
$this->clickLink('Add');
$edit = [
'title[0][value]' => 'German test node with menu',
'menu[enabled]' => TRUE,
'menu[title]' => 'German menu title',
];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$this->assertText('German test node with menu has been updated.');
// Verify that the menu links are correct.
$this->drupalGet('node/1');
$this->assertLink('English menu title');
$this->drupalGet('de/node/1');
$this->assertLink('German menu title');
// Verify that tokens are correct.
$node = Node::load(1);
$this->assertTokens('node', ['node' => $node], ['menu-link' => 'English menu title']);
$this->assertTokens('node', ['node' => $node], [
'menu-link' => 'German menu title',
'menu-link:title' => 'German menu title',
], ['langcode' => 'de']);
// Get the menu link and create a child menu link to assert parent and root
// tokens.
$url = $node->toUrl();
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$links = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters());
$link = reset($links);
$base_options = [
'provider' => 'menu_test',
'menu_name' => 'menu_test',
];
$child_1 = $base_options + [
'title' => 'child_1 title EN',
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child_1'],
'parent' => $link->getPluginId(),
'langcode' => 'en',
];
$child_1 = MenuLinkContent::create($child_1);
$child_1->save();
// Add the german translation.
$child_1->addTranslation('de', ['title' => 'child_1 title DE'] + $child_1->toArray());
$child_1->save();
$this->assertTokens('menu-link', ['menu-link' => $child_1], [
'title' => 'child_1 title EN',
'parents' => 'English menu title',
'root' => 'English menu title',
]);
$this->assertTokens('menu-link', ['menu-link' => $child_1], [
'title' => 'child_1 title DE',
'parents' => 'German menu title',
'root' => 'German menu title',
], ['langcode' => 'de']);
}
/**
* Tests menu link parents token.
*/
public function testMenuLinkParentsToken() {
// Create a menu with a simple link hierarchy :
// - parent
// - child-1
// - child-1-1
Menu::create(array(
'id' => 'menu_test',
'label' => 'Test menu',
))->save();
$base_options = [
'provider' => 'menu_test',
'menu_name' => 'menu_test',
];
$parent = $base_options + [
'title' => 'parent title',
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent'],
];
$parent = MenuLinkContent::create($parent);
$parent->save();
$child_1 = $base_options + [
'title' => 'child_1 title',
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child_1'],
'parent' => $parent->getPluginId(),
];
$child_1 = MenuLinkContent::create($child_1);
$child_1->save();
$child_1_1 = $base_options + [
'title' => 'child_1_1 title',
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child_1/child_1_1'],
'parent' => $child_1->getPluginId(),
];
$child_1_1 = MenuLinkContent::create($child_1_1);
$child_1_1->save();
$this->assertTokens('menu-link', ['menu-link' => $child_1_1], ['parents' => 'parent title, child_1 title']);
// Change the parent of child_1_1 to 'parent' at the entity level.
$child_1_1->parent->value = $parent->getPluginId();
$child_1_1->save();
$this->assertTokens('menu-link', ['menu-link' => $child_1_1], ['parents' => 'parent title']);
// Change the parent of child_1_1 to 'main', at the entity level.
$child_1_1->parent->value = '';
$child_1_1->save();
// The token shouldn't have been generated; the menu link has no parent.
$this->assertNoTokens('menu-link', ['menu-link' => $child_1_1], ['parents']);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\token\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Helper test class with some added functions for testing.
*/
abstract class TokenTestBase extends WebTestBase {
use TokenTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('path', 'token', 'token_module_test');
}

View file

@ -0,0 +1,111 @@
<?php
namespace Drupal\token\Tests;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Render\BubbleableMetadata;
/**
* Helper test trait with some added functions for testing.
*/
trait TokenTestTrait {
function assertToken($type, array $data, $token, $expected, array $options = array()) {
return $this->assertTokens($type, $data, array($token => $expected), $options);
}
function assertTokens($type, array $data, array $tokens, array $options = array()) {
$input = $this->mapTokenNames($type, array_keys($tokens));
$bubbleable_metadata = new BubbleableMetadata();
$replacements = \Drupal::token()->generate($type, $input, $data, $options, $bubbleable_metadata);
foreach ($tokens as $name => $expected) {
$token = $input[$name];
if (!isset($expected)) {
$this->assertTrue(!isset($replacements[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
}
elseif (!isset($replacements[$token])) {
$this->fail(t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
}
elseif (!empty($options['regex'])) {
$this->assertTrue(preg_match('/^' . $expected . '$/', $replacements[$token]), t("Token value for @token was '@actual', matching regular expression pattern '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected)));
}
else {
$this->assertEqual($replacements[$token], $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected)));
}
}
return $replacements;
}
function mapTokenNames($type, array $tokens = array()) {
$return = array();
foreach ($tokens as $token) {
$return[$token] = "[$type:$token]";
}
return $return;
}
function assertNoTokens($type, array $data, array $tokens, array $options = array()) {
$input = $this->mapTokenNames($type, $tokens);
$bubbleable_metadata = new BubbleableMetadata();
$replacements = \Drupal::token()->generate($type, $input, $data, $options, $bubbleable_metadata);
foreach ($tokens as $name) {
$token = $input[$name];
$this->assertTrue(!isset($replacements[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
}
}
function saveAlias($source, $alias, $language = Language::LANGCODE_NOT_SPECIFIED) {
$alias = array(
'source' => $source,
'alias' => $alias,
'language' => $language,
);
\Drupal::service('path.alias_storage')->save($alias['source'], $alias['alias']);
return $alias;
}
function saveEntityAlias($entity_type, EntityInterface $entity, $alias, $language = Language::LANGCODE_NOT_SPECIFIED) {
$uri = $entity->toUrl()->toArray();
return $this->saveAlias($uri['path'], $alias, $language);
}
/**
* Make a page request and test for token generation.
*/
function assertPageTokens($url, array $tokens, array $data = array(), array $options = array()) {
if (empty($tokens)) {
return TRUE;
}
$token_page_tokens = array(
'tokens' => $tokens,
'data' => $data,
'options' => $options,
);
\Drupal::state()->set('token_page_tokens', $token_page_tokens);
$options += array('url_options' => array());
$this->drupalGet($url, $options['url_options']);
$this->refreshVariables();
$result = \Drupal::state()->get('token_page_tokens', array());
if (!isset($result['values']) || !is_array($result['values'])) {
return $this->fail('Failed to generate tokens.');
}
foreach ($tokens as $token => $expected) {
if (!isset($expected)) {
$this->assertTrue(!isset($result['values'][$token]) || $result['values'][$token] === $token, t("Token value for @token was not generated.", array('@token' => $token)));
}
elseif (!isset($result['values'][$token])) {
$this->fail(t('Failed to generate token @token.', array('@token' => $token)));
}
else {
$this->assertIdentical($result['values'][$token], (string) $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@token' => $token, '@actual' => $result['values'][$token], '@expected' => $expected)));
}
}
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\token\Tests;
use Drupal\Core\Url;
/**
* Tests url tokens.
*
* @group token
*/
class TokenURLTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node');
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->saveAlias('/node/1', '/first-node');
}
function testURLTokens() {
$url = new Url('entity.node.canonical', array('node' => 1));
$tokens = array(
'absolute' => $url->setAbsolute()->toString(),
'relative' => $url->setAbsolute(FALSE)->toString(),
'path' => '/first-node',
'brief' => preg_replace(array('!^https?://!', '!/$!'), '', $url->setAbsolute()->toString()),
'args:value:0' => 'first-node',
'args:value:1' => NULL,
'args:value:N' => NULL,
'unaliased' => $url->setAbsolute()->setOption('alias', TRUE)->toString(),
'unaliased:relative' => $url->setAbsolute(FALSE)->setOption('alias', TRUE)->toString(),
'unaliased:path' => '/node/1',
'unaliased:brief' => preg_replace(array('!^https?://!', '!/$!'), '', $url->setAbsolute()->setOption('alias', TRUE)->toString()),
'unaliased:args:value:0' => 'node',
'unaliased:args:value:1' => '1',
'unaliased:args:value:2' => NULL,
// Deprecated tokens.
'alias' => '/first-node',
);
$this->assertTokens('url', array('url' => new Url('entity.node.canonical', array('node' => 1))), $tokens);
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace Drupal\token\Tests;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests user tokens.
*
* @group token
*/
class TokenUserTest extends TokenTestBase {
/**
* The user account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account = NULL;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('token_user_picture');
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->account = $this->drupalCreateUser(['administer users', 'administer account settings']);
$this->drupalLogin($this->account);
}
public function testUserTokens() {
// Enable user pictures.
\Drupal::state()->set('user_pictures', 1);
\Drupal::state()->set('user_picture_file_size', '');
// Set up the pictures directory.
$picture_path = file_default_scheme() . '://' . \Drupal::state()->get('user_picture_path', 'pictures');
if (!file_prepare_directory($picture_path, FILE_CREATE_DIRECTORY)) {
$this->fail('Could not create directory ' . $picture_path . '.');
}
// Add a user picture to the account.
$image = current($this->drupalGetTestFiles('image'));
$edit = array('files[user_picture_0]' => drupal_realpath($image->uri));
$this->drupalPostForm('user/' . $this->account->id() . '/edit', $edit, t('Save'));
$storage = \Drupal::entityTypeManager()->getStorage('user');
// Load actual user data from database.
$storage->resetCache();
$this->account = $storage->load($this->account->id());
$this->assertTrue(!empty($this->account->user_picture->target_id), 'User picture uploaded.');
$picture = [
'#theme' => 'user_picture',
'#account' => $this->account,
];
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$user_tokens = array(
'picture' => $renderer->renderPlain($picture),
'picture:fid' => $this->account->user_picture->target_id,
'picture:size-raw' => 125,
'ip-address' => NULL,
'roles' => implode(', ', $this->account->getRoles()),
);
$this->assertTokens('user', array('user' => $this->account), $user_tokens);
// Remove the simpletest-created user role.
$roles = $this->account->getRoles();
$this->account->removeRole(end($roles));
$this->account->save();
// Remove the user picture field and reload the user.
FieldStorageConfig::loadByName('user', 'user_picture')->delete();
$storage->resetCache();
$this->account = $storage->load($this->account->id());
$user_tokens = array(
'picture' => NULL,
'picture:fid' => NULL,
'ip-address' => NULL,
'roles' => 'authenticated',
'roles:keys' => (string) DRUPAL_AUTHENTICATED_RID,
);
$this->assertTokens('user', array('user' => $this->account), $user_tokens);
// The ip address token should work for the current user token type.
$tokens = array(
'ip-address' => \Drupal::request()->getClientIp(),
);
$this->assertTokens('current-user', array(), $tokens);
$anonymous = new AnonymousUserSession();
$tokens = array(
'roles' => 'anonymous',
'roles:keys' => (string) DRUPAL_ANONYMOUS_RID,
);
$this->assertTokens('user', array('user' => $anonymous), $tokens);
}
public function testUserAccountSettings() {
$this->drupalGet('admin/config/people/accounts');
$this->assertText('The list of available tokens that can be used in e-mails is provided below.');
$this->assertLink('Browse available tokens.');
$this->assertLinkByHref('token/tree');
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\token\Tests\Tree;
use Drupal\token\Tests\TokenTestBase;
/**
* Test token autocomplete.
*
* @group token
*/
class AutocompleteTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node'];
/**
* Tests autocomplete for node tokens.
*/
public function testNodeAutocomplete() {
$url_prefix = "token/autocomplete/node/";
$url = $url_prefix . 'Title of [nod';
$response = $this->drupalGetJSON($url);
$this->assertTrue(isset($response['[node:nid]']));
$this->assertTrue(isset($response['[node:author]']));
$this->assertTrue(isset($response['[node:url]']));
$this->assertTrue(isset($response['[node:url:']));
$url = $url_prefix . 'Title of [node:url:';
$response = $this->drupalGetJSON($url);
$this->assertTrue(isset($response['[node:url:path]']));
$this->assertTrue(isset($response['[node:url:absolute]']));
}
/**
* Tests autocomplete for user tokens.
*/
public function testUserAutocomplete() {
$url_prefix = "token/autocomplete/user/";
$url = $url_prefix . 'Name of the [us';
$response = $this->drupalGetJSON($url);
$this->assertTrue(isset($response['[user:uid]']));
$this->assertTrue(isset($response['[user:original]']));
$this->assertTrue(isset($response['[user:url]']));
$this->assertTrue(isset($response['[user:url:']));
$url = $url_prefix . 'Title of [user:original:';
$response = $this->drupalGetJSON($url);
$this->assertTrue(isset($response['[user:original:uid]']));
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Drupal\token\Tests\Tree;
use Drupal\token\Tests\TokenTestBase;
/**
* Tests token tree on help page.
*
* @group token
*/
class HelpPageTest extends TokenTestBase {
use TokenTreeTestTrait;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['help'];
public function setUp() {
parent::setUp();
$this->account = $this->drupalCreateUser(['access administration pages']);
$this->drupalLogin($this->account);
}
/**
* Tests the token browser on the token help page.
*/
public function testHelpPageTree() {
$this->drupalGet('admin/help/token');
$this->assertText('The list of the currently available tokens on this site are shown below.');
$this->assertTokenGroup('Current date');
$this->assertTokenGroup('Site information');
$this->assertTokenInTree('[current-date:html_date]', 'current-date');
$this->assertTokenInTree('[current-date:html_week]', 'current-date');
$this->assertTokenInTree('[date:html_date]', 'date');
$this->assertTokenInTree('[date:html_week]', 'date');
$this->assertTokenInTree('[current-user:account-name]', 'current-user');
$this->assertTokenInTree('[user:account-name]', 'user');
$this->assertTokenInTree('[current-page:url:unaliased]', 'current-page--url');
$this->assertTokenInTree('[current-page:url:unaliased:args]', 'current-page--url--unaliased');
$this->assertTokenInTree('[user:original:account-name]', 'user--original');
// Assert some of the restricted tokens to ensure they are shown.
$this->assertTokenInTree('[user:one-time-login-url]', 'user');
$this->assertTokenInTree('[user:original:cancel-url]', 'user--original');
// The Array token is marked as nested, so it should not show up as a top
// level token, only nested under another token. For instance, user:roles
// is of type Array and tokens of type Array have 'nested' setting true.
$this->assertTokenNotGroup('Array');
$this->assertTokenNotGroup('user:roles');
$this->assertTokenInTree('[user:roles]', 'user');
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace Drupal\token\Tests\Tree;
/**
* Helper trait to assert tokens in token tree browser.
*/
trait TokenTreeTestTrait {
/**
* Get an array of token groups from the last retrieved page.
*
* @return array
* Array of token group names.
*/
protected function getTokenGroups() {
$groups = $this->xpath('//tr[contains(@class, "token-group")]/td[1]');
return array_map(function ($item) {
return (string) $item;
}, $groups);
}
/**
* Check to see if the specified token group is present in the token browser.
*
* @param string $token_group
* The name of the token group.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output.
*/
protected function assertTokenGroup($token_group, $message = '', $group = 'Other') {
$groups = $this->getTokenGroups();
if (!$message) {
$message = "Token group $token_group found.";
}
$this->assertTrue(in_array($token_group, $groups), $message, $group);
}
/**
* Check to see if the specified token group is not present in the token
* browser.
*
* @param string $token_group
* The name of the token group.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is not in, which is displayed in a
* column in test output.
*/
protected function assertTokenNotGroup($token_group, $message = '', $group = 'Other') {
$groups = $this->getTokenGroups();
if (!$message) {
$message = "Token group $token_group not found.";
}
$this->assertFalse(in_array($token_group, $groups), $message, $group);
}
/**
* Check to see if the specified token is present in the token browser.
*
* @param $token
* The token name with the surrounding square brackets [].
* @param string $parent
* (optional) The parent CSS identifier of this token.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output.
*/
protected function assertTokenInTree($token, $parent = '', $message = '', $group = 'Other') {
$xpath = $this->getXpathForTokenInTree($token, $parent);
if (!$message) {
$message = "Token $token found.";
}
$this->assertIdentical(1, count($this->xpath($xpath)), $message, $group);
}
/**
* Check to see if the specified token is present in the token browser.
*
* @param $token
* The token name with the surrounding square brackets [].
* @param string $parent
* (optional) The parent CSS identifier of this token.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output.
*/
protected function assertTokenNotInTree($token, $parent = '', $message = '', $group = 'Other') {
$xpath = $this->getXpathForTokenInTree($token, $parent);
if (!$message) {
$message = "Token $token not found.";
}
$this->assertIdentical(0, count($this->xpath($xpath)), $message, $group);
}
/**
* Get xpath to check for token in tree.
*
* @param $token
* The token name with the surrounding square brackets [].
* @param string $parent
* (optional) The parent CSS identifier of this token.
*
* @return string
* The xpath to check for the token and parent.
*/
protected function getXpathForTokenInTree($token, $parent = '') {
$xpath = "//tr";
if ($parent) {
$xpath .= '[@data-tt-parent-id="token-' . $parent . '"]';
}
$xpath .= '/td[contains(@class, "token-key") and text() = "' . $token . '"]';
return $xpath;
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace Drupal\token\Tests\Tree;
use Drupal\Component\Serialization\Json;
use Drupal\token\Tests\TokenTestBase;
/**
* Tests token tree page.
*
* @group token
*/
class TreeTest extends TokenTestBase {
use TokenTreeTestTrait;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node'];
public function setUp() {
parent::setUp();
$this->account = $this->drupalCreateUser(['administer account settings']);
$this->drupalLogin($this->account);
}
/**
* Test various tokens that are possible on the site.
*/
public function testAllTokens() {
$this->drupalGet($this->getTokenTreeUrl(['token_types' => 'all']));
$this->assertTokenGroup('Current date');
$this->assertTokenGroup('Site information');
$this->assertTokenInTree('[current-date:html_date]', 'current-date');
$this->assertTokenInTree('[current-date:html_week]', 'current-date');
$this->assertTokenInTree('[date:html_date]', 'date');
$this->assertTokenInTree('[date:html_week]', 'date');
$this->assertTokenInTree('[current-user:account-name]', 'current-user');
$this->assertTokenInTree('[user:account-name]', 'user');
$this->assertTokenInTree('[current-page:url:unaliased]', 'current-page--url');
$this->assertTokenInTree('[current-page:url:unaliased:args]', 'current-page--url--unaliased');
$this->assertTokenInTree('[user:original:account-name]', 'user--original');
}
/**
* Test various tokens that are possible on the site.
*/
public function testGlobalTokens() {
$this->drupalGet($this->getTokenTreeUrl());
$this->assertTokenGroup('Current date');
$this->assertTokenGroup('Site information');
// Assert that non-global tokens are not listed.
$this->assertTokenNotInTree('[user:account-name]', 'user');
$this->assertTokenNotInTree('[user:original:account-name]', 'user--original');
// Assert some of the global tokens, just to be sure.
$this->assertTokenInTree('[current-date:html_date]', 'current-date');
$this->assertTokenInTree('[current-date:html_week]', 'current-date');
$this->assertTokenInTree('[current-user:account-name]', 'current-user');
$this->assertTokenInTree('[current-page:url:unaliased]', 'current-page--url');
$this->assertTokenInTree('[current-page:url:unaliased:args]', 'current-page--url--unaliased');
}
/**
* Tests if the token browser displays the user tokens.
*/
public function testUserTokens() {
$this->drupalGet($this->getTokenTreeUrl(['token_types' => ['user']]));
$this->assertTokenGroup('Users');
$this->assertTokenInTree('[user:account-name]', 'user');
$this->assertTokenInTree('[user:original:account-name]', 'user--original');
// Assert some of the restricted tokens to ensure they are not shown.
$this->assertTokenNotInTree('[user:one-time-login-url]', 'user');
$this->assertTokenNotInTree('[user:original:cancel-url]', 'user--original');
// Request with show_restricted set to TRUE to show restricted tokens and
// check for them.
$this->drupalGet($this->getTokenTreeUrl(['token_types' => ['user'], 'show_restricted' => TRUE]));
$this->assertEqual('MISS', $this->drupalGetHeader('x-drupal-dynamic-cache'), 'Cache was not hit');
$this->assertTokenInTree('[user:one-time-login-url]', 'user');
$this->assertTokenInTree('[user:original:cancel-url]', 'user--original');
}
/**
* Tests if the token browser displays the node tokens.
*/
public function testNodeTokens() {
$this->drupalGet($this->getTokenTreeUrl(['token_types' => ['node']]));
$this->assertTokenGroup('Nodes');
$this->assertTokenInTree('[node:body]', 'node');
$this->assertTokenInTree('[node:author:original:account-name]', 'node--author--original');
}
/**
* Get the URL for the token tree based on the specified options.
*
* The token tree route's URL requires CSRF and cannot be generated in the
* test code. The CSRF token generated using the test runner's session is
* different from the session inside the test environment. This is why the
* link has to be generated inside the environment.
*
* This function calls a page in token_module_test module which generates the
* link and the token. This then replaces the options query parameter with the
* specified options.
*
* The page also uses a title callback to set title to a render array, which
* allows us to test if [current-page:title] works properly.
*
* @param array $options
* The options for the token tree browser.
*
* @return string
* The complete URL of the token tree browser with the CSRF token.
*/
protected function getTokenTreeUrl($options = []) {
$this->drupalGet('token_module_test/browse');
$this->assertTitle('Available Tokens | Drupal');
$links = $this->xpath('//a[contains(@href, :href)]/@href', array(':href' => 'token/tree'));
$link = $this->getAbsoluteUrl((string) current($links));
if (!empty($options)) {
$options = Json::encode($options);
$link = str_replace('options=%5B%5D', 'options=' . urlencode($options), $link);
}
return $link;
}
}

View file

@ -0,0 +1,213 @@
<?php
namespace Drupal\token;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Utility\Token as TokenBase;
/**
* Service to retrieve token information.
*
* This service replaces the core's token service and provides the same
* functionality by extending it. It also provides additional functionality
* commonly required by the additional support provided by token module and
* other modules.
*/
class Token extends TokenBase implements TokenInterface {
/**
* Token definitions.
*
* @var array[]|null
* An array of token definitions, or NULL when the definitions are not set.
*
* @see self::resetInfo()
*/
protected $globalTokenTypes;
/**
* {@inheritdoc}
*/
public function getInfo() {
if (empty($this->tokenInfo)) {
$cache_id = 'token_info_sorted:' . $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
$cache = $this->cache->get($cache_id);
if ($cache) {
$this->tokenInfo = $cache->data;
}
else {
$token_info = $this->moduleHandler->invokeAll('token_info');
$this->moduleHandler->alter('token_info', $token_info);
foreach (array_keys($token_info['types']) as $type_key) {
if (isset($token_info['types'][$type_key]['type'])) {
$base_type = $token_info['types'][$type_key]['type'];
// If this token type extends another token type, then merge in
// the base token type's tokens.
if (isset($token_info['tokens'][$base_type])) {
$token_info['tokens'] += [$type_key => []];
$token_info['tokens'][$type_key] += $token_info['tokens'][$base_type];
}
}
else {
// Add a 'type' value to each token type information.
$token_info['types'][$type_key]['type'] = $type_key;
}
}
// Pre-sort tokens.
$by_name = $this->prepareMultisort($token_info['types']);
array_multisort($by_name, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $token_info['types']);
foreach (array_keys($token_info['tokens']) as $type) {
$by_name = $this->prepareMultisort($token_info['tokens'][$type]);
array_multisort($by_name, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $token_info['tokens'][$type]);
}
$this->tokenInfo = $token_info;
$this->cache->set($cache_id, $this->tokenInfo, CacheBackendInterface::CACHE_PERMANENT, array(
static::TOKEN_INFO_CACHE_TAG,
));
}
}
return $this->tokenInfo;
}
/**
* Extracts data from the token data for use in array_multisort().
*
* @param array $token_info
* List of tokens or token types, each element must have a name key.
*
* @return string[]
* List of the names keyed by the token key.
*/
protected function prepareMultisort($token_info) {
$by_name = [];
foreach ($token_info as $key => $token_info_element) {
$by_name[$key] = $token_info_element['name'];
}
return $by_name;
}
/**
* {@inheritdoc}
*/
public function getTokenInfo($token_type, $token) {
if (empty($this->tokenInfo)) {
$this->getInfo();
}
return isset($this->tokenInfo['tokens'][$token_type][$token]) ? $this->tokenInfo['tokens'][$token_type][$token] : NULL;
}
/**
* {@inheritdoc}
*/
public function getTypeInfo($token_type) {
if (empty($this->tokenInfo)) {
$this->getInfo();
}
return isset($this->tokenInfo['types'][$token_type]) ? $this->tokenInfo['types'][$token_type] : NULL;
}
/**
* {@inheritdoc}
*/
public function getGlobalTokenTypes() {
if (empty($this->globalTokenTypes)) {
$token_info = $this->getInfo();
foreach ($token_info['types'] as $type => $type_info) {
// If the token types has not specified that 'needs-data' => TRUE, then
// it is a global token type that will always be replaced in any context.
if (empty($type_info['needs-data'])) {
$this->globalTokenTypes[] = $type;
}
}
}
return $this->globalTokenTypes;
}
/**
* {@inheritdoc}
*/
function getInvalidTokens($type, $tokens) {
$token_info = $this->getInfo();
$invalid_tokens = array();
foreach ($tokens as $token => $full_token) {
if (isset($token_info['tokens'][$type][$token])) {
continue;
}
// Split token up if it has chains.
$parts = explode(':', $token, 2);
if (!isset($token_info['tokens'][$type][$parts[0]])) {
// This is an invalid token (not defined).
$invalid_tokens[] = $full_token;
}
elseif (count($parts) == 2) {
$sub_token_info = $token_info['tokens'][$type][$parts[0]];
if (!empty($sub_token_info['dynamic'])) {
// If this token has been flagged as a dynamic token, skip it.
continue;
}
elseif (empty($sub_token_info['type'])) {
// If the token has chains, but does not support it, it is invalid.
$invalid_tokens[] = $full_token;
}
else {
// Recursively check the chained tokens.
$sub_tokens = $this->findWithPrefix(array($token => $full_token), $parts[0]);
$invalid_tokens = array_merge($invalid_tokens, $this->getInvalidTokens($sub_token_info['type'], $sub_tokens));
}
}
}
return $invalid_tokens;
}
/**
* {@inheritdoc}
*/
public function getInvalidTokensByContext($value, array $valid_types = []) {
if (in_array('all', $valid_types)) {
$info = $this->getInfo();
$valid_types = array_keys($info['types']);
}
else {
// Add the token types that are always valid in global context.
$valid_types = array_merge($valid_types, $this->getGlobalTokenTypes());
}
$invalid_tokens = array();
$value_tokens = is_string($value) ? $this->scan($value) : $value;
foreach ($value_tokens as $type => $tokens) {
if (!in_array($type, $valid_types)) {
// If the token type is not a valid context, its tokens are invalid.
$invalid_tokens = array_merge($invalid_tokens, array_values($tokens));
}
else {
// Check each individual token for validity.
$invalid_tokens = array_merge($invalid_tokens, $this->getInvalidTokens($type, $tokens));
}
}
array_unique($invalid_tokens);
return $invalid_tokens;
}
/**
* {@inheritdoc}
*/
public function resetInfo() {
parent::resetInfo();
$this->globalTokenTypes = NULL;
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Drupal\token;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Service to provide mappings between entity and token types.
*
* Why do we need this? Because when the token API was moved to core we did not
* reuse the entity type as the base name for taxonomy terms and vocabulary
* tokens.
*/
class TokenEntityMapper implements TokenEntityMapperInterface {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* @var array
*/
protected $entityMappings;
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function getEntityTypeMappings() {
if (empty($this->entityMappings)) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $info) {
$this->entityMappings[$entity_type] = $info->get('token_type') ?: $entity_type;
}
// Allow modules to alter the mapping array.
$this->moduleHandler->alter('token_entity_mapping', $this->entityMappings);
}
return $this->entityMappings;
}
/**
* {@inheritdoc}
*/
function getEntityTypeForTokenType($token_type, $fallback = FALSE) {
if (empty($this->entityMappings)) {
$this->getEntityTypeMappings();
}
$return = array_search($token_type, $this->entityMappings);
return $return !== FALSE ? $return : ($fallback ? $token_type : FALSE);
}
/**
* {@inheritdoc}
*/
function getTokenTypeForEntityType($entity_type, $fallback = FALSE) {
if (empty($this->entityMappings)) {
$this->getEntityTypeMappings();
}
return isset($this->entityMappings[$entity_type]) ? $this->entityMappings[$entity_type] : ($fallback ? $entity_type : FALSE);
}
/**
* {@inheritdoc}
*/
public function resetInfo() {
$this->entityMappings = NULL;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Drupal\token;
interface TokenEntityMapperInterface {
/**
* Return an array of entity type to token type mappings.
*
* @return array
* An array of mappings with entity type mapping to token type.
*/
public function getEntityTypeMappings();
/**
* Return the entity type of a particular token type.
*
* @param string $token_type
* The token type for which the mapping is returned.
* @param bool $fallback
* (optional) Defaults to FALSE. If true, the same $value is returned in
* case the mapping was not found.
*
* @return string
* The entity type of the token type specified.
*
* @see token_entity_info_alter()
* @see http://drupal.org/node/737726
*/
function getEntityTypeForTokenType($token_type, $fallback = FALSE);
/**
* Return the token type of a particular entity type.
*
* @param string $entity_type
* The entity type for which the mapping is returned.
* @param bool $fallback
* (optional) Defaults to FALSE. If true, the same $value is returned in
* case the mapping was not found.
*
* @return string
* The token type of the entity type specified.
*
* @see token_entity_info_alter()
* @see http://drupal.org/node/737726
*/
function getTokenTypeForEntityType($entity_type, $fallback = FALSE);
/**
* Resets metadata describing token and entity mappings.
*/
public function resetInfo();
}

View file

@ -0,0 +1,75 @@
<?php
namespace Drupal\token;
interface TokenInterface {
/**
* Returns metadata describing supported token types.
*
* @param $token_type
* The token type for which the metadata is required.
*
* @return array[]
* An array of token type information from hook_token_info() for the
* specified token type.
*
* @see hook_token_info()
* @see hook_token_info_alter()
*/
public function getTypeInfo($token_type);
/**
* Returns metadata describing supported a token.
*
* @param $token_type
* The token type for which the metadata is required.
* @param $token
* The token name for which the metadata is required.
*
* @return array[]
* An array of information from hook_token_info() for the specified token.
*
* @see hook_token_info()
* @see hook_token_info_alter()
*
* @deprecated
*/
public function getTokenInfo($token_type, $token);
/**
* Get a list of token types that can be used without any context (global).
*
* @return array[]
* An array of global token types.
*/
public function getGlobalTokenTypes();
/**
* Validate an array of tokens based on their token type.
*
* @param string $type
* The type of tokens to validate (e.g. 'node', etc.)
* @param string[] $tokens
* A keyed array of tokens, and their original raw form in the source text.
*
* @return string[]
* An array with the invalid tokens in their original raw forms.
*/
function getInvalidTokens($type, $tokens);
/**
* Validate tokens in raw text based on possible contexts.
*
* @param string|string[] $value
* A string with the raw text containing the raw tokens, or an array of
* tokens from token_scan().
* @param string[] $valid_types
* An array of token types that will be used when token replacement is
* performed.
*
* @return string[]
* An array with the invalid tokens in their original raw forms.
*/
public function getInvalidTokensByContext($value, array $valid_types = []);
}

View file

@ -0,0 +1,20 @@
<?php
namespace Drupal\token;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
/**
* Replace core's token service with our own.
*/
class TokenServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
$definition = $container->getDefinition('token');
$definition->setClass('\Drupal\token\Token');
}
}

View file

@ -0,0 +1,267 @@
<?php
namespace Drupal\token;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\BubbleableMetadata;
class TreeBuilder implements TreeBuilderInterface {
/**
* @var \Drupal\token\Token
*/
protected $tokenService;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $entityMapper;
/**
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* Cache already built trees.
*
* @var array
*/
protected $builtTrees;
public function __construct(TokenInterface $token_service, TokenEntityMapperInterface $entity_mapper, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager) {
$this->tokenService = $token_service;
$this->entityMapper = $entity_mapper;
$this->cacheBackend = $cache_backend;
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public function buildRenderable(array $token_types, array $options = []) {
// Set default options.
$options += [
'global_types' => TRUE,
'click_insert' => TRUE,
'show_restricted' => FALSE,
'show_nested' => FALSE,
'recursion_limit' => 3,
];
$info = $this->tokenService->getInfo();
if ($options['global_types']) {
$token_types = array_merge($token_types, $this->tokenService->getGlobalTokenTypes());
}
$element = array(
/*'#cache' => array(
'cid' => 'tree-rendered:' . hash('sha256', serialize(array('token_types' => $token_types, 'global_types' => NULL) + $variables)),
'tags' => array(Token::TOKEN_INFO_CACHE_TAG),
),*/
);
// @todo Find a way to use the render cache for this.
/*if ($cached_output = token_render_cache_get($element)) {
return $cached_output;
}*/
$tree_options = [
'flat' => TRUE,
'restricted' => $options['show_restricted'],
'nested' => $options['show_nested'],
'depth' => $options['recursion_limit'],
];
$token_tree = [];
foreach ($info['types'] as $type => $type_info) {
if (!in_array($type, $token_types)) {
continue;
}
$token_tree[$type] = $type_info;
$token_tree[$type]['tokens'] = $this->buildTree($type, $tree_options);
}
$element += [
'#type' => 'token_tree_table',
'#token_tree' => $token_tree,
'#show_restricted' => $options['show_restricted'],
'#show_nested' => $options['show_nested'],
'#click_insert' => $options['click_insert'],
'#columns' => ['name', 'token', 'description'],
'#empty' => t('No tokens available'),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function buildAllRenderable(array $options = []) {
$info = $this->tokenService->getInfo();
$token_types = array_keys($info['types']);
// Disable merging in global types as we will be adding in all token types
// explicitly. There is no difference in leaving this set to TRUE except for
// an additional method call which is unnecessary.
$options['global_types'] = FALSE;
return $this->buildRenderable($token_types, $options);
}
/**
* {@inheritdoc}
*/
public function buildTree($token_type, array $options = []) {
$options += [
'restricted' => FALSE,
'depth' => 4,
'data' => [],
'values' => FALSE,
'flat' => FALSE,
];
// Do not allow past the maximum token information depth.
$options['depth'] = min($options['depth'], static::MAX_DEPTH);
// If $token_type is an entity, make sure we are using the actual token type.
if ($entity_token_type = $this->entityMapper->getTokenTypeForEntityType($token_type)) {
$token_type = $entity_token_type;
}
$langcode = $this->languageManager->getCurrentLanguage()->getId();
$tree_cid = "token_tree:{$token_type}:{$langcode}:{$options['depth']}";
// If we do not have this base tree in the static cache, check the cache
// otherwise generate and store it in the cache.
if (!isset($this->builtTrees[$tree_cid])) {
if ($cache = $this->cacheBackend->get($tree_cid)) {
$this->builtTrees[$tree_cid] = $cache->data;
}
else {
$options['parents'] = [];
$this->builtTrees[$tree_cid] = $this->getTokenData($token_type, $options);
$this->cacheBackend->set($tree_cid, $this->builtTrees[$tree_cid], Cache::PERMANENT, [Token::TOKEN_INFO_CACHE_TAG]);
}
}
$tree = $this->builtTrees[$tree_cid];
// If the user has requested a flat tree, convert it.
if (!empty($options['flat'])) {
$tree = $this->flattenTree($tree);
}
// Fill in token values.
if (!empty($options['values'])) {
$token_values = [];
foreach ($tree as $token => $token_info) {
if (!empty($token_info['dynamic']) || !empty($token_info['restricted'])) {
continue;
}
elseif (!isset($token_info['value'])) {
$token_values[$token_info['token']] = $token;
}
}
if (!empty($token_values)) {
$token_values = $this->tokenService->generate($token_type, $token_values, $options['data'], [], new BubbleableMetadata());
foreach ($token_values as $token => $replacement) {
$tree[$token]['value'] = $replacement;
}
}
}
return $tree;
}
/**
* {@inheritdoc}
*/
public function flattenTree(array $tree) {
$result = [];
foreach ($tree as $token => $token_info) {
$result[$token] = $token_info;
if (isset($token_info['children']) && is_array($token_info['children'])) {
$result += $this->flattenTree($token_info['children']);
}
}
return $result;
}
/**
* Generate a token tree.
*
* @param string $token_type
* The token type.
* @param array $options
* An associative array of additional options. See documentation for
* TreeBuilderInterface::buildTree() for more information.
*
* @return array
* The token data for the specified $token_type.
*
* @internal
*/
protected function getTokenData($token_type, array $options) {
$options += [
'parents' => [],
];
$info = $this->tokenService->getInfo();
if ($options['depth'] <= 0 || !isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) {
return [];
}
$tree = [];
foreach ($info['tokens'][$token_type] as $token => $token_info) {
// Build the raw token string.
$token_parents = $options['parents'];
if (empty($token_parents)) {
// If the parents array is currently empty, assume the token type is its
// parent.
$token_parents[] = $token_type;
}
elseif (in_array($token, array_slice($token_parents, 1), TRUE)) {
// Prevent duplicate recursive tokens. For example, this will prevent
// the tree from generating the following tokens or deeper:
// [comment:parent:parent]
// [comment:parent:root:parent]
continue;
}
$token_parents[] = $token;
if (!empty($token_info['dynamic'])) {
$token_parents[] = '?';
}
$raw_token = '[' . implode(':', $token_parents) . ']';
$tree[$raw_token] = $token_info;
$tree[$raw_token]['raw token'] = $raw_token;
// Add the token's real name (leave out the base token type).
$tree[$raw_token]['token'] = implode(':', array_slice($token_parents, 1));
// Add the token's parent as its raw token value.
if (!empty($options['parents'])) {
$tree[$raw_token]['parent'] = '[' . implode(':', $options['parents']) . ']';
}
// Fetch the child tokens.
if (!empty($token_info['type'])) {
$child_options = $options;
$child_options['depth']--;
$child_options['parents'] = $token_parents;
$tree[$raw_token]['children'] = $this->getTokenData($token_info['type'], $child_options);
}
}
return $tree;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Drupal\token;
interface TreeBuilderInterface {
/**
* The maximum depth for token tree recursion.
*/
const MAX_DEPTH = 9;
/**
* Build a tree array of tokens used for themeing or information.
*
* @param string $token_type
* The token type.
* @param array $options
* (optional) An associative array of additional options, with the following
* elements:
* - 'flat' (defaults to FALSE): Set to true to generate a flat list of
* token information. Otherwise, child tokens will be inside the
* 'children' parameter of a token.
* - 'restricted' (defaults to FALSE): Set to true to how restricted tokens.
* - 'depth' (defaults to 4): Maximum number of token levels to recurse.
*
* @return array
* The token information constructed in a tree or flat list form depending
* on $options['flat'].
*/
public function buildTree($token_type, array $options = []);
/**
* Flatten a token tree.
*
* @param array $tree
* The tree array as returned by TreeBuilderInterface::buildTree().
*
* @return array
* The flattened version of the tree.
*/
public function flattenTree(array $tree);
/**
* Build a render array with token tree built as per specified options.
*
* @param array $token_types
* An array containing token types that should be shown in the tree.
* @param array $options
* (optional) An associative array to control which tokens are shown and
* how. The properties available are:
* - 'global_types' (defaults to TRUE): Show all global token types along
* with the specified types.
* - 'click_insert' (defaults to TRUE): Include classes and caption to show
* allow inserting tokens in fields by clicking on them.
* - 'show_restricted' (defaults to FALSE): Show restricted tokens in the
* tree.
* - 'show_nested' (defaults to FALSE): If this token is nested and should
* therefor not show on the token browser as a top level token.
* - 'recursion_limit' (defaults to 3): Only show tokens up to the specified
* depth.
*
* @return array
* Render array for the token tree.
*/
public function buildRenderable(array $token_types, array $options = []);
/**
* Build a render array with token tree containing all possible tokens.
*
* @param array $options
* (optional) An associative array to control which tokens are shown and
* how. The properties available are: See
* \Drupal\token\TreeBuilderInterface::buildRenderable() for details.
*
* @return array
* Render array for the token tree.
*/
public function buildAllRenderable(array $options = []);
}