'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',
'AllMyUploads' => \SpecialAllMyUploads::class,
'PermanentLink' => \SpecialPermanentLink::class,
'Redirect' => \SpecialRedirect::class,
+ 'RedirectExternal' => \SpecialRedirectExternal::class,
'Revisiondelete' => \SpecialRevisionDelete::class,
'RunJobs' => \SpecialRunJobs::class,
'Specialpages' => \SpecialSpecialpages::class,
--- /dev/null
+<?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();
+ }
+}
"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]].",
"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]]",
'Recentchanges' => [ 'RecentChanges' ],
'Recentchangeslinked' => [ 'RecentChangesLinked', 'RelatedChanges' ],
'Redirect' => [ 'Redirect' ],
+ 'RedirectExternal' => [ 'RedirectExternal' ],
'RemoveCredentials' => [ 'RemoveCredentials' ],
'ResetTokens' => [ 'ResetTokens' ],
'Revisiondelete' => [ 'RevisionDelete' ],
--- /dev/null
+<?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 ],
+ ];
+ }
+}