ContextualDynamicContextTest.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <?php
  2. namespace Drupal\Tests\contextual\Functional;
  3. use Drupal\Component\Serialization\Json;
  4. use Drupal\Component\Utility\Crypt;
  5. use Drupal\Core\Site\Settings;
  6. use Drupal\Core\Url;
  7. use Drupal\language\Entity\ConfigurableLanguage;
  8. use Drupal\Tests\BrowserTestBase;
  9. /**
  10. * Tests if contextual links are showing on the front page depending on
  11. * permissions.
  12. *
  13. * @group contextual
  14. */
  15. class ContextualDynamicContextTest extends BrowserTestBase {
  16. /**
  17. * A user with permission to access contextual links and edit content.
  18. *
  19. * @var \Drupal\user\UserInterface
  20. */
  21. protected $editorUser;
  22. /**
  23. * An authenticated user with permission to access contextual links.
  24. *
  25. * @var \Drupal\user\UserInterface
  26. */
  27. protected $authenticatedUser;
  28. /**
  29. * A simulated anonymous user with access only to node content.
  30. *
  31. * @var \Drupal\user\UserInterface
  32. */
  33. protected $anonymousUser;
  34. /**
  35. * Modules to enable.
  36. *
  37. * @var array
  38. */
  39. public static $modules = ['contextual', 'node', 'views', 'views_ui', 'language', 'menu_test'];
  40. protected function setUp() {
  41. parent::setUp();
  42. $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
  43. $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
  44. ConfigurableLanguage::createFromLangcode('it')->save();
  45. $this->rebuildContainer();
  46. $this->editorUser = $this->drupalCreateUser(['access content', 'access contextual links', 'edit any article content']);
  47. $this->authenticatedUser = $this->drupalCreateUser(['access content', 'access contextual links']);
  48. $this->anonymousUser = $this->drupalCreateUser(['access content']);
  49. }
  50. /**
  51. * Tests contextual links with different permissions.
  52. *
  53. * Ensures that contextual link placeholders always exist, even if the user is
  54. * not allowed to use contextual links.
  55. */
  56. public function testDifferentPermissions() {
  57. $this->drupalLogin($this->editorUser);
  58. // Create three nodes in the following order:
  59. // - An article, which should be user-editable.
  60. // - A page, which should not be user-editable.
  61. // - A second article, which should also be user-editable.
  62. $node1 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]);
  63. $node2 = $this->drupalCreateNode(['type' => 'page', 'promote' => 1]);
  64. $node3 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]);
  65. // Now, on the front page, all article nodes should have contextual links
  66. // placeholders, as should the view that contains them.
  67. $ids = [
  68. 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en',
  69. 'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime() . '&langcode=en',
  70. 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=en',
  71. 'entity.view.edit_form:view=frontpage:location=page&name=frontpage&display_id=page_1&langcode=en',
  72. ];
  73. // Editor user: can access contextual links and can edit articles.
  74. $this->drupalGet('node');
  75. for ($i = 0; $i < count($ids); $i++) {
  76. $this->assertContextualLinkPlaceHolder($ids[$i]);
  77. }
  78. $response = $this->renderContextualLinks([], 'node');
  79. $this->assertSame(400, $response->getStatusCode());
  80. $this->assertContains('No contextual ids specified.', (string) $response->getBody());
  81. $response = $this->renderContextualLinks($ids, 'node');
  82. $this->assertSame(200, $response->getStatusCode());
  83. $json = Json::decode((string) $response->getBody());
  84. $this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="entitynodeedit-form"><a href="' . base_path() . 'node/1/edit">Edit</a></li></ul>');
  85. $this->assertIdentical($json[$ids[1]], '');
  86. $this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="entitynodeedit-form"><a href="' . base_path() . 'node/3/edit">Edit</a></li></ul>');
  87. $this->assertIdentical($json[$ids[3]], '');
  88. // Verify that link language is properly handled.
  89. $node3->addTranslation('it')->set('title', $this->randomString())->save();
  90. $id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it';
  91. $this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]);
  92. $this->assertContextualLinkPlaceHolder($id);
  93. // Authenticated user: can access contextual links, cannot edit articles.
  94. $this->drupalLogin($this->authenticatedUser);
  95. $this->drupalGet('node');
  96. for ($i = 0; $i < count($ids); $i++) {
  97. $this->assertContextualLinkPlaceHolder($ids[$i]);
  98. }
  99. $response = $this->renderContextualLinks([], 'node');
  100. $this->assertSame(400, $response->getStatusCode());
  101. $this->assertContains('No contextual ids specified.', (string) $response->getBody());
  102. $response = $this->renderContextualLinks($ids, 'node');
  103. $this->assertSame(200, $response->getStatusCode());
  104. $json = Json::decode((string) $response->getBody());
  105. $this->assertIdentical($json[$ids[0]], '');
  106. $this->assertIdentical($json[$ids[1]], '');
  107. $this->assertIdentical($json[$ids[2]], '');
  108. $this->assertIdentical($json[$ids[3]], '');
  109. // Anonymous user: cannot access contextual links.
  110. $this->drupalLogin($this->anonymousUser);
  111. $this->drupalGet('node');
  112. for ($i = 0; $i < count($ids); $i++) {
  113. $this->assertNoContextualLinkPlaceHolder($ids[$i]);
  114. }
  115. $response = $this->renderContextualLinks([], 'node');
  116. $this->assertSame(403, $response->getStatusCode());
  117. $this->renderContextualLinks($ids, 'node');
  118. $this->assertSame(403, $response->getStatusCode());
  119. // Get a page where contextual links are directly rendered.
  120. $this->drupalGet(Url::fromRoute('menu_test.contextual_test'));
  121. $this->assertEscaped("<script>alert('Welcome to the jungle!')</script>");
  122. $this->assertRaw('<li class="menu-testcontextual-hidden-manage-edit"><a href="' . base_path() . 'menu-test-contextual/1/edit" class="use-ajax" data-dialog-type="modal" data-is-something>Edit menu - contextual</a></li>');
  123. }
  124. /**
  125. * Tests the contextual placeholder content is protected by a token.
  126. */
  127. public function testTokenProtection() {
  128. $this->drupalLogin($this->editorUser);
  129. // Create a node that will have a contextual link.
  130. $node1 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]);
  131. // Now, on the front page, all article nodes should have contextual links
  132. // placeholders, as should the view that contains them.
  133. $id = 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en';
  134. // Editor user: can access contextual links and can edit articles.
  135. $this->drupalGet('node');
  136. $this->assertContextualLinkPlaceHolder($id);
  137. $http_client = $this->getHttpClient();
  138. $url = Url::fromRoute('contextual.render', [], [
  139. 'query' => [
  140. '_format' => 'json',
  141. 'destination' => 'node',
  142. ],
  143. ])->setAbsolute()->toString();
  144. $response = $http_client->request('POST', $url, [
  145. 'cookies' => $this->getSessionCookies(),
  146. 'form_params' => ['ids' => [$id], 'tokens' => []],
  147. 'http_errors' => FALSE,
  148. ]);
  149. $this->assertEquals('400', $response->getStatusCode());
  150. $this->assertContains('No contextual ID tokens specified.', (string) $response->getBody());
  151. $response = $http_client->request('POST', $url, [
  152. 'cookies' => $this->getSessionCookies(),
  153. 'form_params' => ['ids' => [$id], 'tokens' => ['wrong_token']],
  154. 'http_errors' => FALSE,
  155. ]);
  156. $this->assertEquals('400', $response->getStatusCode());
  157. $this->assertContains('Invalid contextual ID specified.', (string) $response->getBody());
  158. $response = $http_client->request('POST', $url, [
  159. 'cookies' => $this->getSessionCookies(),
  160. 'form_params' => ['ids' => [$id], 'tokens' => ['wrong_key' => $this->createContextualIdToken($id)]],
  161. 'http_errors' => FALSE,
  162. ]);
  163. $this->assertEquals('400', $response->getStatusCode());
  164. $this->assertContains('Invalid contextual ID specified.', (string) $response->getBody());
  165. $response = $http_client->request('POST', $url, [
  166. 'cookies' => $this->getSessionCookies(),
  167. 'form_params' => ['ids' => [$id], 'tokens' => [$this->createContextualIdToken($id)]],
  168. 'http_errors' => FALSE,
  169. ]);
  170. $this->assertEquals('200', $response->getStatusCode());
  171. }
  172. /**
  173. * Asserts that a contextual link placeholder with the given id exists.
  174. *
  175. * @param string $id
  176. * A contextual link id.
  177. */
  178. protected function assertContextualLinkPlaceHolder($id) {
  179. $this->assertSession()->elementAttributeContains(
  180. 'css',
  181. 'div[data-contextual-id="' . $id . '"]',
  182. 'data-contextual-token',
  183. $this->createContextualIdToken($id)
  184. );
  185. }
  186. /**
  187. * Asserts that a contextual link placeholder with the given id does not exist.
  188. *
  189. * @param string $id
  190. * A contextual link id.
  191. */
  192. protected function assertNoContextualLinkPlaceHolder($id) {
  193. $this->assertSession()->elementNotExists('css', 'div[data-contextual-id="' . $id . '"]');
  194. }
  195. /**
  196. * Get server-rendered contextual links for the given contextual link ids.
  197. *
  198. * @param array $ids
  199. * An array of contextual link ids.
  200. * @param string $current_path
  201. * The Drupal path for the page for which the contextual links are rendered.
  202. *
  203. * @return \Psr\Http\Message\ResponseInterface
  204. * The response object.
  205. */
  206. protected function renderContextualLinks($ids, $current_path) {
  207. $tokens = array_map([$this, 'createContextualIdToken'], $ids);
  208. $http_client = $this->getHttpClient();
  209. $url = Url::fromRoute('contextual.render', [], [
  210. 'query' => [
  211. '_format' => 'json',
  212. 'destination' => $current_path,
  213. ],
  214. ]);
  215. return $http_client->request('POST', $this->buildUrl($url), [
  216. 'cookies' => $this->getSessionCookies(),
  217. 'form_params' => ['ids' => $ids, 'tokens' => $tokens],
  218. 'http_errors' => FALSE,
  219. ]);
  220. }
  221. /**
  222. * Creates a contextual ID token.
  223. *
  224. * @param string $id
  225. * The contextual ID to create a token for.
  226. *
  227. * @return string
  228. * The contextual ID token.
  229. */
  230. protected function createContextualIdToken($id) {
  231. return Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get());
  232. }
  233. }