core/modules/contextual/contextual.js | 30 +++--
core/modules/contextual/contextual.module | 68 ++++++++--
.../lib/Drupal/contextual/ContextualController.php | 17 +--
.../Tests/ContextualDynamicContextTest.php | 137 ++++++++++++++++++--
.../Drupal/contextual/Tests/ContextualUnitTest.php | 97 ++++++++++++++
5 files changed, 299 insertions(+), 50 deletions(-)
diff --git a/core/modules/contextual/contextual.js b/core/modules/contextual/contextual.js
index 31e30e4..c06128c 100644
--- a/core/modules/contextual/contextual.js
+++ b/core/modules/contextual/contextual.js
@@ -3,7 +3,7 @@
* Attaches behaviors for the Contextual module.
*/
-(function ($, Drupal, document, window) {
+(function ($, Drupal, drupalSettings) {
"use strict";
@@ -49,22 +49,24 @@ Drupal.behaviors.contextual = {
ids.push($(this).attr('data-contextual-id'));
});
$.ajax({
- url: Drupal.url('contextual/render'),
+ url: Drupal.url('contextual/render') + '?destination=' + Drupal.encodePath(drupalSettings.currentPath),
type: 'POST',
data: { 'ids[]' : ids },
dataType: 'json',
success: function(results) {
- for (var id in results) if (results.hasOwnProperty(id)) {
- var $contextual = $context
- // Find the location for the current rendered contextual link.
- .find('[data-contextual-id="' + id + '"]')
- // Move it into the DOM.
- .html(results[id]);
- // Create a Drupal.contextual object and notify listeners of a new
- // contextual link.
- $(document).trigger('drupalContextualLinkAdded', {
- contextual: new Drupal.contextual($contextual, $contextual.closest('.contextual-region'))
- });
+ for (var id in results) {
+ if (results.hasOwnProperty(id)) {
+ var $contextual = $context
+ // Find the location for the current rendered contextual link.
+ .find('[data-contextual-id="' + id + '"]')
+ // Move it into the DOM.
+ .html(results[id]);
+ // Create a Drupal.contextual object and notify listeners of a new
+ // contextual link.
+ $(document).trigger('drupalContextualLinkAdded', {
+ contextual: new Drupal.contextual($contextual, $contextual.closest('.contextual-region'))
+ });
+ }
}
}
});
@@ -241,4 +243,4 @@ Drupal.theme.contextualTrigger = function () {
return '';
};
-})(jQuery, Drupal, document, window);
+})(jQuery, Drupal, drupalSettings);
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index ea5e439..0b00e2e 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -174,17 +174,8 @@ function contextual_preprocess(&$variables, $hook) {
// JavaScript to determine which contextual links should be rendered.
// This div with data- attribute is added unconditionally, and thus does not
// break the render cache.
- // Examples of the data- attribute syntax:
- // - node[node]:1
- // - views_ui[admin/structure/views/view]:frontpage
- // - menu[admin/structure/menu/manage]:tools|block[admin/structure/block/manage]:bartik.tools
- $id = '';
- foreach ($element['#contextual_links'] as $module => $args) {
- if (drupal_strlen($id) > 0) {
- $id .= '|';
- }
- $id .= $module . '[' . $args[0] . ']:' . implode(':', $args[1]);
- }
+ // @see _contextual_links_to_id()
+ $id = _contextual_links_to_id($element['#contextual_links']);
$variables['title_suffix']['contextual_links']['#id'] = $id;
$variables['title_suffix']['contextual_links']['#markup'] = '
';
}
@@ -244,3 +235,58 @@ function contextual_pre_render_links($element) {
return $element;
}
+/**
+ * Serializes #contextual_links property metadata to a "contextual id".
+ *
+ * Examples:
+ * - node[node]:1
+ * - views_ui[admin/structure/views/view]:frontpage
+ * - menu[admin/structure/menu/manage]:tools|block[admin/structure/block/manage]:bartik.tools
+ *
+ * So, expressed in a pattern:
+ * []::
+ *
+ * @param array $contextual_links
+ * The $element['#contextual_links'] value for some render element.
+ *
+ * @return string
+ * A contextual id.
+ */
+function _contextual_links_to_id($contextual_links) {
+ $id = '';
+ foreach ($contextual_links as $module => $args) {
+ if (drupal_strlen($id) > 0) {
+ $id .= '|';
+ }
+ $id .= $module . '[' . $args[0] . ']:' . implode(':', $args[1]);
+ }
+ return $id;
+}
+
+/**
+ * Serializes a contextual id back to #contextual_links property metadata.
+ *
+ * The inverse operation of _contextual_links_to_id().
+ *
+ * @see _contextual_links_to_id
+ *
+ * @param string $id
+ * A contextual id.
+ *
+ * @return array
+ * The value for a #contextual_links property.
+ */
+function _contextual_id_to_links($id) {
+ $contextual_links = array();
+ $contexts = explode('|', $id);
+ foreach ($contexts as $context) {
+ $args = explode(':', $context);
+ $provider = array_shift($args);
+ $pos = strpos($provider, '[');
+ $module = drupal_substr($provider, 0, $pos);
+ $parent_path = drupal_substr($provider, $pos + 1, drupal_strlen($provider) - $pos - 2);
+ $contextual_links[$module] = array($parent_path, $args);
+ }
+ return $contextual_links;
+}
+
diff --git a/core/modules/contextual/lib/Drupal/contextual/ContextualController.php b/core/modules/contextual/lib/Drupal/contextual/ContextualController.php
index f776844..5c986b6 100644
--- a/core/modules/contextual/lib/Drupal/contextual/ContextualController.php
+++ b/core/modules/contextual/lib/Drupal/contextual/ContextualController.php
@@ -32,28 +32,15 @@ class ContextualController extends ContainerAware {
public function render(Request $request) {
$ids = $request->request->get('ids');
if (!isset($ids)) {
- throw new BadRequestHttpException();
+ throw new BadRequestHttpException(t('No contextual ids specified.'));
}
$rendered = array();
foreach ($ids as $id) {
$element = array(
'#type' => 'contextual_links',
- '#contextual_links' => array(),
+ '#contextual_links' => _contextual_id_to_links($id),
);
-
- // Figure out which contextual links should be rendered.
- $contexts = explode('|', $id);
- foreach ($contexts as $context) {
- $args = explode(':', $context);
- $provider = array_shift($args);
- $pos = strpos($provider, '[');
- $module = drupal_substr($provider, 0, $pos);
- $parent_path = drupal_substr($provider, $pos + 1, drupal_strlen($provider) - $pos - 2);
- $element['#contextual_links'][$module] = array($parent_path, $args);
- }
-
- // Render the contextual links.
$rendered[$id] = drupal_render($element);
}
diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
index c657684..45f9fd6 100644
--- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
+++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php
@@ -19,7 +19,7 @@ class ContextualDynamicContextTest extends WebTestBase {
*
* @var array
*/
- public static $modules = array('contextual', 'node', 'views');
+ public static $modules = array('contextual', 'node', 'views', 'views_ui');
public static function getInfo() {
return array(
@@ -31,16 +31,24 @@ public static function getInfo() {
function setUp() {
parent::setUp();
+
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
- $web_user = $this->drupalCreateUser(array('access content', 'access contextual links', 'edit any article content'));
- $this->drupalLogin($web_user);
+
+ $this->editor_user = $this->drupalCreateUser(array('access content', 'access contextual links', 'edit any article content'));
+ $this->authenticated_user = $this->drupalCreateUser(array('access content', 'access contextual links'));
+ $this->anonymous_user = $this->drupalCreateUser(array('access content'));
}
/**
- * Tests contextual links on node lists with different permissions.
+ * Tests contextual links with different permissions.
+ *
+ * Ensures that contextual link placeholders always exist, even if the user is
+ * not allowed to use contextual links.
*/
- function testNodeLinks() {
+ function testDifferentPermissions() {
+ $this->drupalLogin($this->editor_user);
+
// Create three nodes in the following order:
// - An article, which should be user-editable.
// - A page, which should not be user-editable.
@@ -49,11 +57,120 @@ function testNodeLinks() {
$node2 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));
$node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
- // Now, on the front page, all article nodes should have contextual edit
- // links. The page node in between should not.
+ // Now, on the front page, all article nodes should have contextual links
+ // placeholders, as should the view that contains them.
+ $ids = array(
+ 'node[node]:' . $node1->nid,
+ 'node[node]:' . $node2->nid,
+ 'node[node]:' . $node3->nid,
+ 'views_ui[admin/structure/views/view]:frontpage',
+ );
+
+ // Editor user: can access contextual links and can edit articles.
+ $this->drupalGet('node');
+ for ($i = 0; $i < count($ids); $i++) {
+ $this->assertContextualLinkPlaceHolder($ids[$i]);
+ }
+ $this->renderContextualLinks(array(), 'node');
+ $this->assertResponse(400);
+ $this->assertRaw('No contextual ids specified.');
+ $response = $this->renderContextualLinks($ids, 'node');
+ $this->assertResponse(200);
+ $json = drupal_json_decode($response);
+ $this->assertIdentical($json[$ids[0]], '');
+ $this->assertIdentical($json[$ids[1]], NULL);
+ $this->assertIdentical($json[$ids[2]], '');
+ $this->assertIdentical($json[$ids[3]], NULL);
+
+ // Authenticated user: can access contextual links, cannot edit articles.
+ $this->drupalLogin($this->authenticated_user);
$this->drupalGet('node');
- $this->assertRaw('node/' . $node1->nid . '/edit', 'Edit link for oldest article node showing.');
- $this->assertNoRaw('node/' . $node2->nid . '/edit', 'No edit link for page nodes.');
- $this->assertRaw('node/' . $node3->nid . '/edit', 'Edit link for most recent article node showing.');
+ for ($i = 0; $i < count($ids); $i++) {
+ $this->assertContextualLinkPlaceHolder($ids[$i]);
+ }
+ $this->renderContextualLinks(array(), 'node');
+ $this->assertResponse(400);
+ $this->assertRaw('No contextual ids specified.');
+ $response = $this->renderContextualLinks($ids, 'node');
+ $this->assertResponse(200);
+ $json = drupal_json_decode($response);
+ $this->assertIdentical($json[$ids[0]], NULL);
+ $this->assertIdentical($json[$ids[1]], NULL);
+ $this->assertIdentical($json[$ids[2]], NULL);
+ $this->assertIdentical($json[$ids[3]], NULL);
+
+ // Anonymous user: cannot access contextual links.
+ $this->drupalLogin($this->anonymous_user);
+ $this->drupalGet('node');
+ for ($i = 0; $i < count($ids); $i++) {
+ $this->assertContextualLinkPlaceHolder($ids[$i]);
+ }
+ $this->renderContextualLinks(array(), 'node');
+ $this->assertResponse(403);
+ $this->renderContextualLinks($ids, 'node');
+ $this->assertResponse(403);
+ }
+
+ /**
+ * Asserts that a contextual link placeholder with the given id exists.
+ *
+ * @param string $id
+ * A contextual link id.
+ *
+ * @return bool
+ */
+ protected function assertContextualLinkPlaceHolder($id) {
+ $this->assertRaw('', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
+ }
+
+ /**
+ * Asserts that a contextual link placeholder with the given id does not exist.
+ *
+ * @param string $id
+ * A contextual link id.
+ *
+ * @return bool
+ */
+ protected function assertNoContextualLinkPlaceHolder($id) {
+ $this->assertNoRaw('', format_string('Contextual link placeholder with id @id does not exist.', array('@id' => $id)));
+ }
+
+ /**
+ * Get server-rendered contextual links for the given contextual link ids.
+ *
+ * @param array $ids
+ * An array of contextual link ids.
+ * @param string $current_path
+ * The Drupal path for the page for which the contextual links are rendered.
+ *
+ * @return string
+ * The response body.
+ */
+ protected function renderContextualLinks($ids, $current_path) {
+ // Build POST values.
+ $post = array();
+ for ($i = 0; $i < count($ids); $i++) {
+ $post['ids[' . $i . ']'] = $ids[$i];
+ }
+
+ // Serialize POST values.
+ foreach ($post as $key => $value) {
+ // Encode according to application/x-www-form-urlencoded
+ // Both names and values needs to be urlencoded, according to
+ // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
+ $post[$key] = urlencode($key) . '=' . urlencode($value);
+ }
+ $post = implode('&', $post);
+
+ // Perform HTTP request.
+ return $this->curlExec(array(
+ CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => $current_path))),
+ CURLOPT_POST => TRUE,
+ CURLOPT_POSTFIELDS => $post,
+ CURLOPT_HTTPHEADER => array(
+ 'Accept: application/json',
+ 'Content-Type: application/x-www-form-urlencoded',
+ ),
+ ));
}
}
diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php
new file mode 100644
index 0000000..5c545dc
--- /dev/null
+++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php
@@ -0,0 +1,97 @@
+ 'Conversion to and from "contextual id"s (for placeholders)',
+ 'description' => 'Tests all edge cases of converting from #contextual_links to ids and vice versa.',
+ 'group' => 'Contextual',
+ );
+ }
+
+ /**
+ * Provides testcases for testContextualLinksToId() and
+ */
+ function _contextual_links_id_testcases() {
+ // Test branch conditions:
+ // - one module.
+ // - one dynamic path argument.
+ $tests[] = array(
+ 'links' => array(
+ 'node' => array(
+ 'node',
+ array('14031991')
+ ),
+ ),
+ 'id' => 'node[node]:14031991',
+ );
+
+ // Test branch conditions:
+ // - one module.
+ // - multiple dynamic path arguments.
+ $tests[] = array(
+ 'links' => array(
+ 'foo' => array(
+ 'baz/in/ga',
+ array('bar', 'baz', 'qux')
+ ),
+ ),
+ 'id' => 'foo[baz/in/ga]:bar:baz:qux',
+ );
+
+ // Test branch conditions:
+ // - multiple modules.
+ // - multiple dynamic path arguments.
+ $tests[] = array(
+ 'links' => array(
+ 'node' => array(
+ 'node',
+ array('14031991')
+ ),
+ 'foo' => array(
+ 'baz/in/ga',
+ array('bar', 'baz', 'qux')
+ ),
+ 'edge' => array(
+ 'edge',
+ array('20011988')
+ ),
+ ),
+ 'id' => 'node[node]:14031991|foo[baz/in/ga]:bar:baz:qux|edge[edge]:20011988',
+ );
+
+ return $tests;
+ }
+
+ /**
+ * Tests _contextual_links_to_id().
+ */
+ function testContextualLinksToId() {
+ $tests = $this->_contextual_links_id_testcases();
+ foreach ($tests as $test) {
+ $this->assertIdentical(_contextual_links_to_id($test['links']), $test['id']);
+ }
+ }
+
+ /**
+ * Tests _contextual_id_to_links().
+ */
+ function testContextualIdToLinks() {
+ $tests = $this->_contextual_links_id_testcases();
+ foreach ($tests as $test) {
+ $this->assertIdentical(_contextual_id_to_links($test['id']), $test['links']);
+ }
+ }
+}