diff --git a/core/core.services.yml b/core/core.services.yml index a2d373c..3bdb387 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -604,7 +604,7 @@ services: arguments: ['@module_handler'] token: class: Drupal\Core\Utility\Token - arguments: ['@module_handler'] + arguments: ['@module_handler', '@string_translation'] batch.storage: class: Drupal\Core\Batch\BatchStorage arguments: ['@database'] diff --git a/core/lib/Drupal/Core/Utility/Token.php b/core/lib/Drupal/Core/Utility/Token.php index 29fef78..d9484d6 100644 --- a/core/lib/Drupal/Core/Utility/Token.php +++ b/core/lib/Drupal/Core/Utility/Token.php @@ -67,6 +67,16 @@ class Token { protected $tokenInfo; /** + * The global token types. + * + * @var array|null + * An array of token types, or NULL when the types are not set. + * + * @see self::getGlobalTypes() + */ + protected $globalTypes; + + /** * The module handler service. * * @var \Drupal\Core\Extension\ModuleHandlerInterface @@ -305,5 +315,44 @@ public function setInfo(array $tokens) { */ public function resetInfo() { $this->tokenInfo = NULL; + $this->globalTypes = array(); + } + + /** + * Returns the global token types. + */ + public function getGlobalTypes() { + if (is_null($this->globalTypes)) { + $this->globalTypes = array(); + $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->globalTypes[] = $type; + } + } + } + + return $this->globalTypes; + } + + /** + * Gets a token type's real type. + * + * Some token types are aliases for other types. This method gets the + * un-aliased type that owns the tokens. + * + * @param string $type + * + * @return string + */ + public function getRealType($type) { + $token_info = $this->getInfo(); + if (array_key_exists('type', $token_info['types'][$type])) { + return $this->getRealType($token_info['types'][$type]['type']); + } + return $type; } } diff --git a/core/modules/system/lib/Drupal/system/Controller/TokenController.php b/core/modules/system/lib/Drupal/system/Controller/TokenController.php new file mode 100644 index 0000000..61b3f82 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Controller/TokenController.php @@ -0,0 +1,183 @@ +currentUser = $current_user; + $this->request = $request; + $this->token = $token; + $this->translationManager = $translation_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('request'), + $container->get('current_user'), + $container->get('token'), + $container->get('string_translation') + ); + } + + /** + * Checks access to the token overview. + * + * The following GET parameters can be used to manipulate the overview: + * - limitTypes: an array of the token types to limit the overview to. + * Defaults to an empty array. + * - showGlobalTypes: a boolean to display all global types, in addition to + * the types in "limitTypes". Defaults to FALSE. + * + * @return array + * A renderable array. + */ + public function overview() { + if ($this->request->query->has('limitTypes')) { + $limit_types = $this->request->query->get('limitTypes'); + } + else { + $token_info = $this->token->getInfo(); + $limit_types = array_keys($token_info['types']); + } + $all_global_types = $this->request->query->has('showGlobalTypes') ? $this->request->query->get('showGlobalTypes') : FALSE; + if ($all_global_types) { + $limit_types += $this->token->getGlobalTypes(); + } + + return $this->buildOverview($limit_types); + } + + /** + * Checks access to the token overview. + */ + public function overviewAccess(Request $request) { + return $this->currentUser->hasPermission('system.token.overview.view') ? self::ALLOW : self::DENY; + } + + /** + * Builds a token overview for the specified types. + * + * @param array $limit_types + * The token types for which to render the overview. + * + * @return array + * A renderable array. + */ + public function buildOverview(array $limit_types) { + $build = array( + '#empty' => $this->t('There are no available tokens.'), + '#header' => array($this->t('Name'), $this->t('Token'), $this->t('Description')), + '#rows' => array(), + '#type' => 'table', + ); + $token_info = $this->token->getInfo(); + foreach ($limit_types as $type) { + $real_type = $this->token->getRealType($type); + $tokens = $token_info['tokens'][$real_type]; + foreach ($tokens as $token => $token_definition) { + $build['#rows'] = array_merge($build['#rows'], $this->buildOverviewRows($real_type, $token, array($type))); + } + } + + return $build; + } + + /** + * Builds rows for the token overview. + * + * @param string $type + * The type of the token that is displayed. + * @param string $token + * The token that is displayed. + * @param array $parent_selectors + * An array of token selectors that point to the token that is displayed. + * + * @return array + * A renderable array. + */ + protected function buildOverviewRows($type, $token, array $parent_selectors) { + $token_info = $this->token->getInfo(); + $token_definition = $token_info['tokens'][$type][$token]; + $selectors = array_merge($parent_selectors, array($token)); + $row = array( + 'name' => $token_definition['name'], + 'token' => '[' . implode(':', $selectors) . ']', + 'description' => isset($token_definition['description']) ? $token_definition['description'] : NULL, + ); + $rows = array($row); + if (!empty($token_definition['needs-data'])) { + foreach (array_keys($token_info['tokens'][$token_definition['needs-data']]) as $needs_token) { + $rows += $this->buildOverviewRows($token_definition['needs-data'], $needs_token, $selectors); + } + } + + return $rows; + } + + /** + * Translates a string to the current language or to a given language. + * + * See the t() documentation for details. + */ + protected function t($string, array $args = array(), array $options = array()) { + return $this->translationManager->translate($string, $args, $options); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Controller/TokenControllerWebTest.php b/core/modules/system/lib/Drupal/system/Tests/Controller/TokenControllerWebTest.php new file mode 100644 index 0000000..00478f9 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Controller/TokenControllerWebTest.php @@ -0,0 +1,39 @@ + '', + 'name' => '\Drupal\system\Controller\TokenController web test', + 'group' => 'System', + ); + } + + /** + * Tests the token overview. + */ + public function testOverview() { + $this->drupalGet('token/overview'); + $this->assertResponse('403'); + + $user = $this->drupalCreateUser(array('system.token.overview.view')); + $this->drupalLogin($user); + $this->drupalGet('token/overview'); + $this->assertResponse('200'); + } +} diff --git a/core/modules/system/system.module b/core/modules/system/system.module index c68117b..60be7d1 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -220,6 +220,9 @@ function system_permission() { 'access site reports' => array( 'title' => t('View site reports'), ), + 'system.token.overview.view' => array( + 'title' => t('Access the token overview'), + ), ); } diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 4d27aa6..18df254 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -395,3 +395,10 @@ system.batch_page.json: system.update: path: '/core/update.php' + +system.token.overview.all: + path: '/token/overview' + defaults: + _controller: '\Drupal\system\Controller\TokenController::overview' + requirements: + _custom_access: '\Drupal\system\Controller\TokenController::overviewAccess' diff --git a/core/modules/system/tests/Drupal/system/Tests/Controller/TokenControllerUnitTest.php b/core/modules/system/tests/Drupal/system/Tests/Controller/TokenControllerUnitTest.php new file mode 100644 index 0000000..3493629 --- /dev/null +++ b/core/modules/system/tests/Drupal/system/Tests/Controller/TokenControllerUnitTest.php @@ -0,0 +1,141 @@ + '', + 'name' => '\Drupal\system\Controller\TokenController unit test', + 'group' => 'System', + ); + } + + /** + * {@inheritdoc + */ + public function setUp() { + $this->currentUser = $this->getMock('\Drupal\Core\Session\AccountInterface'); + + $this->request = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request') + ->disableOriginalConstructor() + ->getMock(); + + $this->token = $this->getMockBuilder('\Drupal\Core\Utility\Token') + ->disableOriginalConstructor() + ->getMock(); + + $this->translationManager = $this->getMock('\Drupal\Core\StringTranslation\TranslationInterface'); + + $this->tokenController = new TokenController($this->request, $this->currentUser, $this->token, $this->translationManager); + } + + /** + * Tests overviewAccess(). + */ + public function testOverviewAccess() { + $this->currentUser->expects($this->once()) + ->method('hasPermission') + ->with('system.token.overview.view') + ->will($this->returnValue(TRUE)); + $this->assertSame(AccessInterface::ALLOW, $this->tokenController->overviewAccess($this->request)); + } + + /** + * Tests buildOverview(). + */ + public function testBuildOverview() { + + $token_info = array( + 'types' => array( + 'foo' => array( + 'name' => $this->randomName(), + 'needs-data' => TRUE, + ), + 'bar' => array( + 'name' => $this->randomName(), + 'needs-data' => FALSE, + ), + ), + 'tokens' => array( + 'foo' => array( + 'baz' => array( + 'name' => 'Baz', + ), + ), + 'bar' => array( + 'qux' => array( + 'name' => 'Qux', + ), + ), + ), + ); + /** @var \Drupal\Core\Utility\Token|\PHPUnit_Framework_MockObject_MockObject $token */ + $this->token->expects($this->any()) + ->method('getInfo') + ->will($this->returnValue($token_info)); + $this->token->expects($this->any()) + ->method('getGlobalTypes') + ->will($this->returnValue(array('bar'))); + $this->token->expects($this->any()) + ->method('getRealType') + ->will($this->returnArgument(0)); + + $non_global_types = array('foo'); + $build = $this->tokenController->buildOverview($non_global_types); + $this->assertInternalType('array', $build); + $this->arrayHasKey('#type', $build); + $this->assertSame('table', $build['#type']); + // @todo Test that the table has rows for both token types. + } +} diff --git a/core/tests/Drupal/Tests/Core/Utility/TokenUnitTest.php b/core/tests/Drupal/Tests/Core/Utility/TokenUnitTest.php new file mode 100644 index 0000000..036aeb6 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Utility/TokenUnitTest.php @@ -0,0 +1,109 @@ + '', + 'name' => '\Drupal\Tests\Core\Utility\Token unit test', + 'group' => 'System', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface'); + + $this->token = new Token($this->moduleHandler); + } + + /** + * Tests getGlobalTypes(). + */ + public function testGetGlobalTypes() { + $token_info = array( + 'types' => array( + 'foo' => array( + 'name' => $this->randomName(), + 'needs-data' => TRUE, + ), + 'bar' => array( + 'name' => $this->randomName(), + 'needs-data' => FALSE, + ), + ), + ); + /** @var \Drupal\Core\Utility\Token|\PHPUnit_Framework_MockObject_MockObject $token */ + $token = $this->getMockBuilder('\Drupal\Core\Utility\Token') + ->setConstructorArgs(array($this->moduleHandler)) + ->setMethods(array('getInfo')) + ->getMock(); + $token->expects($this->any()) + ->method('getInfo') + ->will($this->returnValue($token_info)); + + $this->assertSame(array('bar'), $token->getGlobalTypes()); + } + + /** + * Tests getRealType(). + */ + public function testGetRealType() { + $token_info = array( + 'types' => array( + 'foo' => array( + 'type' => 'bar', + ), + 'bar' => array( + 'type' => 'baz', + ), + 'baz' => array( + ), + ), + ); + /** @var \Drupal\Core\Utility\Token|\PHPUnit_Framework_MockObject_MockObject $token */ + $token = $this->getMockBuilder('\Drupal\Core\Utility\Token') + ->setConstructorArgs(array($this->moduleHandler)) + ->setMethods(array('getInfo')) + ->getMock(); + $token->expects($this->any()) + ->method('getInfo') + ->will($this->returnValue($token_info)); + + $this->assertSame('baz', $token->getRealType('foo')); + $this->assertSame('baz', $token->getRealType('bar')); + $this->assertSame('baz', $token->getRealType('baz')); + } + +} \ No newline at end of file