drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); ConfigurableLanguage::createFromLangcode('it')->save(); $this->rebuildContainer(); $this->editorUser = $this->drupalCreateUser(['access content', 'access contextual links', 'edit any article content']); $this->authenticatedUser = $this->drupalCreateUser(['access content', 'access contextual links']); $this->anonymousUser = $this->drupalCreateUser(['access content']); } /** * Tests contextual links with different permissions. * * Ensures that contextual link placeholders always exist, even if the user is * not allowed to use contextual links. */ public function testDifferentPermissions() { $this->drupalLogin($this->editorUser); // Create three nodes in the following order: // - An article, which should be user-editable. // - A page, which should not be user-editable. // - A second article, which should also be user-editable. $node1 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]); $node2 = $this->drupalCreateNode(['type' => 'page', 'promote' => 1]); $node3 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]); // Now, on the front page, all article nodes should have contextual links // placeholders, as should the view that contains them. $ids = [ 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en', 'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime() . '&langcode=en', 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=en', 'entity.view.edit_form:view=frontpage:location=page&name=frontpage&display_id=page_1&langcode=en', ]; // Editor user: can access contextual links and can edit articles. $this->drupalGet('node'); for ($i = 0; $i < count($ids); $i++) { $this->assertContextualLinkPlaceHolder($ids[$i]); } $response = $this->renderContextualLinks([], 'node'); $this->assertSame(400, $response->getStatusCode()); $this->assertContains('No contextual ids specified.', (string) $response->getBody()); $response = $this->renderContextualLinks($ids, 'node'); $this->assertSame(200, $response->getStatusCode()); $json = Json::decode((string) $response->getBody()); $this->assertIdentical($json[$ids[0]], ''); $this->assertIdentical($json[$ids[1]], ''); $this->assertIdentical($json[$ids[2]], ''); $this->assertIdentical($json[$ids[3]], ''); // Verify that link language is properly handled. $node3->addTranslation('it')->set('title', $this->randomString())->save(); $id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it'; $this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]); $this->assertContextualLinkPlaceHolder($id); // Authenticated user: can access contextual links, cannot edit articles. $this->drupalLogin($this->authenticatedUser); $this->drupalGet('node'); for ($i = 0; $i < count($ids); $i++) { $this->assertContextualLinkPlaceHolder($ids[$i]); } $response = $this->renderContextualLinks([], 'node'); $this->assertSame(400, $response->getStatusCode()); $this->assertContains('No contextual ids specified.', (string) $response->getBody()); $response = $this->renderContextualLinks($ids, 'node'); $this->assertSame(200, $response->getStatusCode()); $json = Json::decode((string) $response->getBody()); $this->assertIdentical($json[$ids[0]], ''); $this->assertIdentical($json[$ids[1]], ''); $this->assertIdentical($json[$ids[2]], ''); $this->assertIdentical($json[$ids[3]], ''); // Anonymous user: cannot access contextual links. $this->drupalLogin($this->anonymousUser); $this->drupalGet('node'); for ($i = 0; $i < count($ids); $i++) { $this->assertNoContextualLinkPlaceHolder($ids[$i]); } $response = $this->renderContextualLinks([], 'node'); $this->assertSame(403, $response->getStatusCode()); $this->renderContextualLinks($ids, 'node'); $this->assertSame(403, $response->getStatusCode()); // Get a page where contextual links are directly rendered. $this->drupalGet(Url::fromRoute('menu_test.contextual_test')); $this->assertEscaped(""); $this->assertRaw(''); } /** * Tests the contextual placeholder content is protected by a token. */ public function testTokenProtection() { $this->drupalLogin($this->editorUser); // Create a node that will have a contextual link. $node1 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]); // Now, on the front page, all article nodes should have contextual links // placeholders, as should the view that contains them. $id = 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en'; // Editor user: can access contextual links and can edit articles. $this->drupalGet('node'); $this->assertContextualLinkPlaceHolder($id); $http_client = $this->getHttpClient(); $url = Url::fromRoute('contextual.render', [], [ 'query' => [ '_format' => 'json', 'destination' => 'node', ], ])->setAbsolute()->toString(); $response = $http_client->request('POST', $url, [ 'cookies' => $this->getSessionCookies(), 'form_params' => ['ids' => [$id], 'tokens' => []], 'http_errors' => FALSE, ]); $this->assertEquals('400', $response->getStatusCode()); $this->assertContains('No contextual ID tokens specified.', (string) $response->getBody()); $response = $http_client->request('POST', $url, [ 'cookies' => $this->getSessionCookies(), 'form_params' => ['ids' => [$id], 'tokens' => ['wrong_token']], 'http_errors' => FALSE, ]); $this->assertEquals('400', $response->getStatusCode()); $this->assertContains('Invalid contextual ID specified.', (string) $response->getBody()); $response = $http_client->request('POST', $url, [ 'cookies' => $this->getSessionCookies(), 'form_params' => ['ids' => [$id], 'tokens' => ['wrong_key' => $this->createContextualIdToken($id)]], 'http_errors' => FALSE, ]); $this->assertEquals('400', $response->getStatusCode()); $this->assertContains('Invalid contextual ID specified.', (string) $response->getBody()); $response = $http_client->request('POST', $url, [ 'cookies' => $this->getSessionCookies(), 'form_params' => ['ids' => [$id], 'tokens' => [$this->createContextualIdToken($id)]], 'http_errors' => FALSE, ]); $this->assertEquals('200', $response->getStatusCode()); } /** * Asserts that a contextual link placeholder with the given id exists. * * @param string $id * A contextual link id. */ protected function assertContextualLinkPlaceHolder($id) { $this->assertSession()->elementAttributeContains( 'css', 'div[data-contextual-id="' . $id . '"]', 'data-contextual-token', $this->createContextualIdToken($id) ); } /** * Asserts that a contextual link placeholder with the given id does not exist. * * @param string $id * A contextual link id. */ protected function assertNoContextualLinkPlaceHolder($id) { $this->assertSession()->elementNotExists('css', 'div[data-contextual-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 \Psr\Http\Message\ResponseInterface * The response object. */ protected function renderContextualLinks($ids, $current_path) { $tokens = array_map([$this, 'createContextualIdToken'], $ids); $http_client = $this->getHttpClient(); $url = Url::fromRoute('contextual.render', [], [ 'query' => [ '_format' => 'json', 'destination' => $current_path, ], ]); return $http_client->request('POST', $this->buildUrl($url), [ 'cookies' => $this->getSessionCookies(), 'form_params' => ['ids' => $ids, 'tokens' => $tokens], 'http_errors' => FALSE, ]); } /** * Creates a contextual ID token. * * @param string $id * The contextual ID to create a token for. * * @return string * The contextual ID token. */ protected function createContextualIdToken($id) { return Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get()); } }