diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index e89ce31..feb6cec 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -12,6 +12,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Link; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\Url; /** @@ -310,6 +311,19 @@ protected function urlRouteParameters($rel) { $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId(); } + // Nodes and taxonomy link to all possible link relations in the HTML head, + // so we need to support calling $entity->toUrl() for the content + // translation link relations, even though that does not make sense + // semantically without being able to specify the translation language. + // @todo Remove this after https://www.drupal.org/node/2113345 + elseif ($rel === 'drupal:content-translation-add' && $this instanceof TranslatableInterface) { + $uri_route_parameters['source'] = $this->language()->getId(); + $uri_route_parameters['target'] = $this->language()->getId(); + } + elseif (in_array($rel, ['drupal:content-translation-edit', 'drupal:content-translation-delete'], TRUE) && $this instanceof TranslatableInterface) { + $uri_route_parameters['language'] = $this->language()->getId(); + } + return $uri_route_parameters; } diff --git a/core/modules/content_translation/src/Access/ContentTranslationManageAccessCheck.php b/core/modules/content_translation/src/Access/ContentTranslationManageAccessCheck.php index 6a4be1e..3360b19 100644 --- a/core/modules/content_translation/src/Access/ContentTranslationManageAccessCheck.php +++ b/core/modules/content_translation/src/Access/ContentTranslationManageAccessCheck.php @@ -93,6 +93,9 @@ public function access(Route $route, RouteMatchInterface $route_match, AccountIn /* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */ $handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation'); + // Reload unchanged entity to get stored translation only. + $entity = $this->entityManager->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id()); + // Load translation. $translations = $entity->getTranslationLanguages(); $languages = $this->languageManager->getLanguages(); diff --git a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php index fb4b761..1dfe592 100644 --- a/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php +++ b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php @@ -82,8 +82,6 @@ protected function alterRoutes(RouteCollection $collection) { $path . '/add/{source}/{target}', array( '_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::add', - 'source' => NULL, - 'target' => NULL, '_title' => 'Add', 'entity_type_id' => $entity_type_id, @@ -113,7 +111,6 @@ protected function alterRoutes(RouteCollection $collection) { $path . '/edit/{language}', array( '_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::edit', - 'language' => NULL, '_title' => 'Edit', 'entity_type_id' => $entity_type_id, ), @@ -138,7 +135,6 @@ protected function alterRoutes(RouteCollection $collection) { $path . '/delete/{language}', array( '_entity_form' => $entity_type_id . '.content_translation_deletion', - 'language' => NULL, '_title' => 'Delete', 'entity_type_id' => $entity_type_id, ), diff --git a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php index e9fff8d..2136b74 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php +++ b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php @@ -108,6 +108,21 @@ protected function doTestBasicTranslation() { 'source' => $default_langcode, 'target' => $langcode ], array('language' => $language)); + + // Ensure that there are not 'Add' breadcrumbs. + $url_string = $add_url->toString(); + $this->drupalGet($url_string); + $links = $this->xpath('//nav[@class="breadcrumb"]/ol/li/a[text()="Add"]'); + $this->assert(empty($links), 'No "Add" breadcrumbs.'); + + // Ensure that removing the last two url parts does not yield a fatal error. + $url_parts = explode('/', $url_string); + for ($i = 0; $i < 2; $i++) { + array_pop($url_parts); + $this->drupalGet(implode('/', $url_parts)); + $this->assertResponse(404); + } + $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode)); // Assert that HTML is escaped in "all languages" in UI after SafeMarkup diff --git a/core/modules/content_translation/tests/src/Unit/Access/ContentTranslationManageAccessCheckTest.php b/core/modules/content_translation/tests/src/Unit/Access/ContentTranslationManageAccessCheckTest.php index 7390c0e..d073c1b 100644 --- a/core/modules/content_translation/tests/src/Unit/Access/ContentTranslationManageAccessCheckTest.php +++ b/core/modules/content_translation/tests/src/Unit/Access/ContentTranslationManageAccessCheckTest.php @@ -54,11 +54,49 @@ public function testCreateAccess() { ->method('getTranslationAccess') ->will($this->returnValue(AccessResult::allowed())); + // Set the mock entity. We need to use ContentEntityBase for mocking due to + // issues with phpunit and multiple interfaces. + $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') + ->disableOriginalConstructor() + ->getMock(); + $entity->expects($this->exactly(2)) + ->method('getEntityTypeId') + ->willReturn('node'); + $entity->expects($this->once()) + ->method('getTranslationLanguages') + ->with() + ->will($this->returnValue(array())); + $entity->expects($this->once()) + ->method('getCacheContexts') + ->willReturn([]); + $entity->expects($this->once()) + ->method('getCacheMaxAge') + ->willReturn(Cache::PERMANENT); + $entity->expects($this->once()) + ->method('getCacheTags') + ->will($this->returnValue(array('node:1337'))); + $entity->expects($this->once()) + ->method('getCacheContexts') + ->willReturn(array()); + $entity->expects($this->once()) + ->method('id') + ->willReturn(1337); + + $entity_storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface'); + $entity_storage->expects($this->once()) + ->method('loadUnchanged') + ->with(1337) + ->willReturn($entity); + $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); $entity_manager->expects($this->once()) ->method('getHandler') ->withAnyParameters() ->will($this->returnValue($translation_handler)); + $entity_manager->expects($this->once()) + ->method('getStorage') + ->with('node') + ->willReturn($entity_storage); // Set our source and target languages. $source = 'en'; @@ -82,30 +120,6 @@ public function testCreateAccess() { ->with($this->equalTo($target)) ->will($this->returnValue(new Language(array('id' => 'it')))); - // Set the mock entity. We need to use ContentEntityBase for mocking due to - // issues with phpunit and multiple interfaces. - $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') - ->disableOriginalConstructor() - ->getMock(); - $entity->expects($this->once()) - ->method('getEntityTypeId'); - $entity->expects($this->once()) - ->method('getTranslationLanguages') - ->with() - ->will($this->returnValue(array())); - $entity->expects($this->once()) - ->method('getCacheContexts') - ->willReturn([]); - $entity->expects($this->once()) - ->method('getCacheMaxAge') - ->willReturn(Cache::PERMANENT); - $entity->expects($this->once()) - ->method('getCacheTags') - ->will($this->returnValue(array('node:1337'))); - $entity->expects($this->once()) - ->method('getCacheContexts') - ->willReturn(array()); - // Set the route requirements. $route = new Route('test_route'); $route->setRequirement('_access_content_translation_manage', 'create'); diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php index 7db0950..6d2f815 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php @@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException; use Drupal\Core\Entity\RevisionableInterface; +use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\Url; use Drupal\Tests\UnitTestCase; @@ -128,6 +129,9 @@ public function providerTestToUrlLinkTemplates() { $test_cases['edit-form'] = ['edit-form', 'entity.test_entity.edit_form']; $test_cases['delete-form'] = ['delete-form', 'entity.test_entity.delete_form']; $test_cases['revision'] = ['revision', 'entity.test_entity.revision']; + $test_cases['drupal:content-translation-add'] = ['drupal:content-translation-add', 'entity.test_entity.content_translation_add']; + $test_cases['drupal:content-translation-edit'] = ['drupal:content-translation-edit', 'entity.test_entity.content_translation_edit']; + $test_cases['drupal:content-translation-delete'] = ['drupal:content-translation-delete', 'entity.test_entity.content_translation_delete']; return $test_cases; } @@ -202,6 +206,72 @@ public function testToUrlLinkTemplateCollection() { } /** + * Tests the toUrl() method with the content translation add link template. + * + * @covers ::toUrl + * @covers ::linkTemplates + * @covers ::urlRouteParameters + */ + public function testToUrlLinkTemplateContentTranslationAdd() { + $values = ['id' => $this->entityId, 'langcode' => $this->langcode]; + $entity = $this->getEntity(TranslatableEntity::class, $values); + $link_template = 'drupal:content-translation-add'; + $this->registerLinkTemplate($link_template); + + /** @var \Drupal\Core\Url $url */ + $url = $entity->toUrl($link_template); + $expected_route_parameters = [ + $this->entityTypeId => $this->entityId, + 'source' => $this->langcode, + 'target' => $this->langcode, + ]; + $this->assertUrl('entity.test_entity.content_translation_add', $expected_route_parameters, $entity, TRUE, $url); + } + + /** + * Tests the toUrl() method with the content translation link templates. + * + * @param string $link_template + * The link template to test. + * @param string $expected_route_name + * The expected route name of the generated URL. + * + * @dataProvider providerTestToUrlLinkTemplateContentTranslation + * + * @covers ::toUrl + * @covers ::linkTemplates + * @covers ::urlRouteParameters + */ + public function testToUrlLinkTemplateContentTranslation($link_template, $expected_route_name) { + $values = ['id' => $this->entityId, 'langcode' => $this->langcode]; + $entity = $this->getEntity(TranslatableEntity::class, $values); + $this->registerLinkTemplate($link_template); + + /** @var \Drupal\Core\Url $url */ + $url = $entity->toUrl($link_template); + $expected_route_parameters = [ + $this->entityTypeId => $this->entityId, + 'language' => $this->langcode, + ]; + $this->assertUrl($expected_route_name, $expected_route_parameters, $entity, TRUE, $url); + } + + /** + * Provides data for testToUrlLinkTemplateContentTranslation(). + * + * @return array + * An array of test cases for testToUrlLinkTemplateContentTranslation(). + */ + public function providerTestToUrlLinkTemplateContentTranslation() { + $test_cases = []; + + $test_cases['drupal:content-translation-edit'] = ['drupal:content-translation-edit', 'entity.test_entity.content_translation_edit']; + $test_cases['drupal:content-translation-delete'] = ['drupal:content-translation-delete', 'entity.test_entity.content_translation_delete']; + + return $test_cases; + } + + /** * Tests the toUrl() method with neither link templates nor a URI callback. * * @param array $bundle_info @@ -500,3 +570,5 @@ protected function registerBundleInfo($bundle_info) { } abstract class RevisionableEntity extends Entity implements RevisionableInterface {} + +abstract class TranslatableEntity extends Entity implements TranslatableInterface {}