Add interwiki support to LinkTarget and TitleValue
authorKunal Mehta <legoktm@member.fsf.org>
Wed, 27 Apr 2016 02:21:59 +0000 (19:21 -0700)
committerKunal Mehta <legoktm@member.fsf.org>
Wed, 27 Apr 2016 05:25:28 +0000 (22:25 -0700)
This adds support to the LinkTarget interface and TitleValue
implementation for having an interwiki component, matching the function
names used in Title.

MediaWikiTitleCodec was updated accordingly.

The motivation behind this change is to be able to fully use LinkTarget
in the Linker rewrite instead of depending upon Title.

Change-Id: I6666b64f0e336aadc7261e7ca87ac2e498c61856

includes/linker/LinkTarget.php
includes/title/MediaWikiTitleCodec.php
includes/title/TitleFormatter.php
includes/title/TitleValue.php
tests/phpunit/includes/title/MediaWikiTitleCodecTest.php
tests/phpunit/includes/title/TitleValueTest.php

index 2764f46..7b59751 100644 (file)
@@ -73,4 +73,18 @@ interface LinkTarget {
         * @return LinkTarget
         */
        public function createFragmentTarget( $fragment );
+
+       /**
+        * Whether this LinkTarget has an interwiki component
+        *
+        * @return bool
+        */
+       public function isExternal();
+
+       /**
+        * The interwiki component of this LinkTarget
+        *
+        * @return string
+        */
+       public function getInterwiki();
 }
index 7c08be4..0e291ed 100644 (file)
@@ -98,11 +98,12 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
         * @param string $text The page title. Should be valid. Only minimal normalization is applied.
         *        Underscores will be replaced.
         * @param string $fragment The fragment name (may be empty).
+        * @param string $interwiki The interwiki name (may be empty).
         *
         * @throws InvalidArgumentException If the namespace is invalid
         * @return string
         */
