From 74db053104cd07bdebcaedeb5ee6165814fe811e Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Fri, 8 May 2015 21:53:09 +0100
Subject: [PATCH] Issue #2428103 by dawehner, amateescu, Berdir, AjitS: String
 formatter should link to its translation

---
 core/lib/Drupal/Core/Entity/Entity.php        | 22 +++--
 .../src/Tests/EntityUrlLanguageTest.php       | 69 +++++++++++++
 .../src/Tests/StatisticsReportsTest.php       |  4 +-
 .../taxonomy/src/Tests/TermIndexTest.php      |  4 +-
 .../src/Tests/Handler/FieldEntityLinkTest.php |  4 +-
 .../src/Tests/Handler/FieldFieldTest.php      |  3 +-
 .../Tests/Core/Entity/EntityLinkTest.php      | 25 ++++-
 .../Tests/Core/Entity/EntityUrlTest.php       | 97 +++++++++++++------
 8 files changed, 186 insertions(+), 42 deletions(-)
 create mode 100644 core/modules/language/src/Tests/EntityUrlLanguageTest.php

diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 1b384776a9fa..a5c3d0f0a46a 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -204,8 +204,15 @@ public function urlInfo($rel = 'canonical', array $options = []) {
     $uri
       ->setOption('entity_type', $this->getEntityTypeId())
       ->setOption('entity', $this);
+
+    // Display links by default based on the current language.
+    if ($rel !== 'collection') {
+      $options += ['language' => $this->language()];
+    }
+
     $uri_options = $uri->getOptions();
     $uri_options += $options;
+
     return $uri->setOptions($uri_options);
   }
 
@@ -323,13 +330,16 @@ public function access($operation, AccountInterface $account = NULL, $return_as_
    * {@inheritdoc}
    */
   public function language() {
-    $langcode = $this->{$this->getEntityType()->getKey('langcode')};
-    $language = $this->languageManager()->getLanguage($langcode);
-    if (!$language) {
-      // Make sure we return a proper language object.
-      $langcode = $this->langcode ?: LanguageInterface::LANGCODE_NOT_SPECIFIED;
-      $language = new Language(array('id' => $langcode));
+    if ($key = $this->getEntityType()->getKey('langcode')) {
+      $langcode = $this->$key;
+      $language = $this->languageManager()->getLanguage($langcode);
+      if ($language) {
+        return $language;
+      }
     }
+    // Make sure we return a proper language object.
+    $langcode = !empty($this->langcode) ? $this->langcode : LanguageInterface::LANGCODE_NOT_SPECIFIED;
+    $language = new Language(array('id' => $langcode));
     return $language;
   }
 
diff --git a/core/modules/language/src/Tests/EntityUrlLanguageTest.php b/core/modules/language/src/Tests/EntityUrlLanguageTest.php
new file mode 100644
index 000000000000..915f7f605ed3
--- /dev/null
+++ b/core/modules/language/src/Tests/EntityUrlLanguageTest.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\language\Tests\EntityUrlLanguageTest.
+ */
+
+namespace Drupal\language\Tests;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Tests the language of entity URLs.
+ * @group language
+ */
+class EntityUrlLanguageTest extends KernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['language', 'entity_test', 'user', 'system'];
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('entity_test');
+    $this->installEntitySchema('configurable_language');
+    $this->installSchema('system', 'router');
+    \Drupal::service('router.builder')->rebuild();
+
+    // In order to reflect the changes for a multilingual site in the container
+    // we have to rebuild it.
+    ConfigurableLanguage::create(['id' => 'es'])->save();
+    ConfigurableLanguage::create(['id' => 'fr'])->save();
+
+    $this->config('language.types')->setData([
+      'configurable' => ['language_interface'],
+      'negotiation' => ['language_interface' => ['enabled' => ['language-url' => 0]]],
+    ])->save();
+    $this->config('language.negotiation')->setData([
+      'url' => [
+        'source' => 'path_prefix',
+        'prefixes' => ['en' => 'en', 'es' => 'es', 'fr' => 'fr']
+      ],
+    ])->save();
+    $this->kernel->rebuildContainer();
+    $this->container = $this->kernel->getContainer();
+    \Drupal::setContainer($this->container);
+  }
+
+  /**
+   * Ensures that entity URLs in a language have the right language prefix.
+   */
+  public function testEntityUrlLanguage() {
+    $entity = EntityTest::create();
+    $entity->addTranslation('es', ['name' => 'name spanish']);
+    $entity->addTranslation('fr', ['name' => 'name french']);
+    $entity->save();
+
+    $this->assertTrue(strpos($entity->urlInfo()->toString(), '/en/entity_test/' . $entity->id()) !== FALSE);
+    $this->assertTrue(strpos($entity->getTranslation('es')->urlInfo()->toString(), '/es/entity_test/' . $entity->id()) !== FALSE);
+    $this->assertTrue(strpos($entity->getTranslation('fr')->urlInfo()->toString(), '/fr/entity_test/' . $entity->id()) !== FALSE);
+  }
+
+}
diff --git a/core/modules/statistics/src/Tests/StatisticsReportsTest.php b/core/modules/statistics/src/Tests/StatisticsReportsTest.php
index 3ec956a06b19..7f2937d08f7e 100644
--- a/core/modules/statistics/src/Tests/StatisticsReportsTest.php
+++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php
@@ -49,7 +49,9 @@ function testPopularContentBlock() {
     $this->assertText('All time', 'Found the all time popular content.');
     $this->assertText('Last viewed', 'Found the last viewed popular content.');
 
-    $this->assertRaw(\Drupal::l($node->label(), $node->urlInfo()), 'Found link to visited node.');
+    // statistics.module doesn't use node entities, prevent the node language
+    // from being added to the options.
+    $this->assertRaw(\Drupal::l($node->label(), $node->urlInfo('canonical', ['language' => NULL])), 'Found link to visited node.');
   }
 
 }
