diff --git a/pathologic.module b/pathologic.module index 464410f005a439e9d774e16395ccc87e7220ca68..3231d7d59a57a3a499901e9133fc92b3ce0e027c 100644 --- a/pathologic.module +++ b/pathologic.module @@ -26,6 +26,7 @@ declare(strict_types=1); use Drupal\Component\Utility\Html; +use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Url; /** @@ -128,13 +129,14 @@ function _pathologic_replace($matches) { // guess… Note that we don't do the & thing here so that we can modify // $cached_settings later and not have the changes be "permanent." $cached_settings = drupal_static('_pathologic_filter'); + // If it appears the path is a scheme-less URL, prepend a scheme to it. // parse_url() cannot properly parse scheme-less URLs. Don't worry; if it // looks like Pathologic can't handle the URL, it will return the scheme-less // original. // @see https://drupal.org/node/1617944 // @see https://drupal.org/node/2030789 - if (strpos($matches[2], '//') === 0) { + if (str_starts_with($matches[2], '//')) { if (\Drupal::request()->isSecure()) { $matches[2] = 'https:' . $matches[2]; } @@ -180,6 +182,12 @@ function _pathologic_replace($matches) { $parts['path'] = ''; } + // Variable to define whether we need to rewrite/transform the URL through a + // URL object. + $dont_rewrite = FALSE; + // Variable to define whether the given path is a file path. + $is_file = FALSE; + // Check to see if we're dealing with a file. // @todo Should we still try to do path correction on these files too? if (isset($parts['scheme']) && $parts['scheme'] === 'files') { @@ -194,10 +202,14 @@ function _pathologic_replace($matches) { $new_parts['path'] = rawurldecode($new_parts['path']); $parts = $new_parts; // Don't do language handling for file paths. - $cached_settings['is_file'] = TRUE; + $is_file = TRUE; } - else { - $cached_settings['is_file'] = FALSE; + // Check to see if instead of a 'files:' scheme we have a normal internal + // file url starting with the public base path. + elseif (str_contains($parts['path'], PublicStream::basePath())) { + // This url should not be turned into a URL object, because we don't want + // language handling for this path. + $dont_rewrite = TRUE; } // Let's also bail out of this doesn't look like a local path. @@ -291,12 +303,14 @@ function _pathologic_replace($matches) { // If we didn't previously identify this as a file, check to see if the file // exists now that we have the correct path relative to DRUPAL_ROOT - if (!$cached_settings['is_file']) { - $cached_settings['is_file'] = !empty($parts['path']) && is_file(DRUPAL_ROOT . '/' . $parts['path']); + if (!$is_file) { + $is_file = !empty($parts['path']) && is_file(DRUPAL_ROOT . '/' . $parts['path']); } // Okay, deal with language stuff. - // Let's see if we can split off a language prefix from the path. + // Let's see if path has a language prefix, so we can distinguish the target + // language for this path. + $specific_language = NULL; if (\Drupal::moduleHandler()->moduleExists('language')) { // This logic is based on // \Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl::getLangcode(). @@ -310,8 +324,8 @@ function _pathologic_replace($matches) { // Search for prefix within added languages. foreach ($languages as $language) { if (isset($config['prefixes'][$language->getId()]) && $config['prefixes'][$language->getId()] == $prefix) { - $parts['path'] = implode('/', $path_args); - $parts['language_obj'] = $language; + // Do not strip off the language code from the path. + $specific_language = $language; break; } } @@ -335,7 +349,7 @@ function _pathologic_replace($matches) { 'absolute' => $cached_settings['current_settings']['protocol_style'] !== 'path', // If we seem to have found a language for the path, pass it along to // url(). Otherwise, ignore the 'language' parameter. - 'language' => isset($parts['language_obj']) ? $parts['language_obj'] : NULL, + 'language' => $specific_language ?? NULL, // A special parameter not actually used by url(), but we use it to see if // an alter hook implementation wants us to just pass through the original // URL. @@ -360,8 +374,28 @@ function _pathologic_replace($matches) { if ($parts['path'] == '<front>') { $url = Url::fromRoute('<front>', [], $url_params['options'])->toString(); } + elseif ($dont_rewrite) { + // The URL is just the internal path that doesn't need rewriting. + $url = $parts['path']; + if (!str_starts_with($url, '/')) { + $url = '/' . $url; + } + } else { - $path = (empty($url_params['options']['external']) ? 'base://' : '') . $url_params['path']; + // If we've been told this is already an external URL, leave it alone. + if (!empty($url_params['options']['external'])) { + $scheme = ''; + } + // If it's a file, use 'base:' so that the path is not re-written. + elseif ($is_file) { + $scheme = 'base:/'; + } + // For everything we did not recognize as external or files, we use the + // internal scheme so aliases and language prefixes are set correctly. + else { + $scheme = 'internal:/'; + } + $path = $scheme . $url_params['path']; try { $url = Url::fromUri($path, $url_params['options'])->toString(); } diff --git a/tests/src/Functional/PathologicLanguageAliasTest.php b/tests/src/Functional/PathologicLanguageAliasTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6872909ac03900e0766cd7eb6e7ff6550ef50c75 --- /dev/null +++ b/tests/src/Functional/PathologicLanguageAliasTest.php @@ -0,0 +1,152 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\pathologic\Functional; + +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; +use Drupal\Tests\pathologic\Traits\PathologicFormatTrait; +use Drupal\Tests\Traits\Core\PathAliasTestTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Test multilingual integration of Pathologic functionality. + * + * @group pathologic + */ +class PathologicLanguageAliasTest extends BrowserTestBase { + + use ContentTypeCreationTrait; + use NodeCreationTrait; + use PathAliasTestTrait; + use PathologicFormatTrait; + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'content_translation', + 'field', + 'language', + 'locale', + 'node', + 'path', + 'path_alias', + 'pathologic', + 'text', + 'user', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->setUpCurrentUser(); + + $this->createContentType([ + 'type' => 'page', + 'name' => 'Basic page', + ]); + + // Add more languages. + ConfigurableLanguage::createFromLangcode('fr')->save(); + ConfigurableLanguage::createFromLangcode('pt-br')->save(); + + // Enable URL language detection and selection. + \Drupal::configFactory()->getEditable('language.negotiation') + ->set('url.prefixes.fr', 'fr') + ->set('url.prefixes.pt-br', 'pt-br') + ->save(); + + // Configure Pathologic on a text format. + $this->buildFormat([ + 'settings_source' => 'local', + 'local_settings' => [ + 'protocol_style' => 'path', + ], + ]); + + // To reflect the changes for a multilingual site, rebuild the container. + $this->container->get('kernel')->rebuildContainer(); + } + + /** + * Tests how links to nodes and files are handled with translations. + */ + public function testContentTranslation(): void { + + // Create a node that will be referenced in a link inside another node. + $node_to_reference = $this->createNode([ + 'type' => 'page', + 'title' => 'Reference page', + ]); + + // Add translations and path aliases for the reference node, and try a whole + // series of possible input texts to see how they are handled. + $fr_reference = $node_to_reference->addTranslation('fr', [ + 'title' => 'Page de référence en français', + ])->save(); + $pt_br_reference = $node_to_reference->addTranslation('pt-br', [ + 'title' => 'Página de referência em Português', + ])->save(); + + $this->createPathAlias('/node/' . $node_to_reference->id(), '/reference-en', 'en'); + $this->createPathAlias('/node/' . $node_to_reference->id(), '/reference-fr', 'fr'); + $this->createPathAlias('/node/' . $node_to_reference->id(), '/referencia-pt', 'pt-br'); + + global $base_path; + $nid = $node_to_reference->id(); + + // The link replacement shouldn't change for any of these based on the language the filter runs with. + foreach (['en', 'fr', 'pt-br'] as $langcode) { + $this->assertSame( + '<a href="' . $base_path . 'sites/default/files/test.png">Test file link</a>', + $this->runFilter('<a href="/sites/default/files/test.png">Test file link</a>', $langcode), + "$langcode: file links do not get a language prefix", + ); + $this->assertSame( + '<a href="' . $base_path . 'reference-en">Test node link</a>', + $this->runFilter('<a href="/node/' . $nid . '">Test node link</a>', $langcode), + "$langcode: node/N link uses EN alias", + ); + $this->assertSame( + '<a href="' . $base_path . 'fr/reference-fr">Test node link</a>', + $this->runFilter('<a href="/fr/node/' . $nid . '">Test node link</a>', $langcode), + "$langcode: fr/node/N link uses the FR alias", + ); + $this->assertSame( + '<a href="' . $base_path . 'pt-br/referencia-pt">Test node link</a>', + $this->runFilter('<a href="/pt-br/node/' . $nid . '">Test node link</a>', $langcode), + "$langcode: pt-br/node/N link uses the PT-BR alias", + ); + $this->assertSame( + '<a href="' . $base_path . 'reference-en">Test node link</a>', + $this->runFilter('<a href="/reference-en">Test node link</a>', $langcode), + "$langcode: /reference-en link uses EN alias", + ); + $this->assertSame( + '<a href="' . $base_path . 'fr/reference-fr">Test node link</a>', + $this->runFilter('<a href="/fr/reference-fr">Test node link</a>', $langcode), + "$langcode: fr/reference-fr link uses the FR alias", + ); + $this->assertSame( + '<a href="' . $base_path . 'pt-br/referencia-pt">Test node link</a>', + $this->runFilter('<a href="/pt-br/referencia-pt">Test node link</a>', $langcode), + "$langcode: pt-br/referencia-pt uses the PT-BR alias", + ); + } + + } + +} diff --git a/tests/src/Kernel/PathologicKernelTestBase.php b/tests/src/Kernel/PathologicKernelTestBase.php index e5eb77c8c60539496b9316f6ddf4dc432e9d551e..0b11e8a8472299ec7fc3bce922ef2f7fff22c993 100644 --- a/tests/src/Kernel/PathologicKernelTestBase.php +++ b/tests/src/Kernel/PathologicKernelTestBase.php @@ -6,14 +6,16 @@ namespace Drupal\Tests\pathologic\Kernel; use Drupal\Component\Utility\Html; use Drupal\Core\Url; -use Drupal\filter\Entity\FilterFormat; use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\pathologic\Traits\PathologicFormatTrait; /** * Base class for all Pathologic Kernel tests. */ abstract class PathologicKernelTestBase extends KernelTestBase { + use PathologicFormatTrait; + /** * {@inheritdoc} */ @@ -28,13 +30,6 @@ abstract class PathologicKernelTestBase extends KernelTestBase { */ protected $defaultTheme = 'stark'; - /** - * The ID of a text format to be used in a test. - * - * @var formatId - */ - protected $formatId = ''; - /** * {@inheritdoc} */ @@ -44,42 +39,6 @@ abstract class PathologicKernelTestBase extends KernelTestBase { $this->installConfig(['system', 'filter', 'pathologic']); } - /** - * Build a text format with Pathologic configured a certain way. - * - * @param array $settings - * An array of settings for the Pathologic instance on the format. - * - * @return string - * The randomly generated format machine name for the new format. - */ - protected function buildFormat(array $settings) { - $this->formatId = ($settings['local_settings']['protocol_style'] ?? 'unknown') . '_' . $this->randomMachineName(8); - $format = FilterFormat::create([ - 'format' => $this->formatId, - 'name' => $this->formatId, - ]); - $format->setFilterConfig('filter_pathologic', [ - 'status' => 1, - 'settings' => $settings, - ]); - $format->save(); - return $this->formatId; - } - - /** - * Runs the given string through the Pathologic text filter. - * - * @param string $markup - * Raw markup to be processed. - * - * @return string - * A string of text-format-filtered markup. - */ - protected function runFilter(string $markup): string { - return check_markup($markup, $this->formatId)->__toString(); - } - /** * Wrapper around url() which does HTML entity decoding and encoding. * diff --git a/tests/src/Traits/PathologicFormatTrait.php b/tests/src/Traits/PathologicFormatTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..7099865d0150e9614dda1177ed115605344e41ce --- /dev/null +++ b/tests/src/Traits/PathologicFormatTrait.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\pathologic\Traits; + +use Drupal\filter\Entity\FilterFormat; + +/** + * Provides management of a text format configured for Pathologic. + * + * This trait is meant to be used only by test classes. + */ +trait PathologicFormatTrait { + + /** + * The ID of a text format to be used in a test. + * + * @var formatId + */ + protected $formatId = ''; + + /** + * Build a text format with Pathologic configured a certain way. + * + * @param array $settings + * An array of settings for the Pathologic instance on the format. + * + * @return string + * The randomly generated format machine name for the new format. + */ + protected function buildFormat(array $settings) { + $this->formatId = ($settings['local_settings']['protocol_style'] ?? 'unknown') . '_' . $this->randomMachineName(8); + $format = FilterFormat::create([ + 'format' => $this->formatId, + 'name' => $this->formatId, + ]); + $format->setFilterConfig('filter_pathologic', [ + 'status' => 1, + 'settings' => $settings, + ]); + $format->save(); + return $this->formatId; + } + + /** + * Runs the given string through the Pathologic text filter. + * + * @param string $markup + * Raw markup to be processed. + * @param string $langcode + * The optional language to render the text with. + * + * @return string + * A string of text-format-filtered markup. + */ + protected function runFilter(string $markup, string $langcode = ''): string { + return check_markup($markup, $this->formatId, $langcode)->__toString(); + } + +}