-       public function formatTitle( $namespace, $text, $fragment = '' ) {
+       public function formatTitle( $namespace, $text, $fragment = '', $interwiki = '' ) {
                if ( $namespace !== false ) {
                        $namespace = $this->getNamespaceName( $namespace, $text );
 
@@ -115,6 +116,10 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
                        $text = $text . '#' . $fragment;
                }
 
+               if ( $interwiki !== '' ) {
+                       $text = $interwiki . ':' . $text;
+               }
+
                $text = str_replace( '_', ' ', $text );
 
                return $text;
@@ -136,17 +141,17 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
                // be refactored to avoid this.
                $parts = $this->splitTitleString( $text, $defaultNamespace );
 
-               // Interwiki links are not supported by TitleValue
-               if ( $parts['interwiki'] !== '' ) {
-                       throw new MalformedTitleException( 'title-invalid-interwiki', $text );
-               }
-
                // Relative fragment links are not supported by TitleValue
                if ( $parts['dbkey'] === '' ) {
                        throw new MalformedTitleException( 'title-invalid-empty', $text );
                }
 
-               return new TitleValue( $parts['namespace'], $parts['dbkey'], $parts['fragment'] );
+               return new TitleValue(
+                       $parts['namespace'],
+                       $parts['dbkey'],
+                       $parts['fragment'],
+                       $parts['interwiki']
+               );
        }
 
        /**
@@ -168,7 +173,12 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
         * @return string
         */
        public function getPrefixedText( LinkTarget $title ) {
-               return $this->formatTitle( $title->getNamespace(), $title->getText(), '' );
+               return $this->formatTitle(
+                       $title->getNamespace(),
+                       $title->getText(),
+                       '',
+                       $title->getInterwiki()
+               );
        }
 
        /**
@@ -179,7 +189,12 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser {
         * @return string
         */
        public function getFullText( LinkTarget $title ) {
-               return $this->formatTitle( $title->getNamespace(), $title->getText(), $title->getFragment() );
+               return $this->formatTitle(
+                       $title->getNamespace(),
+                       $title->getText(),
+                       $title->getFragment(),
+                       $title->getInterwiki()
+               );
        }
 
        /**
index 96f396c..c081129 100644 (file)
@@ -42,10 +42,11 @@ interface TitleFormatter {
         * @param int|bool $namespace The namespace ID (or false, if the namespace should be ignored)
         * @param string $text The page title
         * @param string $fragment The fragment name (may be empty).
+        * @param string $interwiki The interwiki prefix (may be empty).
         *
         * @return string
         */
-       public function formatTitle( $namespace, $text, $fragment = '' );
+       public function formatTitle( $namespace, $text, $fragment = '', $interwiki = '' );
 
        /**
         * Returns the title text formatted for display, without namespace of fragment.
index c23d698..63c075f 100644 (file)
@@ -30,9 +30,6 @@ use Wikimedia\Assert\Assert;
  * @note In contrast to Title, this is designed to be a plain value object. That is,
  * it is immutable, does not use global state, and causes no side effects.
  *
- * @note TitleValue represents the title of a local page (or fragment of a page).
- * It does not represent a link, and does not support interwiki prefixes etc.
- *
  * @see https://www.mediawiki.org/wiki/Requests_for_comment/TitleValue
  * @since 1.23
  */
@@ -52,6 +49,11 @@ class TitleValue implements LinkTarget {
         */
        protected $fragment;
 
+       /**
+        * @var string
+        */
+       protected $interwiki;
+
        /**
         * Constructs a TitleValue.
         *
@@ -65,13 +67,15 @@ class TitleValue implements LinkTarget {
         * @param string $dbkey The page title in valid DBkey form. No normalization is applied.
         * @param string $fragment The fragment title. Use '' to represent the whole page.
         *   No validation or normalization is applied.
+        * @param string $interwiki The interwiki component
         *
         * @throws InvalidArgumentException
         */
-       public function __construct( $namespace, $dbkey, $fragment = '' ) {
+       public function __construct( $namespace, $dbkey, $fragment = '', $interwiki = '' ) {
                Assert::parameterType( 'integer', $namespace, '$namespace' );
                Assert::parameterType( 'string', $dbkey, '$dbkey' );
                Assert::parameterType( 'string', $fragment, '$fragment' );
+               Assert::parameterType( 'string', $interwiki, '$interwiki' );
 
                // Sanity check, no full validation or normalization applied here!
                Assert::parameter( !preg_match( '/^_|[ \r\n\t]|_$/', $dbkey ), '$dbkey', 'invalid DB key' );
@@ -80,6 +84,7 @@ class TitleValue implements LinkTarget {
                $this->namespace = $namespace;
                $this->dbkey = $dbkey;
                $this->fragment = $fragment;
+               $this->interwiki = $interwiki;
        }
 
        /**
@@ -138,7 +143,32 @@ class TitleValue implements LinkTarget {
         * @return TitleValue
         */
        public function createFragmentTarget( $fragment ) {
-               return new TitleValue( $this->namespace, $this->dbkey, $fragment );
+               return new TitleValue(
+                       $this->namespace,
+                       $this->dbkey,
+                       $fragment,
+                       $this->interwiki
+               );
+       }
+
+       /**
+        * Whether it has an interwiki part
+        *
+        * @since 1.27
+        * @return bool
+        */
+       public function isExternal() {
+               return $this->interwiki !== '';
+       }
+
+       /**
+        * Returns the interwiki part
+        *
+        * @since 1.27
+        * @return string
+        */
+       public function getInterwiki() {
+               return $this->interwiki;
        }
 
        /**
@@ -155,6 +185,10 @@ class TitleValue implements LinkTarget {
                        $name .= '#' . $this->fragment;
                }
 
+               if ( $this->interwiki !== '' ) {
+                       $name = $this->interwiki . ':' . $name;
+               }
+
                return $name;
        }
 }
index 0bb0afe..e321bdb 100644 (file)
@@ -87,13 +87,14 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
 
        public static function provideFormat() {
                return [
-                       [ NS_MAIN, 'Foo_Bar', '', 'en', 'Foo Bar' ],
-                       [ NS_USER, 'Hansi_Maier', 'stuff_and_so_on', 'en', 'User:Hansi Maier#stuff and so on' ],
-                       [ false, 'Hansi_Maier', '', 'en', 'Hansi Maier' ],
+                       [ NS_MAIN, 'Foo_Bar', '', '', 'en', 'Foo Bar' ],
+                       [ NS_USER, 'Hansi_Maier', 'stuff_and_so_on', '', 'en', 'User:Hansi Maier#stuff and so on' ],
+                       [ false, 'Hansi_Maier', '', '', 'en', 'Hansi Maier' ],
                        [
                                NS_USER_TALK,
                                'hansi__maier',
                                '',
+                               '',
                                'en',
                                'User talk:hansi  maier',
                                'User talk:Hansi maier'
@@ -101,20 +102,23 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
 
                        // getGenderCache() provides a mock that considers first
                        // names ending in "a" to be female.
-                       [ NS_USER, 'Lisa_Müller', '', 'de', 'Benutzerin:Lisa Müller' ],
+                       [ NS_USER, 'Lisa_Müller', '', '', 'de', 'Benutzerin:Lisa Müller' ],
+                       [ NS_MAIN, 'FooBar', '', 'remotetestiw', 'en', 'remotetestiw:FooBar' ],
                ];
        }
 
        /**
         * @dataProvider provideFormat
         */
-       public function testFormat( $namespace, $text, $fragment, $lang, $expected, $normalized = null ) {
+       public function testFormat( $namespace, $text, $fragment, $interwiki, $lang, $expected,
+               $normalized = null
+       ) {
                if ( $normalized === null ) {
                        $normalized = $expected;
                }
 
                $codec = $this->makeCodec( $lang );
-               $actual = $codec->formatTitle( $namespace, $text, $fragment );
+               $actual = $codec->formatTitle( $namespace, $text, $fragment, $interwiki );
 
                $this->assertEquals( $expected, $actual, 'formatted' );
 
@@ -123,7 +127,8 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
                $actual2 = $codec->formatTitle(
                        $parsed->getNamespace(),
                        $parsed->getText(),
-                       $parsed->getFragment()
+                       $parsed->getFragment(),
+                       $parsed->getInterwiki()
                );
 
                $this->assertEquals( $normalized, $actual2, 'normalized after round trip' );
@@ -293,7 +298,6 @@ class MediaWikiTitleCodecTest extends MediaWikiTestCase {
 
                        [ 'Talk:File:Foo.jpg' ],
                        [ 'Talk:localtestiw:Foo' ],
-                       [ 'remotetestiw:Foo' ],
                        [ '::1' ], // only valid in user namespace
                        [ 'User::x' ], // leading ":" in a user name is only valid of IPv6 addresses
 
index 913253b..7922553 100644 (file)
@@ -28,49 +28,57 @@ class TitleValueTest extends MediaWikiTestCase {
 
        public function goodConstructorProvider() {
                return [
-                       [ NS_USER, 'TestThis', 'stuff', true ],
-                       [ NS_USER, 'TestThis', '', false ],
+                       [ NS_USER, 'TestThis', 'stuff', '', true, false ],
+                       [ NS_USER, 'TestThis', '', 'baz', false, true ],
                ];
        }
 
        /**
         * @dataProvider goodConstructorProvider
         */
-       public function testConstruction( $ns, $text, $fragment, $hasFragment ) {
-               $title = new TitleValue( $ns, $text, $fragment );
+       public function testConstruction( $ns, $text, $fragment, $interwiki, $hasFragment,
+               $hasInterwiki
+       ) {
+               $title = new TitleValue( $ns, $text, $fragment, $interwiki );
 
                $this->assertEquals( $ns, $title->getNamespace() );
                $this->assertEquals( $text, $title->getText() );
                $this->assertEquals( $fragment, $title->getFragment() );
                $this->assertEquals( $hasFragment, $title->hasFragment() );
+               $this->assertEquals( $interwiki, $title->getInterwiki() );
+               $this->assertEquals( $hasInterwiki, $title->isExternal() );
        }
 
        public function badConstructorProvider() {
                return [
-                       [ 'foo', 'title', 'fragment' ],
-                       [ null, 'title', 'fragment' ],
-                       [ 2.3, 'title', 'fragment' ],
+                       [ 'foo', 'title', 'fragment', '' ],
+                       [ null, 'title', 'fragment', '' ],
+                       [ 2.3, 'title', 'fragment', '' ],
 
-                       [ NS_MAIN, 5, 'fragment' ],
-                       [ NS_MAIN, null, 'fragment' ],
-                       [ NS_MAIN, '', 'fragment' ],
-                       [ NS_MAIN, 'foo bar', '' ],
-                       [ NS_MAIN, 'bar_', '' ],
-                       [ NS_MAIN, '_foo', '' ],
-                       [ NS_MAIN, ' eek ', '' ],
+                       [ NS_MAIN, 5, 'fragment', '' ],
+                       [ NS_MAIN, null, 'fragment', '' ],
+                       [ NS_MAIN, '', 'fragment', '' ],
+                       [ NS_MAIN, 'foo bar', '', '' ],
+                       [ NS_MAIN, 'bar_', '', '' ],
+                       [ NS_MAIN, '_foo', '', '' ],
+                       [ NS_MAIN, ' eek ', '', '' ],
 
-                       [ NS_MAIN, 'title', 5 ],
-                       [ NS_MAIN, 'title', null ],
-                       [ NS_MAIN, 'title', [] ],
+                       [ NS_MAIN, 'title', 5, '' ],
+                       [ NS_MAIN, 'title', null, '' ],
+                       [ NS_MAIN, 'title', [], '' ],
+
+                       [ NS_MAIN, 'title', '', 5 ],
+                       [ NS_MAIN, 'title', null, 5 ],
+                       [ NS_MAIN, 'title', [], 5 ],
                ];
        }
 
        /**
         * @dataProvider badConstructorProvider
         */
-       public function testConstructionErrors( $ns, $text, $fragment ) {
+       public function testConstructionErrors( $ns, $text, $fragment, $interwiki ) {
                $this->setExpectedException( 'InvalidArgumentException' );
-               new TitleValue( $ns, $text, $fragment );
+               new TitleValue( $ns, $text, $fragment, $interwiki );
        }
 
        public function fragmentTitleProvider() {