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