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: }