diff --git a/core/modules/taxonomy/src/Tests/TermIndexTest.php b/core/modules/taxonomy/src/Tests/TermIndexTest.php
index 43f97d57d9dc..aecb90a64c46 100644
--- a/core/modules/taxonomy/src/Tests/TermIndexTest.php
+++ b/core/modules/taxonomy/src/Tests/TermIndexTest.php
@@ -211,6 +211,8 @@ function testTaxonomyTermHierarchyBreadcrumbs() {
 
     // Verify that the page breadcrumbs include a link to the parent term.
     $this->drupalGet('taxonomy/term/' . $term1->id());
-    $this->assertRaw(\Drupal::l($term2->getName(), $term2->urlInfo()), 'Parent term link is displayed when viewing the node.');
+    // Breadcrumbs are not rendered with a language, prevent the term
+    // language from being added to the options.
+    $this->assertRaw(\Drupal::l($term2->getName(), $term2->urlInfo('canonical', ['language' => NULL])), 'Parent term link is displayed when viewing the node.');
   }
 }
diff --git a/core/modules/views/src/Tests/Handler/FieldEntityLinkTest.php b/core/modules/views/src/Tests/Handler/FieldEntityLinkTest.php
index d00b3afd09f4..08c6ae7567c1 100644
--- a/core/modules/views/src/Tests/Handler/FieldEntityLinkTest.php
+++ b/core/modules/views/src/Tests/Handler/FieldEntityLinkTest.php
@@ -122,10 +122,10 @@ protected function doTestEntityLink(AccountInterface $account, $expected_results
         if ($expected_result) {
           $path = $entity->url($template);
           $destination = $info[$template]['destination'] ? '?destination=/' : '';
-          $expected_link = '<a href="' . $path . $destination . '">' . $info[$template]['label'] . '</a>';
+          $expected_link = '<a href="' . $path . $destination . '" hreflang="en">' . $info[$template]['label'] . '</a>';
         }
         $link = $view->style_plugin->getField($index, $info[$template]['field_id']);
-        $this->assertEqual($link, $expected_link, SafeMarkup::format('@template entity link behaves as expected.', ['@template' => $template]));
+        $this->assertEqual($link, $expected_link);
       }
       $index++;
     }
