Introduce Special:RedirectExternal
authorKosta Harlan <kharlan@wikimedia.org>
Wed, 17 Oct 2018 17:35:57 +0000 (13:35 -0400)
committerRoan Kattouw <roan.kattouw@gmail.com>
Wed, 17 Oct 2018 21:52:09 +0000 (14:52 -0700)
Special:RedirectExternal is an unlisted special page that accepts a URL as
the first argument, and redirects the user to that page.
Example: Special:RedirectExternal/https://mediawiki.org

At the moment, this is intended to be used by the GrowthExperiments project in
order to track outbound visits to certain external links. But it could be
extended in the future to provide parameters for showing a message to the user
before redirecting, or explicitly requiring a user to click on the link, which
could help improve security when users follow on-wiki links to off-wiki sites.

Bug: T207115
Change-Id: I822af14a84569aab22249e2f16a662a60e60f76a

autoload.php
includes/specialpage/SpecialPageFactory.php
includes/specials/SpecialRedirectExternal.php [new file with mode: 0644]
languages/i18n/en.json
languages/i18n/qqq.json
languages/messages/MessagesEn.php
tests/phpunit/includes/specials/SpecialRedirectExternalTest.php [new file with mode: 0644]

index 0f92ccb..22ddaf8 100644 (file)
@@ -1396,6 +1396,7 @@ $wgAutoloadLocalClasses = [
        'SpecialRecentChanges' => __DIR__ . '/includes/specials/SpecialRecentchanges.php',
        'SpecialRecentChangesLinked' => __DIR__ . '/includes/specials/SpecialRecentchangeslinked.php',
        'SpecialRedirect' => __DIR__ . '/includes/specials/SpecialRedirect.php',
+       'SpecialRedirectExternal' => __DIR__ . '/includes/specials/SpecialRedirectExternal.php',
        'SpecialRedirectToSpecial' => __DIR__ . '/includes/specialpage/RedirectSpecialPage.php',
        'SpecialRemoveCredentials' => __DIR__ . '/includes/specials/SpecialRemoveCredentials.php',
        'SpecialResetTokens' => __DIR__ . '/includes/specials/SpecialResetTokens.php',
index 013ceb2..f29d265 100644 (file)
@@ -202,6 +202,7 @@ class SpecialPageFactory {
                'AllMyUploads' => \SpecialAllMyUploads::class,
                'PermanentLink' => \SpecialPermanentLink::class,
                'Redirect' => \SpecialRedirect::class,
+               'RedirectExternal' => \SpecialRedirectExternal::class,
                'Revisiondelete' => \SpecialRevisionDelete::class,
                'RunJobs' => \SpecialRunJobs::class,
                'Specialpages' => \SpecialSpecialpages::class,
diff --git a/includes/specials/SpecialRedirectExternal.php b/includes/specials/SpecialRedirectExternal.php
new file mode 100644 (file)
index 0000000..41a03ed
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * Implements Special:RedirectExternal.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * An unlisted special page that accepts a URL as the first argument, and redirects the user to
+ * that page. Example: Special:Redirect/https://mediawiki.org
+ *
+ * At the moment, this is intended to be used by the GrowthExperiments project in order
+ * to track outbound visits to certain external links. But it could be extended in the future to
+ * provide parameters for showing a message to the user before redirecting, or explicitly requiring
+ * a user to click on the link. This can help improve security when users follow on-wiki links to
+ * off-wiki sites.
+ */
+class SpecialRedirectExternal extends UnlistedSpecialPage {
+
+       public function __construct() {
+               parent::__construct( 'RedirectExternal' );
+       }
+
+       /**
+        * @param string $url
+        * @return bool
+        * @throws HttpError
+        */
+       public function execute( $url = '' ) {
+               $dispatch = $this->dispatch( $url );
+               if ( $dispatch->getStatusValue()->isGood() ) {
+                       $this->getOutput()->redirect( $url );
+                       return true;
+               }
+               throw new HttpError( 400, $dispatch->getMessage() );
+       }
+
+       /**
+        * @param string $url
+        * @return Status
+        */
+       public function dispatch( $url ) {
+               if ( !$url ) {
+                       return Status::newFatal( 'redirectexternal-no-url' );
+               }
+               $url = filter_var( $url, FILTER_SANITIZE_URL );
+               if ( !filter_var( $url, FILTER_VALIDATE_URL ) ) {
+                       return Status::newFatal( 'redirectexternal-invalid-url', $url );
+               }
+               return Status::newGood();
+       }
+}
index ea63054..3ce047c 100644 (file)
        "lag-warn-normal": "Changes newer than $1 {{PLURAL:$1|second|seconds}} may not be shown in this list.",
        "lag-warn-high": "Due to high database server lag, changes newer than $1 {{PLURAL:$1|second|seconds}} may not be shown in this list.",
        "editwatchlist-summary": "",
+       "redirectexternal-summary":  "",
+       "redirectexternal-invalid-url": "$1 is not a valid URL",
+       "redirectexternal-no-url":  "No argument was provided to Special:RedirectExternal",
        "watchlistedit-normal-title": "Edit watchlist",
        "watchlistedit-normal-legend": "Remove titles from watchlist",
        "watchlistedit-normal-explain": "Titles on your watchlist are shown below.\nTo remove a title, check the box next to it, and click \"{{int:Watchlistedit-normal-submit}}\".\nYou can also [[Special:EditWatchlist/raw|edit the raw list]].",
index a17cfca..0c50f40 100644 (file)
        "nimagelinks": "Used on [[Special:MostLinkedFiles]] to indicate how often a specific file is used.\n\nParameters:\n* $1 - number of pages\nSee also:\n* {{msg-mw|Ntransclusions}}",
        "ntransclusions": "Used on [[Special:MostTranscludedPages]] to indicate how often a template is in use.\n\nParameters:\n* $1 - number of pages\nSee also:\n* {{msg-mw|Nimagelinks}}",
        "specialpage-empty": "Used on a special page when there is no data. For example on [[Special:Unusedimages]] when all images are used.",
+       "redirectexternal-summary":  "{{doc-specialpagessummary|redirectexternal}}",
+       "redirectexternal-invalid-url": "Error message shown when the argument to [[Special:RedirectExternal]] is an invalid URL.\n\nParameters:\n* $1 - The first URL argument to Special:RedirectExternal",
+       "redirectexternal-no-url": "Error message shown when no argument is supplied to [[Special:RedirectExternal]]",
        "lonelypages": "{{doc-special|LonelyPages}}",
        "lonelypages-summary": "{{doc-specialpagesummary|lonelypages}}",
        "lonelypagestext": "Text displayed in [[Special:LonelyPages]]",
index 7a7370f..e78f003 100644 (file)
@@ -482,6 +482,7 @@ $specialPageAliases = [
        'Recentchanges'             => [ 'RecentChanges' ],
        'Recentchangeslinked'       => [ 'RecentChangesLinked', 'RelatedChanges' ],
        'Redirect'                  => [ 'Redirect' ],
+       'RedirectExternal'          => [ 'RedirectExternal' ],
        'RemoveCredentials'         => [ 'RemoveCredentials' ],
        'ResetTokens'               => [ 'ResetTokens' ],
        'Revisiondelete'            => [ 'RevisionDelete' ],
diff --git a/tests/phpunit/includes/specials/SpecialRedirectExternalTest.php b/tests/phpunit/includes/specials/SpecialRedirectExternalTest.php
new file mode 100644 (file)
index 0000000..ab5b2cd
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Test class for SpecialRedirectExternal class.
+ *
+ * @license GPL-2.0-or-later
+ */
+class SpecialRedirectExternalTest extends MediaWikiTestCase {
+
+       /**
+        * @dataProvider provideDispatch
+        * @covers SpecialRedirectExternal::dispatch
+        * @covers SpecialRedirectExternal
+        * @param $url
+        * @param $expectedStatus
+        */
+       public function testDispatch( $url, $expectedStatus ) {
+               $page = new SpecialRedirectExternal();
+               $this->assertEquals( $expectedStatus, $page->dispatch( $url )->isGood() );
+       }
+
+       /**
+        * @throws HttpError
+        * @expectedException HttpError
+        * @expectedExceptionMessage asdf is not a valid URL
+        * @covers SpecialRedirectExternal::execute
+        */
+       public function testExecuteInvalidUrl() {
+               $page = new SpecialRedirectExternal();
+               $page->execute( 'asdf' );
+       }
+
+       /**
+        * @throws HttpError
+        * @covers SpecialRedirectExternal::execute
+        */
+       public function testValidUrl() {
+               $page = new SpecialRedirectExternal();
+               $this->assertTrue( $page->execute( 'https://www.mediawiki.org' ) );
+       }
+
+       public static function provideDispatch() {
+               return [
+                       [ 'asdf', false ],
+                       [ null, false ],
+                       [ 'https://www.mediawiki.org?test=1', true ],
+               ];
+       }
+}