diff --git a/core/modules/views/src/Tests/Handler/FieldFieldTest.php b/core/modules/views/src/Tests/Handler/FieldFieldTest.php
index 942379b9b4e7..14b114c8dd0c 100644
--- a/core/modules/views/src/Tests/Handler/FieldFieldTest.php
+++ b/core/modules/views/src/Tests/Handler/FieldFieldTest.php
@@ -302,7 +302,8 @@ public function testFieldAliasRender() {
     for ($i = 0; $i < 5; $i++) {
       $this->assertEqual($i + 1, $executable->getStyle()->getField($i, 'id'));
       $this->assertEqual('test ' . $i, $executable->getStyle()->getField($i, 'name'));
-      $this->assertEqual('<a href="' . EntityTest::load($i + 1)->url() . '">test ' . $i . '</a>', $executable->getStyle()->getField($i, 'name_alias'));
+      $entity = EntityTest::load($i + 1);
+      $this->assertEqual('<a href="' . $entity->url() . '" hreflang="' . $entity->language()->getId() . '">test ' . $i . '</a>', $executable->getStyle()->getField($i, 'name_alias'));
     }
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityLinkTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityLinkTest.php
index 361a7ea45681..9e54ebedb1ca 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityLinkTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityLinkTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\Entity;
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Language\Language;
 use Drupal\Core\Link;
 use Drupal\Tests\UnitTestCase;
 
@@ -31,6 +32,13 @@ class EntityLinkTest extends UnitTestCase {
    */
   protected $linkGenerator;
 
+  /**
+   * The mocked language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $languageManager;
+
   /**
    * {@inheritdoc}
    */
@@ -39,10 +47,12 @@ protected function setUp() {
 
     $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
     $this->linkGenerator = $this->getMock('Drupal\Core\Utility\LinkGeneratorInterface');
+    $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
 
     $container = new ContainerBuilder();
     $container->set('entity.manager', $this->entityManager);
     $container->set('link_generator', $this->linkGenerator);
+    $container->set('language_manager', $this->languageManager);
     \Drupal::setContainer($container);
   }
 
@@ -52,6 +62,13 @@ protected function setUp() {
    * @dataProvider providerTestLink
    */
   public function testLink($entity_label, $link_text, $expected_text, $link_rel = 'canonical', array $link_options = []) {
+    $language = new Language(['id' => 'es']);
+    $link_options += ['language' => $language];
+    $this->languageManager->expects($this->any())
+      ->method('getLanguage')
+      ->with('es')
+      ->willReturn($language);
+
     $route_name_map = [
       'canonical' => 'entity.test_entity_type.canonical',
       'edit-form' => 'entity.test_entity_type.edit_form',
@@ -67,8 +84,10 @@ public function testLink($entity_label, $link_text, $expected_text, $link_rel =
       ->willReturn($route_name_map);
     $entity_type->expects($this->any())
       ->method('getKey')
-      ->with('label')
-      ->willReturn('label');
+      ->willReturnMap([
+        ['label', 'label'],
+        ['langcode', 'langcode'],
+      ]);
 
     $this->entityManager
       ->expects($this->any())
@@ -78,7 +97,7 @@ public function testLink($entity_label, $link_text, $expected_text, $link_rel =
 
     /** @var \Drupal\Core\Entity\Entity $entity */
     $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', [
-      ['id' => $entity_id, 'label' => $entity_label],
+      ['id' => $entity_id, 'label' => $entity_label, 'langcode' => 'es'],
       $entity_type_id
     ]);
 
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
index 390eaf7d3af4..9cab4fa6f2b2 100644
--- a/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityUrlTest.php
@@ -7,8 +7,11 @@
 
 namespace Drupal\Tests\Core\Entity;
 
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Language\Language;
+use Drupal\Core\Language\LanguageInterface;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -51,13 +54,39 @@ protected function setUp() {
    *
    * @dataProvider providerTestUrlInfo
    */
-  public function testUrlInfo($entity_class, $link_template, $expected) {
+  public function testUrlInfo($entity_class, $link_template, $expected, $langcode = NULL) {
     /** @var $entity \Drupal\Core\Entity\EntityInterface */
     $entity = $this->getMockForAbstractClass($entity_class, array(array('id' => 'test_entity_id'), 'test_entity_type'));
-    $uri = $this->getTestUrlInfo($entity, $link_template);
+    $uri = $this->getTestUrlInfo($entity, $link_template, [], $langcode);
 
     $this->assertSame($expected, $uri->getRouteName());
     $this->assertSame($entity, $uri->getOption('entity'));
+
+    if ($langcode) {
+      $this->assertEquals($langcode, $uri->getOption('language')->getId());
+    }
+    else {
+      // The expected langcode for a config entity is 'en', because it sets the
+      // value as default property.
+      $expected_langcode = $entity instanceof ConfigEntityInterface ? 'en' : LanguageInterface::LANGCODE_NOT_SPECIFIED;
+      $this->assertEquals($expected_langcode, $uri->getOption('language')->getId());
+    }
+  }
+
+  /**
+   * @covers ::urlInfo
+   */
+  public function testUrlInfoWithSpecificLanguageInOptions() {
+    /** @var $entity \Drupal\Core\Entity\EntityInterface */
+    $entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array('id' => 'test_entity_id'), 'test_entity_type'));
+
+    // Ensure that a specified language overrides the current translation
+    // language.
+    $uri = $this->getTestUrlInfo($entity, 'edit-form', [], 'en');
+    $this->assertEquals('en', $uri->getOption('language')->getId());
+
+    $uri = $this->getTestUrlInfo($entity, 'edit-form', ['language' => new Language(['id' => 'fr'])], 'en');
+    $this->assertEquals('fr', $uri->getOption('language')->getId());
   }
 
   /**
@@ -65,10 +94,13 @@ public function testUrlInfo($entity_class, $link_template, $expected) {
    */
   public function providerTestUrlInfo() {
     return array(
-      array('Drupal\Core\Entity\Entity', 'edit-form', 'entity.test_entity_type.edit_form'),
-      array('Drupal\Core\Config\Entity\ConfigEntityBase', 'edit-form', 'entity.test_entity_type.edit_form'),
+      array('Drupal\Core\Entity\Entity', 'edit-form', 'entity.test_entity_type.edit_form', NULL),
+      // Specify a langcode.
+      array('Drupal\Core\Entity\Entity', 'edit-form', 'entity.test_entity_type.edit_form', 'es'),
+      array('Drupal\Core\Entity\Entity', 'edit-form', 'entity.test_entity_type.edit_form', 'en'),
+      array('Drupal\Core\Config\Entity\ConfigEntityBase', 'edit-form', 'entity.test_entity_type.edit_form', NULL),
       // Test that overriding the default $rel parameter works.
-      array('Drupal\Core\Config\Entity\ConfigEntityBase', FALSE, 'entity.test_entity_type.edit_form'),
+      array('Drupal\Core\Config\Entity\ConfigEntityBase', FALSE, 'entity.test_entity_type.edit_form', NULL),
     );
   }
 
@@ -108,18 +140,24 @@ public function providerTestUrlInfoForInvalidLinkTemplate() {
    *   The test entity.
    * @param string $link_template
    *   The link template.
+   * @param string $langcode
+   *   The langcode.
    *
    * @return \Drupal\Core\Url
    *   The URL for this entity's link template.
    */
-  protected function getTestUrlInfo(EntityInterface $entity, $link_template) {
+  protected function getTestUrlInfo(EntityInterface $entity, $link_template, array $options = [], $langcode = NULL) {
     $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
-    $entity_type->expects($this->once())
+    $entity_type->expects($this->any())
       ->method('getLinkTemplates')
       ->will($this->returnValue(array(
         'edit-form' => 'test_entity_type.edit',
       )));
 
+    if ($langcode) {
+      $entity->langcode = $langcode;
+    }
+
     $this->entityManager
       ->expects($this->any())
       ->method('getDefinition')
@@ -128,10 +166,15 @@ protected function getTestUrlInfo(EntityInterface $entity, $link_template) {
 
     // If no link template is given, call without a value to test the default.
     if ($link_template) {
-      $uri = $entity->urlInfo($link_template);
+      $uri = $entity->urlInfo($link_template, $options);
     }
     else {
-      $uri = $entity->urlInfo();
+      if ($entity instanceof ConfigEntityInterface) {
+        $uri = $entity->urlInfo('edit-form', $options);
+      }
+      else {
+        $uri = $entity->urlInfo('canonical', $options);
+      }
     }
 
     return $uri;
@@ -158,14 +201,14 @@ public function testUrlInfoForNewEntity() {
    */
   public function testUrl() {
     $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
-    $entity_type->expects($this->exactly(5))
+    $entity_type->expects($this->any())
       ->method('getLinkTemplates')
       ->will($this->returnValue(array(
         'canonical' => 'test_entity_type.view',
       )));
 
     $this->entityManager
-      ->expects($this->exactly(5))
+      ->expects($this->any())
       ->method('getDefinition')
       ->with('test_entity_type')
       ->will($this->returnValue($entity_type));
@@ -177,22 +220,20 @@ public function testUrl() {
     $this->assertSame('', $no_link_entity->url('banana'));
 
     $valid_entity = $this->getMockForAbstractClass('Drupal\Core\Entity\Entity', array(array('id' => 'test_entity_id'), 'test_entity_type'));
-    $this->urlGenerator->expects($this->exactly(2))
+
+    $language = new Language(array('id' => LanguageInterface::LANGCODE_NOT_SPECIFIED));
+    $this->urlGenerator->expects($this->any())
       ->method('generateFromRoute')
-      ->will($this->returnValueMap(array(
-        array(
-          'entity.test_entity_type.canonical',
-          array('test_entity_type' => 'test_entity_id'),
-          array('entity_type' => 'test_entity_type', 'entity' => $valid_entity),
-          '/entity/test_entity_type/test_entity_id',
-        ),
-        array(
-          'entity.test_entity_type.canonical',
-          array('test_entity_type' => 'test_entity_id'),
-          array('absolute' => TRUE, 'entity_type' => 'test_entity_type', 'entity' => $valid_entity),
-          'http://drupal/entity/test_entity_type/test_entity_id',
-        ),
-      )));
+      // Sadly returnValueMap() uses ===, see \PHPUnit_Framework_MockObject_Stub_ReturnValueMap::invoke
+      // so the $language object can't be compared directly.
+      ->willReturnCallback(function ($route_name, $route_parameters, $options) use ($language) {
+        if ($route_name === 'entity.test_entity_type.canonical' && $route_parameters === array('test_entity_type' => 'test_entity_id') && array_keys($options) === ['entity_type', 'entity', 'language'] && $options['language'] == $language) {
+          return '/entity/test_entity_type/test_entity_id';
+        }
+        if ($route_name === 'entity.test_entity_type.canonical' && $route_parameters === array('test_entity_type' => 'test_entity_id') && array_keys($options) === ['absolute', 'entity_type', 'entity', 'language'] && $options['language'] == $language) {
+          return 'http://drupal/entity/test_entity_type/test_entity_id';
+        }
+    });
 
     $this->assertSame('/entity/test_entity_type/test_entity_id', $valid_entity->url());
     $this->assertSame('http://drupal/entity/test_entity_type/test_entity_id', $valid_entity->url('canonical', array('absolute' => TRUE)));
@@ -205,14 +246,14 @@ public function testUrl() {
    */
   public function testGetSystemPath() {
     $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
-    $entity_type->expects($this->exactly(3))
+    $entity_type->expects($this->any())
       ->method('getLinkTemplates')
       ->will($this->returnValue(array(
         'canonical' => 'entity.test_entity_type.canonical',
       )));
 
     $this->entityManager
-      ->expects($this->exactly(3))
+      ->expects($this->any())
       ->method('getDefinition')
       ->with('test_entity_type')
       ->will($this->returnValue($entity_type));
-- 
GitLab