diff options
Diffstat (limited to 'includes/specials')
77 files changed, 2377 insertions, 1295 deletions
diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php index f983b452..5e2ee1c2 100644 --- a/includes/specials/SpecialActiveusers.php +++ b/includes/specials/SpecialActiveusers.php @@ -130,6 +130,8 @@ class ActiveUsersPager extends UsersPager { } function doBatchLookups() { + parent::doBatchLookups(); + $uids = array(); foreach ( $this->mResult as $row ) { $uids[] = $row->user_id; @@ -178,7 +180,8 @@ class ActiveUsersPager extends UsersPager { // Note: This is a different loop than for user rights, // because we're reusing it to build the group links // at the same time - foreach ( $user->getGroups() as $group ) { + $groups_list = self::getGroups( intval( $row->user_id ), $this->userGroupCache ); + foreach ( $groups_list as $group ) { if ( in_array( $group, $this->hideGroups ) ) { return ''; } @@ -211,7 +214,8 @@ class ActiveUsersPager extends UsersPager { # Username field $out .= Xml::inputLabel( $this->msg( 'activeusers-from' )->text(), - 'username', 'offset', 20, $this->requestedUser, array( 'tabindex' => 1 ) ) . '<br />'; + 'username', 'offset', 20, $this->requestedUser, + array( 'class' => 'mw-ui-input-inline', 'tabindex' => 1 ) ) . '<br />'; $out .= Xml::checkLabel( $this->msg( 'activeusers-hidebots' )->text(), 'hidebots', 'hidebots', $this->opts->getValue( 'hidebots' ), array( 'tabindex' => 2 ) ); @@ -263,11 +267,22 @@ class SpecialActiveUsers extends SpecialPage { $out->wrapWikiMsg( "<div class='mw-activeusers-intro'>\n$1\n</div>", array( 'activeusers-intro', $this->getLanguage()->formatNum( $days ) ) ); - // Occasionally merge in new updates - $seconds = min( self::mergeActiveUsers( 600, $days ), $days * 86400 ); - // Mention the level of staleness - $out->addWikiMsg( 'cachedspecial-viewing-cached-ttl', - $this->getLanguage()->formatDuration( $seconds ) ); + // Get the timestamp of the last cache update + $dbr = wfGetDB( DB_SLAVE, 'recentchanges' ); + $cTime = $dbr->selectField( 'querycache_info', + 'qci_timestamp', + array( 'qci_type' => 'activeusers' ) + ); + + $secondsOld = $cTime + ? time() - wfTimestamp( TS_UNIX, $cTime ) + : $days * 86400; // fully stale :) + + if ( $secondsOld > 0 ) { + // Mention the level of staleness + $out->addWikiMsg( 'cachedspecial-viewing-cached-ttl', + $this->getLanguage()->formatDuration( $secondsOld ) ); + } $up = new ActiveUsersPager( $this->getContext(), null, $par ); @@ -289,149 +304,4 @@ class SpecialActiveUsers extends SpecialPage { protected function getGroupName() { return 'users'; } - - /** - * @param int $period Seconds (do updates no more often than this) - * @param int $days How many days user must be idle before he is considered inactive - * @return int How many seconds old the cache is - */ - public static function mergeActiveUsers( $period, $days ) { - $dbr = wfGetDB( DB_SLAVE ); - $cTime = $dbr->selectField( 'querycache_info', - 'qci_timestamp', - array( 'qci_type' => 'activeusers' ) - ); - - if ( !wfReadOnly() ) { - if ( !$cTime || ( time() - wfTimestamp( TS_UNIX, $cTime ) ) > $period ) { - $dbw = wfGetDB( DB_MASTER ); - if ( $dbw->estimateRowCount( 'recentchanges' ) <= 10000 ) { - $window = $days * 86400; // small wiki - } else { - $window = $period * 2; - } - $cTime = self::doQueryCacheUpdate( $dbw, $days, $window ) ?: $cTime; - } - } - - return ( time() - - ( $cTime ? wfTimestamp( TS_UNIX, $cTime ) : $days * 86400 ) ); - } - - /** - * @param DatabaseBase $dbw Passed in from updateSpecialPages.php - * @return void - */ - public static function cacheUpdate( DatabaseBase $dbw ) { - global $wgActiveUserDays; - - self::doQueryCacheUpdate( $dbw, $wgActiveUserDays, $wgActiveUserDays * 86400 ); - } - - /** - * Update the query cache as needed - * - * @param DatabaseBase $dbw - * @param int $days How many days user must be idle before he is considered inactive - * @param int $window Maximum time range of new data to scan (in seconds) - * @return int|bool UNIX timestamp the cache is now up-to-date as of (false on error) - */ - protected static function doQueryCacheUpdate( DatabaseBase $dbw, $days, $window ) { - $lockKey = wfWikiID() . '-activeusers'; - if ( !$dbw->lock( $lockKey, __METHOD__, 1 ) ) { - return false; // exclusive update (avoids duplicate entries) - } - - $now = time(); - $cTime = $dbw->selectField( 'querycache_info', - 'qci_timestamp', - array( 'qci_type' => 'activeusers' ) - ); - $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1; - - // Pick the date range to fetch from. This is normally from the last - // update to till the present time, but has a limited window for sanity. - // If the window is limited, multiple runs are need to fully populate it. - $sTimestamp = max( $cTimeUnix, $now - $days * 86400 ); - $eTimestamp = min( $sTimestamp + $window, $now ); - - // Get all the users active since the last update - $res = $dbw->select( - array( 'recentchanges' ), - array( 'rc_user_text', 'lastedittime' => 'MAX(rc_timestamp)' ), - array( - 'rc_user > 0', // actual accounts - 'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata - 'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ), - 'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ), - 'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) ) - ), - __METHOD__, - array( - 'GROUP BY' => array( 'rc_user_text' ), - 'ORDER BY' => 'NULL' // avoid filesort - ) - ); - $names = array(); - foreach ( $res as $row ) { - $names[$row->rc_user_text] = $row->lastedittime; - } - - // Rotate out users that have not edited in too long (according to old data set) - $dbw->delete( 'querycachetwo', - array( - 'qcc_type' => 'activeusers', - 'qcc_value < ' . $dbw->addQuotes( $now - $days * 86400 ) // TS_UNIX - ), - __METHOD__ - ); - - // Find which of the recently active users are already accounted for - if ( count( $names ) ) { - $res = $dbw->select( 'querycachetwo', - array( 'user_name' => 'qcc_title' ), - array( - 'qcc_type' => 'activeusers', - 'qcc_namespace' => NS_USER, - 'qcc_title' => array_keys( $names ) ), - __METHOD__ - ); - foreach ( $res as $row ) { - unset( $names[$row->user_name] ); - } - } - - // Insert the users that need to be added to the list (which their last edit time - if ( count( $names ) ) { - $newRows = array(); - foreach ( $names as $name => $lastEditTime ) { - $newRows[] = array( - 'qcc_type' => 'activeusers', - 'qcc_namespace' => NS_USER, - 'qcc_title' => $name, - 'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ), - 'qcc_namespacetwo' => 0, // unused - 'qcc_titletwo' => '' // unused - ); - } - foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) { - $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ ); - if ( !$dbw->trxLevel() ) { - wfWaitForSlaves(); - } - } - } - - // Touch the data freshness timestamp - $dbw->replace( 'querycache_info', - array( 'qci_type' ), - array( 'qci_type' => 'activeusers', - 'qci_timestamp' => $dbw->timestamp( $eTimestamp ) ), // not always $now - __METHOD__ - ); - - $dbw->unlock( $lockKey, __METHOD__ ); - - return $eTimestamp; - } } diff --git a/includes/specials/SpecialAllMessages.php b/includes/specials/SpecialAllMessages.php index 96be4d03..7b596bb0 100644 --- a/includes/specials/SpecialAllMessages.php +++ b/includes/specials/SpecialAllMessages.php @@ -59,6 +59,7 @@ class SpecialAllMessages extends SpecialPage { $this->outputHeader( 'allmessagestext' ); $out->addModuleStyles( 'mediawiki.special' ); + $this->addHelpLink( 'Help:System message' ); $this->table = new AllmessagesTablePager( $this, @@ -223,19 +224,17 @@ class AllMessagesTablePager extends TablePager { } function getAllMessages( $descending ) { - wfProfileIn( __METHOD__ ); $messageNames = Language::getLocalisationCache()->getSubitemList( 'en', 'messages' ); + + // Normalise message names so they look like page titles and sort correctly - T86139 + $messageNames = array_map( array( $this->lang, 'ucfirst' ), $messageNames ); + if ( $descending ) { rsort( $messageNames ); } else { asort( $messageNames ); } - // Normalise message names so they look like page titles - $messageNames = array_map( array( $this->lang, 'ucfirst' ), $messageNames ); - - wfProfileOut( __METHOD__ ); - return $messageNames; } @@ -252,7 +251,6 @@ class AllMessagesTablePager extends TablePager { */ public static function getCustomisedStatuses( $messageNames, $langcode = 'en', $foreign = false ) { // FIXME: This function should be moved to Language:: or something. - wfProfileIn( __METHOD__ . '-db' ); $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'page', @@ -288,8 +286,6 @@ class AllMessagesTablePager extends TablePager { } } - wfProfileOut( __METHOD__ . '-db' ); - return array( 'pages' => $pageFlags, 'talks' => $talkFlags ); } diff --git a/includes/specials/SpecialAllPages.php b/includes/specials/SpecialAllPages.php index 08b8761a..74b1f7bb 100644 --- a/includes/specials/SpecialAllPages.php +++ b/includes/specials/SpecialAllPages.php @@ -101,7 +101,10 @@ class SpecialAllPages extends IncludableSpecialPage { $t = $this->getPageTitle(); $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); - $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getConfig()->get( 'Script' ) ) ); + $out .= Xml::openElement( + 'form', + array( 'method' => 'get', 'action' => $this->getConfig()->get( 'Script' ) ) + ); $out .= Html::hidden( 'title', $t->getPrefixedText() ); $out .= Xml::openElement( 'fieldset' ); $out .= Xml::element( 'legend', null, $this->msg( 'allpages' )->text() ); diff --git a/includes/specials/SpecialApiHelp.php b/includes/specials/SpecialApiHelp.php new file mode 100644 index 00000000..b43911f5 --- /dev/null +++ b/includes/specials/SpecialApiHelp.php @@ -0,0 +1,93 @@ +<?php +/** + * Implements Special:ApiHelp + * + * 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 + */ + +/** + * Special page to redirect to API help pages, for situations where linking to + * the api.php endpoint is not wanted. + * + * @ingroup SpecialPage + */ +class SpecialApiHelp extends UnlistedSpecialPage { + public function __construct() { + parent::__construct( 'ApiHelp' ); + } + + public function execute( $par ) { + if ( empty( $par ) ) { + $par = 'main'; + } + + // These come from transclusions + $request = $this->getRequest(); + $options = array( + 'action' => 'help', + 'nolead' => true, + 'submodules' => $request->getCheck( 'submodules' ), + 'recursivesubmodules' => $request->getCheck( 'recursivesubmodules' ), + 'title' => $request->getVal( 'title', $this->getPageTitle( '$1' )->getPrefixedText() ), + ); + + // These are for linking from wikitext, since url parameters are a pain + // to do. + while ( true ) { + if ( substr( $par, 0, 4 ) === 'sub/' ) { + $par = substr( $par, 4 ); + $options['submodules'] = 1; + continue; + } + + if ( substr( $par, 0, 5 ) === 'rsub/' ) { + $par = substr( $par, 5 ); + $options['recursivesubmodules'] = 1; + continue; + } + + $moduleName = $par; + break; + } + + if ( !$this->including() ) { + unset( $options['nolead'], $options['title'] ); + $options['modules'] = $moduleName; + $link = wfAppendQuery( wfExpandUrl( wfScript( 'api' ), PROTO_CURRENT ), $options ); + $this->getOutput()->redirect( $link ); + return; + } + + $main = new ApiMain( $this->getContext(), false ); + try { + $module = $main->getModuleFromPath( $moduleName ); + } catch ( UsageException $ex ) { + $this->getOutput()->addHTML( Html::rawElement( 'span', array( 'class' => 'error' ), + $this->msg( 'apihelp-no-such-module', $moduleName )->inContentLanguage()->parse() + ) ); + return; + } + + ApiHelp::getHelp( $this->getContext(), $module, $options ); + } + + public function isIncludable() { + return true; + } +} diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index 3297c17a..b80d921d 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -98,13 +98,16 @@ class SpecialBlock extends FormSpecialPage { $form->setWrapperLegendMsg( 'blockip-legend' ); $form->setHeaderText( '' ); $form->setSubmitCallback( array( __CLASS__, 'processUIForm' ) ); + $form->setSubmitDestructive(); $msg = $this->alreadyBlocked ? 'ipb-change-block' : 'ipbsubmit'; $form->setSubmitTextMsg( $msg ); + $this->addHelpLink( 'Help:Blocking users' ); + # Don't need to do anything if the form has been posted if ( !$this->getRequest()->wasPosted() && $this->preErrors ) { - $s = HTMLForm::formatErrors( $this->preErrors ); + $s = $form->formatErrors( $this->preErrors ); if ( $s ) { $form->addHeaderText( Html::rawElement( 'div', @@ -135,6 +138,7 @@ class SpecialBlock extends FormSpecialPage { 'autofocus' => true, 'required' => true, 'validation-callback' => array( __CLASS__, 'validateTargetField' ), + 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest ), 'Expiry' => array( 'type' => !count( $suggestedDurations ) ? 'text' : 'selectorother', @@ -146,6 +150,7 @@ class SpecialBlock extends FormSpecialPage { ), 'Reason' => array( 'type' => 'selectandother', + 'maxlength' => 255, 'label-message' => 'ipbreason', 'options-message' => 'ipbreason-dropdown', ), @@ -217,7 +222,7 @@ class SpecialBlock extends FormSpecialPage { $this->maybeAlterFormDefaults( $a ); // Allow extensions to add more fields - wfRunHooks( 'SpecialBlockModifyFormFields', array( $this, &$a ) ); + Hooks::run( 'SpecialBlockModifyFormFields', array( $this, &$a ) ); return $a; } @@ -233,6 +238,14 @@ class SpecialBlock extends FormSpecialPage { # This will be overwritten by request data $fields['Target']['default'] = (string)$this->target; + if ( $this->target ) { + $status = self::validateTarget( $this->target, $this->getUser() ); + if ( !$status->isOK() ) { + $errors = $status->getErrorsArray(); + $this->preErrors = array_merge( $this->preErrors, $errors ); + } + } + # This won't be $fields['PreviousTarget']['default'] = (string)$this->target; @@ -307,14 +320,14 @@ class SpecialBlock extends FormSpecialPage { * @return string */ protected function preText() { - $this->getOutput()->addModules( 'mediawiki.special.block' ); + $this->getOutput()->addModules( array( 'mediawiki.special.block', 'mediawiki.userSuggest' ) ); $text = $this->msg( 'blockiptext' )->parse(); $otherBlockMessages = array(); if ( $this->target !== null ) { # Get other blocks, i.e. from GlobalBlocking or TorBlock extension - wfRunHooks( 'OtherBlockLogLink', array( &$otherBlockMessages, $this->target ) ); + Hooks::run( 'OtherBlockLogLink', array( &$otherBlockMessages, $this->target ) ); if ( count( $otherBlockMessages ) ) { $s = Html::rawElement( @@ -623,7 +636,7 @@ class SpecialBlock extends FormSpecialPage { # permission anyway, although the code does allow for it. # Note: Important to use $target instead of $data['Target'] # since both $data['PreviousTarget'] and $target are normalized - # but $data['target'] gets overriden by (non-normalized) request variable + # but $data['target'] gets overridden by (non-normalized) request variable # from previous request. if ( $target === $performer->getName() && ( $data['PreviousTarget'] !== $target || !$data['Confirm'] ) @@ -668,7 +681,7 @@ class SpecialBlock extends FormSpecialPage { # Recheck params here... if ( $type != Block::TYPE_USER ) { $data['HideUser'] = false; # IP users should not be hidden - } elseif ( !in_array( $data['Expiry'], array( 'infinite', 'infinity', 'indefinite' ) ) ) { + } elseif ( !wfIsInfinity( $data['Expiry'] ) ) { # Bad expiry. return array( 'ipb_expiry_temp' ); } elseif ( $wgHideUserContribLimit !== false @@ -698,7 +711,7 @@ class SpecialBlock extends FormSpecialPage { $block->mHideName = $data['HideUser']; $reason = array( 'hookaborted' ); - if ( !wfRunHooks( 'BlockIp', array( &$block, &$performer, &$reason ) ) ) { + if ( !Hooks::run( 'BlockIp', array( &$block, &$performer, &$reason ) ) ) { return $reason; } @@ -759,7 +772,7 @@ class SpecialBlock extends FormSpecialPage { $logaction = 'block'; } - wfRunHooks( 'BlockIpComplete', array( $block, $performer ) ); + Hooks::run( 'BlockIpComplete', array( $block, $performer ) ); # Set *_deleted fields if requested if ( $data['HideUser'] ) { @@ -781,22 +794,21 @@ class SpecialBlock extends FormSpecialPage { # Prepare log parameters $logParams = array(); - $logParams[] = $data['Expiry']; - $logParams[] = self::blockLogFlags( $data, $type ); + $logParams['5::duration'] = $data['Expiry']; + $logParams['6::flags'] = self::blockLogFlags( $data, $type ); # Make log entry, if the name is hidden, put it in the oversight log $log_type = $data['HideUser'] ? 'suppress' : 'block'; - $log = new LogPage( $log_type ); - $log_id = $log->addEntry( - $logaction, - Title::makeTitle( NS_USER, $target ), - $data['Reason'][0], - $logParams, - $performer - ); + $logEntry = new ManualLogEntry( $log_type, $logaction ); + $logEntry->setTarget( Title::makeTitle( NS_USER, $target ) ); + $logEntry->setComment( $data['Reason'][0] ); + $logEntry->setPerformer( $performer ); + $logEntry->setParameters( $logParams ); # Relate log ID to block IDs (bug 25763) $blockIds = array_merge( array( $status['id'] ), $status['autoIds'] ); - $log->addRelations( 'ipb_id', $blockIds, $log_id ); + $logEntry->setRelations( array( 'ipb_id' => $blockIds ) ); + $logId = $logEntry->insert(); + $logEntry->publish( $logId ); # Report to the user return true; @@ -826,7 +838,7 @@ class SpecialBlock extends FormSpecialPage { } list( $show, $value ) = explode( ':', $option ); - $a[htmlspecialchars( $show )] = htmlspecialchars( $value ); + $a[$show] = $value; } return $a; @@ -844,7 +856,7 @@ class SpecialBlock extends FormSpecialPage { $infinity = wfGetDB( DB_SLAVE )->getInfinity(); } - if ( $expiry == 'infinite' || $expiry == 'indefinite' ) { + if ( wfIsInfinity( $expiry ) ) { $expiry = $infinity; } else { $expiry = strtotime( $expiry ); diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php index 456f4ecb..0ec144a2 100644 --- a/includes/specials/SpecialBlockList.php +++ b/includes/specials/SpecialBlockList.php @@ -47,6 +47,7 @@ class SpecialBlockList extends SpecialPage { $lang = $this->getLanguage(); $out->setPageTitle( $this->msg( 'ipblocklist' ) ); $out->addModuleStyles( 'mediawiki.special' ); + $out->addModules( 'mediawiki.userSuggest' ); $request = $this->getRequest(); $par = $request->getVal( 'ip', $par ); @@ -72,6 +73,7 @@ class SpecialBlockList extends SpecialPage { 'tabindex' => '1', 'size' => '45', 'default' => $this->target, + 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest ), 'Options' => array( 'type' => 'multiselect', @@ -103,6 +105,7 @@ class SpecialBlockList extends SpecialPage { $form->setMethod( 'get' ); $form->setWrapperLegendMsg( 'ipblocklist-legend' ); $form->setSubmitTextMsg( 'ipblocklist-submit' ); + $form->setSubmitProgressive(); $form->prepareForm(); $form->displayForm( '' ); @@ -110,11 +113,6 @@ class SpecialBlockList extends SpecialPage { } function showList() { - # Purge expired entries on one in every 10 queries - if ( !mt_rand( 0, 10 ) ) { - Block::purgeExpired(); - } - $conds = array(); # Is the user allowed to see hidden blocks? if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { @@ -167,7 +165,7 @@ class SpecialBlockList extends SpecialPage { # Check for other blocks, i.e. global/tor blocks $otherBlockLink = array(); - wfRunHooks( 'OtherBlockLogLink', array( &$otherBlockLink, $this->target ) ); + Hooks::run( 'OtherBlockLogLink', array( &$otherBlockLink, $this->target ) ); $out = $this->getOutput(); @@ -259,7 +257,6 @@ class BlockListPager extends TablePager { 'blocklist-nousertalk', 'unblocklink', 'change-blocklink', - 'infiniteblock', ); $msg = array_combine( $msg, array_map( array( $this, 'msg' ), $msg ) ); } @@ -396,6 +393,10 @@ class BlockListPager extends TablePager { 'join_conds' => array( 'user' => array( 'LEFT JOIN', 'user_id = ipb_by' ) ) ); + # Filter out any expired blocks + $db = $this->getDatabase(); + $info['conds'][] = 'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ); + # Is the user allowed to see hidden blocks? if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { $info['conds']['ipb_deleted'] = 0; @@ -425,7 +426,6 @@ class BlockListPager extends TablePager { * @param ResultWrapper $result */ function preprocessResults( $result ) { - wfProfileIn( __METHOD__ ); # Do a link batch query $lb = new LinkBatch; $lb->setCaller( __METHOD__ ); @@ -450,6 +450,5 @@ class BlockListPager extends TablePager { } $lb->execute(); - wfProfileOut( __METHOD__ ); } } diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php index 72f4e466..7028fdcb 100644 --- a/includes/specials/SpecialBooksources.php +++ b/includes/specials/SpecialBooksources.php @@ -26,7 +26,6 @@ * The parser creates links to this page when dealing with ISBNs in wikitext * * @author Rob Church <robchur@gmail.com> - * @todo Validate ISBNs using the standard check-digit method * @ingroup SpecialPage */ class SpecialBookSources extends SpecialPage { @@ -73,7 +72,9 @@ class SpecialBookSources extends SpecialPage { $sum = 0; if ( strlen( $isbn ) == 13 ) { for ( $i = 0; $i < 12; $i++ ) { - if ( $i % 2 == 0 ) { + if ( $isbn[$i] === 'X' ) { + return false; + } elseif ( $i % 2 == 0 ) { $sum += $isbn[$i]; } else { $sum += 3 * $isbn[$i]; @@ -81,11 +82,14 @@ class SpecialBookSources extends SpecialPage { } $check = ( 10 - ( $sum % 10 ) ) % 10; - if ( $check == $isbn[12] ) { + if ( (string)$check === $isbn[12] ) { return true; } } elseif ( strlen( $isbn ) == 10 ) { for ( $i = 0; $i < 9; $i++ ) { + if ( $isbn[$i] === 'X' ) { + return false; + } $sum += $isbn[$i] * ( $i + 1 ); } @@ -93,7 +97,7 @@ class SpecialBookSources extends SpecialPage { if ( $check == 10 ) { $check = "X"; } - if ( $check == $isbn[9] ) { + if ( (string)$check === $isbn[9] ) { return true; } } @@ -131,9 +135,14 @@ class SpecialBookSources extends SpecialPage { 'isbn', 20, $this->isbn, - array( 'autofocus' => true ) + array( 'autofocus' => '', 'class' => 'mw-ui-input-inline' ) ); - $form .= ' ' . Xml::submitButton( $this->msg( 'booksources-go' )->text() ) . "</p>\n"; + + $form .= ' ' . Html::submitButton( + $this->msg( 'booksources-search' )->text(), + array(), array( 'mw-ui-progressive' ) + ) . "</p>\n"; + $form .= Html::closeElement( 'form' ) . "\n"; $form .= Html::closeElement( 'fieldset' ) . "\n"; @@ -152,7 +161,7 @@ class SpecialBookSources extends SpecialPage { # Hook to allow extensions to insert additional HTML, # e.g. for API-interacting plugins and so on - wfRunHooks( 'BookInformation', array( $this->isbn, $this->getOutput() ) ); + Hooks::run( 'BookInformation', array( $this->isbn, $this->getOutput() ) ); # Check for a local page such as Project:Book_sources and use that if available $page = $this->msg( 'booksources' )->inContentLanguage()->text(); diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php index 95f9efd2..3a13b7ed 100644 --- a/includes/specials/SpecialCategories.php +++ b/includes/specials/SpecialCategories.php @@ -188,9 +188,11 @@ class CategoryPager extends AlphabeticPager { $this->msg( 'categories' )->text(), Xml::inputLabel( $this->msg( 'categoriesfrom' )->text(), - 'from', 'from', 20, $from ) . + 'from', 'from', 20, $from, array( 'class' => 'mw-ui-input-inline' ) ) . ' ' . - Xml::submitButton( $this->msg( 'allpagessubmit' )->text() + Html::submitButton( + $this->msg( 'allpagessubmit' )->text(), + array(), array( 'mw-ui-progressive' ) ) ) ); diff --git a/includes/specials/SpecialChangeEmail.php b/includes/specials/SpecialChangeEmail.php index e0be838b..eca307d9 100644 --- a/includes/specials/SpecialChangeEmail.php +++ b/includes/specials/SpecialChangeEmail.php @@ -39,7 +39,7 @@ class SpecialChangeEmail extends FormSpecialPage { /** * @return bool */ - function isListed() { + public function isListed() { global $wgAuth; return $wgAuth->allowPropChange( 'emailaddress' ); @@ -54,7 +54,7 @@ class SpecialChangeEmail extends FormSpecialPage { $out->disallowUserJs(); $out->addModules( 'mediawiki.special.changeemail' ); - return parent::execute( $par ); + parent::execute( $par ); } protected function checkExecutePermissions( User $user ) { @@ -106,22 +106,20 @@ class SpecialChangeEmail extends FormSpecialPage { return $fields; } + protected function getDisplayFormat() { + return 'vform'; + } + protected function alterForm( HTMLForm $form ) { - $form->setDisplayFormat( 'vform' ); $form->setId( 'mw-changeemail-form' ); $form->setTableId( 'mw-changeemail-table' ); - $form->setWrapperLegend( false ); $form->setSubmitTextMsg( 'changeemail-submit' ); - $form->addHiddenField( 'returnto', $this->getRequest()->getVal( 'returnto' ) ); + $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) ); } public function onSubmit( array $data ) { - if ( $this->getRequest()->getBool( 'wpCancel' ) ) { - $status = Status::newGood( true ); - } else { - $password = isset( $data['Password'] ) ? $data['Password'] : null; - $status = $this->attemptChange( $this->getUser(), $password, $data['NewEmail'] ); - } + $password = isset( $data['Password'] ) ? $data['Password'] : null; + $status = $this->attemptChange( $this->getUser(), $password, $data['NewEmail'] ); $this->status = $status; @@ -129,18 +127,22 @@ class SpecialChangeEmail extends FormSpecialPage { } public function onSuccess() { - $titleObj = Title::newFromText( $this->getRequest()->getVal( 'returnto' ) ); + $request = $this->getRequest(); + + $titleObj = Title::newFromText( $request->getVal( 'returnto' ) ); if ( !$titleObj instanceof Title ) { $titleObj = Title::newMainPage(); } + $query = $request->getVal( 'returntoquery' ); if ( $this->status->value === true ) { - $this->getOutput()->redirect( $titleObj->getFullURL() ); + $this->getOutput()->redirect( $titleObj->getFullURL( $query ) ); } elseif ( $this->status->value === 'eauth' ) { # Notify user that a confirmation email has been sent... $this->getOutput()->wrapWikiMsg( "<div class='error' style='clear: both;'>\n$1\n</div>", 'eauthentsent', $this->getUser()->getName() ); - $this->getOutput()->addReturnTo( $titleObj ); // just show the link to go back + // just show the link to go back + $this->getOutput()->addReturnTo( $titleObj, wfCgiToArray( $query ) ); } } @@ -150,7 +152,7 @@ class SpecialChangeEmail extends FormSpecialPage { * @param string $newaddr * @return Status */ - protected function attemptChange( User $user, $pass, $newaddr ) { + private function attemptChange( User $user, $pass, $newaddr ) { global $wgAuth; if ( $newaddr != '' && !Sanitizer::validateEmail( $newaddr ) ) { @@ -184,7 +186,7 @@ class SpecialChangeEmail extends FormSpecialPage { return $status; } - wfRunHooks( 'PrefsEmailAudit', array( $user, $oldaddr, $newaddr ) ); + Hooks::run( 'PrefsEmailAudit', array( $user, $oldaddr, $newaddr ) ); $user->saveSettings(); diff --git a/includes/specials/SpecialChangePassword.php b/includes/specials/SpecialChangePassword.php index 24664edb..168095f8 100644 --- a/includes/specials/SpecialChangePassword.php +++ b/includes/specials/SpecialChangePassword.php @@ -118,7 +118,7 @@ class SpecialChangePassword extends FormSpecialPage { } $extraFields = array(); - wfRunHooks( 'ChangePasswordForm', array( &$extraFields ) ); + Hooks::run( 'ChangePasswordForm', array( &$extraFields ) ); foreach ( $extraFields as $extra ) { list( $name, $label, $type, $default ) = $extra; $fields[$name] = array( @@ -248,7 +248,7 @@ class SpecialChangePassword extends FormSpecialPage { } if ( $newpass !== $retype ) { - wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'badretype' ) ); + Hooks::run( 'PrefsPasswordAudit', array( $user, $newpass, 'badretype' ) ); throw new PasswordError( $this->msg( 'badretype' )->text() ); } @@ -264,7 +264,7 @@ class SpecialChangePassword extends FormSpecialPage { // @todo Make these separate messages, since the message is written for both cases if ( !$user->checkTemporaryPassword( $oldpass ) && !$user->checkPassword( $oldpass ) ) { - wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'wrongpassword' ) ); + Hooks::run( 'PrefsPasswordAudit', array( $user, $newpass, 'wrongpassword' ) ); throw new PasswordError( $this->msg( 'resetpass-wrong-oldpass' )->text() ); } @@ -276,8 +276,8 @@ class SpecialChangePassword extends FormSpecialPage { // Do AbortChangePassword after checking mOldpass, so we don't leak information // by possibly aborting a new password before verifying the old password. $abortMsg = 'resetpass-abort-generic'; - if ( !wfRunHooks( 'AbortChangePassword', array( $user, $oldpass, $newpass, &$abortMsg ) ) ) { - wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'abortreset' ) ); + if ( !Hooks::run( 'AbortChangePassword', array( $user, $oldpass, $newpass, &$abortMsg ) ) ) { + Hooks::run( 'PrefsPasswordAudit', array( $user, $newpass, 'abortreset' ) ); throw new PasswordError( $this->msg( $abortMsg )->text() ); } @@ -288,9 +288,9 @@ class SpecialChangePassword extends FormSpecialPage { try { $user->setPassword( $newpass ); - wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'success' ) ); + Hooks::run( 'PrefsPasswordAudit', array( $user, $newpass, 'success' ) ); } catch ( PasswordError $e ) { - wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'error' ) ); + Hooks::run( 'PrefsPasswordAudit', array( $user, $newpass, 'error' ) ); throw new PasswordError( $e->getMessage() ); } diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php index d771589d..b6ab112b 100644 --- a/includes/specials/SpecialConfirmemail.php +++ b/includes/specials/SpecialConfirmemail.php @@ -38,6 +38,9 @@ class EmailConfirmation extends UnlistedSpecialPage { * Main execution point * * @param null|string $code Confirmation code passed to the page + * @throws PermissionsError + * @throws ReadOnlyError + * @throws UserNotLoggedIn */ function execute( $code ) { $this->setHeaders(); diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 32a887c4..c2cd8122 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -176,7 +176,7 @@ class SpecialContributions extends IncludableSpecialPage { // Add RSS/atom links $this->addFeedLinks( $feedParams ); - if ( wfRunHooks( 'SpecialContributionsBeforeMainOutput', array( $id, $userObj, $this ) ) ) { + if ( Hooks::run( 'SpecialContributionsBeforeMainOutput', array( $id, $userObj, $this ) ) ) { if ( !$this->including() ) { $out->addHTML( $this->getForm() ); } @@ -386,7 +386,7 @@ class SpecialContributions extends IncludableSpecialPage { ); } - wfRunHooks( 'ContributionsToolLinks', array( $id, $userpage, &$tools ) ); + Hooks::run( 'ContributionsToolLinks', array( $id, $userpage, &$tools ) ); return $tools; } @@ -478,18 +478,15 @@ class SpecialContributions extends IncludableSpecialPage { if ( $tagFilter ) { $filterSelection = Html::rawElement( 'td', - array( 'class' => 'mw-label' ), - array_shift( $tagFilter ) - ); - $filterSelection .= Html::rawElement( - 'td', - array( 'class' => 'mw-input' ), - implode( ' ', $tagFilter ) + array(), + array_shift( $tagFilter ) . implode( ' ', $tagFilter ) ); } else { $filterSelection = Html::rawElement( 'td', array( 'colspan' => 2 ), '' ); } + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); + $labelNewbies = Xml::radioLabel( $this->msg( 'sp-contributions-newbies' )->text(), 'contribs', @@ -510,9 +507,15 @@ class SpecialContributions extends IncludableSpecialPage { 'target', $this->opts['target'], 'text', - array( 'size' => '40', 'required' => '', 'class' => 'mw-input' ) + - ( $this->opts['target'] ? array() : array( 'autofocus' ) - ) + array( + 'size' => '40', + 'required' => '', + 'class' => array( + 'mw-input', + 'mw-ui-input-inline', + 'mw-autocomplete-user', // used by mediawiki.userSuggest + ), + ) + ( $this->opts['target'] ? array() : array( 'autofocus' ) ) ); $targetSelection = Html::rawElement( 'td', @@ -522,16 +525,12 @@ class SpecialContributions extends IncludableSpecialPage { $namespaceSelection = Xml::tags( 'td', - array( 'class' => 'mw-label' ), + array(), Xml::label( $this->msg( 'namespace' )->text(), 'namespace', '' - ) - ); - $namespaceSelection .= Html::rawElement( - 'td', - null, + ) . Html::namespaceSelector( array( 'selected' => $this->opts['namespace'], 'all' => '' ), array( @@ -617,9 +616,9 @@ class SpecialContributions extends IncludableSpecialPage { $this->opts['year'] === '' ? MWTimestamp::getInstance()->format( 'Y' ) : $this->opts['year'], $this->opts['month'] ) . ' ' . - Xml::submitButton( + Html::submitButton( $this->msg( 'sp-contributions-submit' )->text(), - array( 'class' => 'mw-submit' ) + array( 'class' => 'mw-submit' ), array( 'mw-ui-progressive' ) ) ); @@ -659,7 +658,7 @@ class ContribsPager extends ReverseChronologicalPager { public $mDb; public $preventClickjacking = false; - /** @var DatabaseBase */ + /** @var IDatabase */ public $mDbSecondary; /** @@ -673,10 +672,7 @@ class ContribsPager extends ReverseChronologicalPager { $msgs = array( 'diff', 'hist', - 'newarticle', 'pipe-separator', - 'rev-delundel', - 'rollbacklink', 'uctop' ); @@ -715,7 +711,7 @@ class ContribsPager extends ReverseChronologicalPager { /** * This method basically executes the exact same code as the parent class, though with - * a hook added, to allow extentions to add additional queries. + * a hook added, to allow extensions to add additional queries. * * @param string $offset Index offset, inclusive * @param int $limit Exact query limit @@ -751,7 +747,7 @@ class ContribsPager extends ReverseChronologicalPager { $data = array( $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ) ); - wfRunHooks( + Hooks::run( 'ContribsPager::reallyDoQuery', array( &$data, $pager, $offset, $limit, $descending ) ); @@ -828,7 +824,7 @@ class ContribsPager extends ReverseChronologicalPager { $this->tagFilter ); - wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) ); + Hooks::run( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) ); return $queryInfo; } @@ -935,7 +931,7 @@ class ContribsPager extends ReverseChronologicalPager { * @return string */ function getStartBody() { - return "<ul>\n"; + return "<ul class=\"mw-contributions-list\">\n"; } /** @@ -958,7 +954,6 @@ class ContribsPager extends ReverseChronologicalPager { * @return string */ function formatRow( $row ) { - wfProfileIn( __METHOD__ ); $ret = ''; $classes = array(); @@ -974,7 +969,7 @@ class ContribsPager extends ReverseChronologicalPager { try { $rev = new Revision( $row ); $validRevision = (bool)$rev->getId(); - } catch ( MWException $e ) { + } catch ( Exception $e ) { $validRevision = false; } wfRestoreWarnings(); @@ -1113,7 +1108,7 @@ class ContribsPager extends ReverseChronologicalPager { } // Let extensions add data - wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) ); + Hooks::run( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) ); if ( $classes === array() && $ret === '' ) { wfDebug( "Dropping Special:Contribution row that could not be formatted\n" ); @@ -1122,8 +1117,6 @@ class ContribsPager extends ReverseChronologicalPager { $ret = Html::rawElement( 'li', array( 'class' => $classes ), $ret ) . "\n"; } - wfProfileOut( __METHOD__ ); - return $ret; } diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php index 68f2c469..9e4bbbe5 100644 --- a/includes/specials/SpecialDeletedContributions.php +++ b/includes/specials/SpecialDeletedContributions.php @@ -78,6 +78,53 @@ class DeletedContribsPager extends IndexPager { ); } + /** + * This method basically executes the exact same code as the parent class, though with + * a hook added, to allow extensions to add additional queries. + * + * @param string $offset Index offset, inclusive + * @param int $limit Exact query limit + * @param bool $descending Query direction, false for ascending, true for descending + * @return ResultWrapper + */ + function reallyDoQuery( $offset, $limit, $descending ) { + $pager = $this; + + $data = array( parent::reallyDoQuery( $offset, $limit, $descending ) ); + + // This hook will allow extensions to add in additional queries, nearly + // identical to ContribsPager::reallyDoQuery. + Hooks::run( + 'DeletedContribsPager::reallyDoQuery', + array( &$data, $pager, $offset, $limit, $descending ) + ); + + $result = array(); + + // loop all results and collect them in an array + foreach ( $data as $query ) { + foreach ( $query as $i => $row ) { + // use index column as key, allowing us to easily sort in PHP + $result[$row->{$this->getIndexField()} . "-$i"] = $row; + } + } + + // sort results + if ( $descending ) { + ksort( $result ); + } else { + krsort( $result ); + } + + // enforce limit + $result = array_slice( $result, 0, $limit ); + + // get rid of array keys + $result = array_values( $result ); + + return new FakeResultWrapper( $result ); + } + function getUserCond() { $condition = array(); @@ -141,6 +188,50 @@ class DeletedContribsPager extends IndexPager { /** * Generates each row in the contributions list. * + * @todo This would probably look a lot nicer in a table. + * @param stdClass $row + * @return string + */ + function formatRow( $row ) { + $ret = ''; + $classes = array(); + + /* + * There may be more than just revision rows. To make sure that we'll only be processing + * revisions here, let's _try_ to build a revision out of our row (without displaying + * notices though) and then trying to grab data from the built object. If we succeed, + * we're definitely dealing with revision data and we may proceed, if not, we'll leave it + * to extensions to subscribe to the hook to parse the row. + */ + wfSuppressWarnings(); + try { + $rev = Revision::newFromArchiveRow( $row ); + $validRevision = (bool)$rev->getId(); + } catch ( Exception $e ) { + $validRevision = false; + } + wfRestoreWarnings(); + + if ( $validRevision ) { + $ret = $this->formatRevisionRow( $row ); + } + + // Let extensions add data + Hooks::run( 'DeletedContributionsLineEnding', array( $this, &$ret, $row, &$classes ) ); + + if ( $classes === array() && $ret === '' ) { + wfDebug( "Dropping Special:DeletedContribution row that could not be formatted\n" ); + $ret = "<!-- Could not format Special:DeletedContribution row. -->\n"; + } else { + $ret = Html::rawElement( 'li', array( 'class' => $classes ), $ret ) . "\n"; + } + + return $ret; + } + + /** + * Generates each row in the contributions list for archive entries. + * * Contributions which are marked "top" are currently on top of the history. * For these contributions, a [rollback] link is shown for users with sysop * privileges. The rollback link restores the most recent version that was not @@ -150,9 +241,7 @@ class DeletedContribsPager extends IndexPager { * @param stdClass $row * @return string */ - function formatRow( $row ) { - wfProfileIn( __METHOD__ ); - + function formatRevisionRow( $row ) { $page = Title::makeTitle( $row->ar_namespace, $row->ar_title ); $rev = new Revision( array( @@ -256,17 +345,13 @@ class DeletedContribsPager extends IndexPager { $ret .= " <strong>" . $this->msg( 'rev-deleted-user-contribs' )->escaped() . "</strong>"; } - $ret = Html::rawElement( 'li', array(), $ret ) . "\n"; - - wfProfileOut( __METHOD__ ); - return $ret; } /** * Get the Database object in use * - * @return DatabaseBase + * @return IDatabase */ public function getDatabase() { return $this->mDb; @@ -315,7 +400,8 @@ class DeletedContributionsPage extends SpecialPage { return; } - $options['limit'] = $request->getInt( 'limit', $this->getConfig()->get( 'QueryPageDefaultLimit' ) ); + $options['limit'] = $request->getInt( 'limit', + $this->getConfig()->get( 'QueryPageDefaultLimit' ) ); $options['target'] = $target; $userObj = User::newFromName( $target, false ); @@ -465,7 +551,7 @@ class DeletedContributionsPage extends SpecialPage { ); } - wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) ); + Hooks::run( 'ContributionsToolLinks', array( $id, $nt, &$tools ) ); $links = $this->getLanguage()->pipeList( $tools ); @@ -533,6 +619,8 @@ class DeletedContributionsPage extends SpecialPage { $f .= "\t" . Html::hidden( $name, $value ) . "\n"; } + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); + $f .= Xml::openElement( 'fieldset' ); $f .= Xml::element( 'legend', array(), $this->msg( 'sp-contributions-search' )->text() ); $f .= Xml::tags( @@ -546,7 +634,10 @@ class DeletedContributionsPage extends SpecialPage { 'text', array( 'size' => '20', - 'required' => '' + 'required' => '', + 'class' => array( + 'mw-autocomplete-user', // used by mediawiki.userSuggest + ), ) + ( $options['target'] ? array() : array( 'autofocus' ) ) ) . ' '; $f .= Html::namespaceSelector( diff --git a/includes/specials/SpecialDiff.php b/includes/specials/SpecialDiff.php index 77d23173..89c1c021 100644 --- a/includes/specials/SpecialDiff.php +++ b/includes/specials/SpecialDiff.php @@ -53,6 +53,7 @@ class SpecialDiff extends RedirectSpecialPage { $this->mAddedRedirectParams['diff'] = $parts[1]; } else { // Wrong number of parameters, bail out + $this->addHelpLink( 'Help:Diff' ); throw new ErrorPageError( 'nopagetitle', 'nopagetext' ); } diff --git a/includes/specials/SpecialEditTags.php b/includes/specials/SpecialEditTags.php new file mode 100644 index 00000000..f41a1f1d --- /dev/null +++ b/includes/specials/SpecialEditTags.php @@ -0,0 +1,460 @@ +<?php +/** + * 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 + */ + +/** + * Special page for adding and removing change tags to individual revisions. + * A lot of this is copied out of SpecialRevisiondelete. + * + * @ingroup SpecialPage + * @since 1.25 + */ +class SpecialEditTags extends UnlistedSpecialPage { + /** @var bool Was the DB modified in this request */ + protected $wasSaved = false; + + /** @var bool True if the submit button was clicked, and the form was posted */ + private $submitClicked; + + /** @var array Target ID list */ + private $ids; + + /** @var Title Title object for target parameter */ + private $targetObj; + + /** @var string Deletion type, may be revision or logentry */ + private $typeName; + + /** @var ChangeTagsList Storing the list of items to be tagged */ + private $revList; + + /** @var bool Whether user is allowed to perform the action */ + private $isAllowed; + + /** @var string */ + private $reason; + + public function __construct() { + parent::__construct( 'EditTags', 'changetags' ); + } + + public function execute( $par ) { + $this->checkPermissions(); + $this->checkReadOnly(); + + $output = $this->getOutput(); + $user = $this->getUser(); + $request = $this->getRequest(); + + $this->setHeaders(); + $this->outputHeader(); + + $this->getOutput()->addModules( array( 'mediawiki.special.edittags', + 'mediawiki.special.edittags.styles' ) ); + + $this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' ); + + // Handle our many different possible input types + $ids = $request->getVal( 'ids' ); + if ( !is_null( $ids ) ) { + // Allow CSV from the form hidden field, or a single ID for show/hide links + $this->ids = explode( ',', $ids ); + } else { + // Array input + $this->ids = array_keys( $request->getArray( 'ids', array() ) ); + } + $this->ids = array_unique( array_filter( $this->ids ) ); + + // No targets? + if ( count( $this->ids ) == 0 ) { + throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' ); + } + + $this->typeName = $request->getVal( 'type' ); + $this->targetObj = Title::newFromText( $request->getText( 'target' ) ); + + // sanity check of parameter + switch ( $this->typeName ) { + case 'logentry': + case 'logging': + $this->typeName = 'logentry'; + break; + default: + $this->typeName = 'revision'; + break; + } + + // Allow the list type to adjust the passed target + // Yuck! Copied straight out of SpecialRevisiondelete, but it does exactly + // what we want + $this->targetObj = RevisionDeleter::suggestTarget( + $this->typeName === 'revision' ? 'revision' : 'logging', + $this->targetObj, + $this->ids + ); + + $this->isAllowed = $user->isAllowed( 'changetags' ); + + $this->reason = $request->getVal( 'wpReason' ); + // We need a target page! + if ( is_null( $this->targetObj ) ) { + $output->addWikiMsg( 'undelete-header' ); + return; + } + // Give a link to the logs/hist for this page + $this->showConvenienceLinks(); + + // Either submit or create our form + if ( $this->isAllowed && $this->submitClicked ) { + $this->submit( $request ); + } else { + $this->showForm(); + } + + // Show relevant lines from the tag log + $tagLogPage = new LogPage( 'tag' ); + $output->addHTML( "<h2>" . $tagLogPage->getName()->escaped() . "</h2>\n" ); + LogEventsList::showLogExtract( + $output, + 'tag', + $this->targetObj, + '', /* user */ + array( 'lim' => 25, 'conds' => array(), 'useMaster' => $this->wasSaved ) + ); + } + + /** + * Show some useful links in the subtitle + */ + protected function showConvenienceLinks() { + // Give a link to the logs/hist for this page + if ( $this->targetObj ) { + // Also set header tabs to be for the target. + $this->getSkin()->setRelevantTitle( $this->targetObj ); + + $links = array(); + $links[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Log' ), + $this->msg( 'viewpagelogs' )->escaped(), + array(), + array( + 'page' => $this->targetObj->getPrefixedText(), + 'hide_tag_log' => '0', + ) + ); + if ( !$this->targetObj->isSpecialPage() ) { + // Give a link to the page history + $links[] = Linker::linkKnown( + $this->targetObj, + $this->msg( 'pagehist' )->escaped(), + array(), + array( 'action' => 'history' ) + ); + } + // Link to Special:Tags + $links[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Tags' ), + $this->msg( 'tags-edit-manage-link' )->escaped() + ); + // Logs themselves don't have histories or archived revisions + $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) ); + } + } + + /** + * Get the list object for this request + * @return ChangeTagsList + */ + protected function getList() { + if ( is_null( $this->revList ) ) { + $this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(), + $this->targetObj, $this->ids ); + } + + return $this->revList; + } + + /** + * Show a list of items that we will operate on, and show a form which allows + * the user to modify the tags applied to those items. + */ + protected function showForm() { + $userAllowed = true; + + $out = $this->getOutput(); + // Messages: tags-edit-revision-selected, tags-edit-logentry-selected + $out->wrapWikiMsg( "<strong>$1</strong>", array( + "tags-edit-{$this->typeName}-selected", + $this->getLanguage()->formatNum( count( $this->ids ) ), + $this->targetObj->getPrefixedText() + ) ); + + $this->addHelpLink( 'Help:Tags' ); + $out->addHTML( "<ul>" ); + + $numRevisions = 0; + // Live revisions... + $list = $this->getList(); + // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed + for ( $list->reset(); $list->current(); $list->next() ) { + // @codingStandardsIgnoreEnd + $item = $list->current(); + $numRevisions++; + $out->addHTML( $item->getHTML() ); + } + + if ( !$numRevisions ) { + throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' ); + } + + $out->addHTML( "</ul>" ); + // Explanation text + $out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" ); + + // Show form if the user can submit + if ( $this->isAllowed ) { + $form = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ), + 'id' => 'mw-revdel-form-revisions' ) ) . + Xml::fieldset( $this->msg( "tags-edit-{$this->typeName}-legend", + count( $this->ids ) )->text() ) . + $this->buildCheckBoxes() . + Xml::openElement( 'table' ) . + "<tr>\n" . + '<td class="mw-label">' . + Xml::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) . + '</td>' . + '<td class="mw-input">' . + Xml::input( + 'wpReason', + 60, + $this->reason, + array( 'id' => 'wpReason', 'maxlength' => 100 ) + ) . + '</td>' . + "</tr><tr>\n" . + '<td></td>' . + '<td class="mw-submit">' . + Xml::submitButton( $this->msg( "tags-edit-{$this->typeName}-submit", + $numRevisions )->text(), array( 'name' => 'wpSubmit' ) ) . + '</td>' . + "</tr>\n" . + Xml::closeElement( 'table' ) . + Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) . + Html::hidden( 'target', $this->targetObj->getPrefixedText() ) . + Html::hidden( 'type', $this->typeName ) . + Html::hidden( 'ids', implode( ',', $this->ids ) ) . + Xml::closeElement( 'fieldset' ) . "\n" . + Xml::closeElement( 'form' ) . "\n"; + } else { + $form = ''; + } + $out->addHTML( $form ); + } + + /** + * @return string HTML + */ + protected function buildCheckBoxes() { + // If there is just one item, provide the user with a multi-select field + $list = $this->getList(); + if ( $list->length() == 1 ) { + $list->reset(); + $tags = $list->current()->getTags(); + if ( $tags ) { + $tags = explode( ',', $tags ); + } else { + $tags = array(); + } + + $html = '<table id="mw-edittags-tags-selector">'; + $html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() . + '</td><td>'; + if ( $tags ) { + $html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) ); + } else { + $html .= $this->msg( 'tags-edit-existing-tags-none' )->parse(); + } + $html .= '</td></tr>'; + $tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() ); + $html .= '<tr><td>' . $tagSelect[0] . '</td><td>' . $tagSelect[1]; + // also output the tags currently applied as a hidden form field, so we + // know what to remove from the revision/log entry when the form is submitted + $html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) ); + $html .= '</td></tr></table>'; + } else { + // Otherwise, use a multi-select field for adding tags, and a list of + // checkboxes for removing them + $tags = array(); + + // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed + for ( $list->reset(); $list->current(); $list->next() ) { + // @codingStandardsIgnoreEnd + $currentTags = $list->current()->getTags(); + if ( $currentTags ) { + $tags = array_merge( $tags, explode( ',', $currentTags ) ); + } + } + $tags = array_unique( $tags ); + + $html = '<table id="mw-edittags-tags-selector-multi"><tr><td>'; + $tagSelect = $this->getTagSelect( array(), $this->msg( 'tags-edit-add' )->plain() ); + $html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1] . '</td><td>'; + $html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() ); + $html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(), + 'wpRemoveAllTags', 'mw-edittags-remove-all' ); + $i = 0; // used for generating checkbox IDs only + foreach ( $tags as $tag ) { + $html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag, + 'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, array( + 'value' => $tag, + 'class' => 'mw-edittags-remove-checkbox', + ) ); + } + $html .= '</td></tr></table>'; + } + + return $html; + } + + /** + * Returns a <select multiple> element with a list of change tags that can be + * applied by users. + * + * @param array $selectedTags The tags that should be preselected in the + * list. Any tags in this list, but not in the list returned by + * ChangeTags::listExplicitlyDefinedTags, will be appended to the <select> + * element. + * @param string $label The text of a <label> to precede the <select> + * @return array HTML <label> element at index 0, HTML <select> element at + * index 1 + */ + protected function getTagSelect( $selectedTags, $label ) { + $result = array(); + $result[0] = Xml::label( $label, 'mw-edittags-tag-list' ); + $result[1] = Xml::openElement( 'select', array( + 'name' => 'wpTagList[]', + 'id' => 'mw-edittags-tag-list', + 'multiple' => 'multiple', + 'size' => '8', + ) ); + + $tags = ChangeTags::listExplicitlyDefinedTags(); + $tags = array_unique( array_merge( $tags, $selectedTags ) ); + foreach ( $tags as $tag ) { + $result[1] .= Xml::option( $tag, $tag, in_array( $tag, $selectedTags ) ); + } + + $result[1] .= Xml::closeElement( 'select' ); + return $result; + } + + /** + * UI entry point for form submission. + * @throws PermissionsError + * @return bool + */ + protected function submit() { + // Check edit token on submission + $request = $this->getRequest(); + $token = $request->getVal( 'wpEditToken' ); + if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) { + $this->getOutput()->addWikiMsg( 'sessionfailure' ); + return false; + } + + // Evaluate incoming request data + $tagList = $request->getArray( 'wpTagList' ); + if ( is_null( $tagList ) ) { + $tagList = array(); + } + $existingTags = $request->getVal( 'wpExistingTags' ); + if ( is_null( $existingTags ) || $existingTags === '' ) { + $existingTags = array(); + } else { + $existingTags = explode( ',', $existingTags ); + } + + if ( count( $this->ids ) > 1 ) { + // multiple revisions selected + $tagsToAdd = $tagList; + if ( $request->getBool( 'wpRemoveAllTags' ) ) { + $tagsToRemove = $existingTags; + } else { + $tagsToRemove = $request->getArray( 'wpTagsToRemove' ); + } + } else { + // single revision selected + // The user tells us which tags they want associated to the revision. + // We have to figure out which ones to add, and which to remove. + $tagsToAdd = array_diff( $tagList, $existingTags ); + $tagsToRemove = array_diff( $existingTags, $tagList ); + } + + if ( !$tagsToAdd && !$tagsToRemove ) { + $status = Status::newFatal( 'tags-edit-none-selected' ); + } else { + $status = $this->getList()->updateChangeTagsOnAll( $tagsToAdd, + $tagsToRemove, null, $this->reason, $this->getUser() ); + } + + if ( $status->isGood() ) { + $this->success(); + return true; + } else { + $this->failure( $status ); + return false; + } + } + + /** + * Report that the submit operation succeeded + */ + protected function success() { + $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) ); + $this->getOutput()->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>", + 'tags-edit-success' ); + $this->wasSaved = true; + $this->revList->reloadFromMaster(); + $this->reason = ''; // no need to spew the reason back at the user + $this->showForm(); + } + + /** + * Report that the submit operation failed + * @param Status $status + */ + protected function failure( $status ) { + $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) ); + $this->getOutput()->addWikiText( '<div class="errorbox">' . + $status->getWikiText( 'tags-edit-failure' ) . + '</div>' + ); + $this->showForm(); + } + + public function getDescription() { + return $this->msg( 'tags-edit-title' )->text(); + } + + protected function getGroupName() { + return 'pagetools'; + } +} diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php index 3656b9cc..910fe259 100644 --- a/includes/specials/SpecialEditWatchlist.php +++ b/includes/specials/SpecialEditWatchlist.php @@ -134,22 +134,17 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { } /** - * Return an array of subpages beginning with $search that this special page will accept. + * Return an array of subpages that this special page will accept. * - * @param string $search Prefix to search for - * @param int $limit Maximum number of results to return - * @return string[] Matching subpages + * @see also SpecialWatchlist::getSubpagesForPrefixSearch + * @return string[] subpages */ - public function prefixSearchSubpages( $search, $limit = 10 ) { - return self::prefixSearchArray( - $search, - $limit, - // SpecialWatchlist uses SpecialEditWatchlist::getMode, so new types should be added - // here and there - no 'edit' here, because that the default for this page - array( - 'clear', - 'raw', - ) + public function getSubpagesForPrefixSearch() { + // SpecialWatchlist uses SpecialEditWatchlist::getMode, so new types should be added + // here and there - no 'edit' here, because that the default for this page + return array( + 'clear', + 'raw', ); } @@ -261,7 +256,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { // Do a batch existence check $batch = new LinkBatch(); if ( count( $titles ) >= 100 ) { - $output = wfMessage( 'watchlistedit-too-many' )->parse(); + $output = $this->msg( 'watchlistedit-too-many' )->parse(); return; } foreach ( $titles as $title ) { @@ -349,7 +344,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { */ protected function getWatchlistInfo() { $titles = array(); - $dbr = wfGetDB( DB_MASTER ); + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( array( 'watchlist' ), @@ -518,7 +513,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { ); $page = WikiPage::factory( $title ); - wfRunHooks( 'UnwatchArticleComplete', array( $this->getUser(), &$page ) ); + Hooks::run( 'UnwatchArticleComplete', array( $this->getUser(), &$page ) ); } } } @@ -556,7 +551,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { // Allow subscribers to manipulate the list of watched pages (or use it // to preload lots of details at once) $watchlistInfo = $this->getWatchlistInfo(); - wfRunHooks( + Hooks::run( 'WatchlistEditorBeforeFormRender', array( &$watchlistInfo ) ); @@ -609,6 +604,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $context->setTitle( $this->getPageTitle() ); // Remove subpage $form = new EditWatchlistNormalHTMLForm( $fields, $context ); $form->setSubmitTextMsg( 'watchlistedit-normal-submit' ); + $form->setSubmitDestructive(); # Used message keys: # 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit' $form->setSubmitTooltip( 'watchlistedit-normal-submit' ); @@ -628,7 +624,10 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { private function buildRemoveLine( $title ) { $link = Linker::link( $title ); - $tools['talk'] = Linker::link( $title->getTalkPage(), $this->msg( 'talkpagelinktext' )->escaped() ); + $tools['talk'] = Linker::link( + $title->getTalkPage(), + $this->msg( 'talkpagelinktext' )->escaped() + ); if ( $title->exists() ) { $tools['history'] = Linker::linkKnown( @@ -646,7 +645,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { ); } - wfRunHooks( + Hooks::run( 'WatchlistEditorBuildRemoveLine', array( &$tools, $title, $title->isRedirect(), $this->getSkin(), &$link ) ); @@ -701,6 +700,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $form->setWrapperLegendMsg( 'watchlistedit-clear-legend' ); $form->addHeaderText( $this->msg( 'watchlistedit-clear-explain' )->parse() ); $form->setSubmitCallback( array( $this, 'submitClear' ) ); + $form->setSubmitDestructive(); return $form; } @@ -760,7 +760,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { return Html::rawElement( 'span', array( 'class' => 'mw-watchlist-toollinks' ), - wfMessage( 'parentheses', $wgLang->pipeList( $tools ) )->text() + wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $tools ) )->escaped() ); } } diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index 20532a92..c55fa94c 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -160,7 +160,7 @@ class SpecialEmailUser extends UnlistedSpecialPage { $form->setWrapperLegendMsg( 'email-legend' ); $form->loadData(); - if ( !wfRunHooks( 'EmailUserForm', array( &$form ) ) ) { + if ( !Hooks::run( 'EmailUserForm', array( &$form ) ) ) { return; } @@ -243,8 +243,8 @@ class SpecialEmailUser extends UnlistedSpecialPage { $hookErr = false; - wfRunHooks( 'UserCanSendEmail', array( &$user, &$hookErr ) ); - wfRunHooks( 'EmailUserPermissionsErrors', array( $user, $editToken, &$hookErr ) ); + Hooks::run( 'UserCanSendEmail', array( &$user, &$hookErr ) ); + Hooks::run( 'EmailUserPermissionsErrors', array( $user, $editToken, &$hookErr ) ); if ( $hookErr ) { return $hookErr; @@ -324,7 +324,7 @@ class SpecialEmailUser extends UnlistedSpecialPage { $from->name, $to->name )->inContentLanguage()->text(); $error = ''; - if ( !wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$text, &$error ) ) ) { + if ( !Hooks::run( 'EmailUser', array( &$to, &$from, &$subject, &$text, &$error ) ) ) { return $error; } @@ -367,12 +367,12 @@ class SpecialEmailUser extends UnlistedSpecialPage { if ( $data['CCMe'] && $to != $from ) { $cc_subject = $context->msg( 'emailccsubject' )->rawParams( $target->getName(), $subject )->text(); - wfRunHooks( 'EmailUserCC', array( &$from, &$from, &$cc_subject, &$text ) ); + Hooks::run( 'EmailUserCC', array( &$from, &$from, &$cc_subject, &$text ) ); $ccStatus = UserMailer::send( $from, $from, $cc_subject, $text ); $status->merge( $ccStatus ); } - wfRunHooks( 'EmailUserComplete', array( $to, $from, $subject, $text ) ); + Hooks::run( 'EmailUserComplete', array( $to, $from, $subject, $text ) ); return $status; } diff --git a/includes/specials/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php index 62f957fc..b7582e6c 100644 --- a/includes/specials/SpecialExpandTemplates.php +++ b/includes/specials/SpecialExpandTemplates.php @@ -77,7 +77,7 @@ class SpecialExpandTemplates extends SpecialPage { $options->setMaxIncludeSize( self::MAX_INCLUDE_SIZE ); if ( $this->generateXML ) { - $wgParser->startExternalParse( $title, $options, OT_PREPROCESS ); + $wgParser->startExternalParse( $title, $options, Parser::OT_PREPROCESS ); $dom = $wgParser->preprocessToDom( $input ); if ( method_exists( $dom, 'saveXML' ) ) { @@ -154,7 +154,7 @@ class SpecialExpandTemplates extends SpecialPage { 'contexttitle', 60, $title, - array( 'autofocus' => true ) + array( 'autofocus' => '', 'class' => 'mw-ui-input-inline' ) ) . '</p>'; $form .= '<p>' . Xml::label( $this->msg( 'expand_templates_input' )->text(), @@ -278,6 +278,7 @@ class SpecialExpandTemplates extends SpecialPage { ) ) ); $out->addParserOutputContent( $pout ); $out->addHTML( Html::closeElement( 'div' ) ); + $out->setCategoryLinks( $pout->getCategories() ); } protected function getGroupName() { diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php index 38c52a01..c30d962a 100644 --- a/includes/specials/SpecialExport.php +++ b/includes/specials/SpecialExport.php @@ -182,13 +182,20 @@ class SpecialExport extends SpecialPage { $out = $this->getOutput(); $out->addWikiMsg( 'exporttext' ); + if ( $page == '' ) { + $categoryName = $request->getText( 'catname' ); + } else { + $categoryName = ''; + } + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ) ) ); $form .= Xml::inputLabel( $this->msg( 'export-addcattext' )->text(), 'catname', 'catname', - 40 + 40, + $categoryName ) . ' '; $form .= Xml::submitButton( $this->msg( 'export-addcat' )->text(), diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php index fc26c903..da79bb81 100644 --- a/includes/specials/SpecialFileDuplicateSearch.php +++ b/includes/specials/SpecialFileDuplicateSearch.php @@ -110,25 +110,31 @@ class FileDuplicateSearchPage extends QueryPage { $out = $this->getOutput(); # Create the input form - $out->addHTML( - Html::openElement( - 'form', - array( 'id' => 'fileduplicatesearch', 'method' => 'get', 'action' => wfScript() ) - ) . "\n" . - Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . "\n" . - Html::openElement( 'fieldset' ) . "\n" . - Html::element( 'legend', null, $this->msg( 'fileduplicatesearch-legend' )->text() ) . "\n" . - Xml::inputLabel( - $this->msg( 'fileduplicatesearch-filename' )->text(), - 'filename', - 'filename', - 50, - $this->filename - ) . "\n" . - Xml::submitButton( $this->msg( 'fileduplicatesearch-submit' )->text() ) . "\n" . - Html::closeElement( 'fieldset' ) . "\n" . - Html::closeElement( 'form' ) + $formFields = array( + 'filename' => array( + 'type' => 'text', + 'name' => 'filename', + 'label-message' => 'fileduplicatesearch-filename', + 'id' => 'filename', + 'size' => 50, + 'value' => $this->filename, + 'cssclass' => 'mw-ui-input-inline' + ), + ); + $hiddenFields = array( + 'title' => $this->getPageTitle()->getPrefixedDBKey(), ); + $htmlForm = HTMLForm::factory( 'inline', $formFields, $this->getContext() ); + $htmlForm->addHiddenFields( $hiddenFields ); + $htmlForm->setAction( wfScript() ); + $htmlForm->setMethod( 'get' ); + $htmlForm->setSubmitProgressive(); + $htmlForm->setSubmitTextMsg( $this->msg( 'fileduplicatesearch-submit' ) ); + $htmlForm->setWrapperLegendMsg( 'fileduplicatesearch-legend' ); + + // The form should be visible always, even if it was submitted (e.g. to perform another action). + // To bypass the callback validation of HTMLForm, use prepareForm() and displayForm(). + $htmlForm->prepareForm()->displayForm( false ); if ( $this->file ) { $this->hash = $this->file->getSha1(); @@ -196,7 +202,7 @@ class FileDuplicateSearchPage extends QueryPage { * * @param Skin $skin * @param File $result - * @return string + * @return string HTML */ function formatResult( $skin, $result ) { global $wgContLang; @@ -204,15 +210,14 @@ class FileDuplicateSearchPage extends QueryPage { $nt = $result->getTitle(); $text = $wgContLang->convert( $nt->getText() ); $plink = Linker::link( - Title::newFromText( $nt->getPrefixedText() ), - $text + $nt, + htmlspecialchars( $text ) ); $userText = $result->getUser( 'text' ); if ( $result->isLocal() ) { $userId = $result->getUser( 'id' ); $user = Linker::userLink( $userId, $userText ); - $user .= $this->getContext()->msg( 'word-separator' )->plain(); $user .= '<span style="white-space: nowrap;">'; $user .= Linker::userToolLinks( $userId, $userText ); $user .= '</span>'; @@ -220,7 +225,8 @@ class FileDuplicateSearchPage extends QueryPage { $user = htmlspecialchars( $userText ); } - $time = $this->getLanguage()->userTimeAndDate( $result->getTimestamp(), $this->getUser() ); + $time = htmlspecialchars( $this->getLanguage()->userTimeAndDate( + $result->getTimestamp(), $this->getUser() ) ); return "$plink . . $user . . $time"; } diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php index 5860f636..93232117 100644 --- a/includes/specials/SpecialFilepath.php +++ b/includes/specials/SpecialFilepath.php @@ -2,7 +2,6 @@ /** * Implements Special:Filepath * - * @section LICENSE * 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 diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php index eab4784c..af869647 100644 --- a/includes/specials/SpecialImport.php +++ b/includes/specials/SpecialImport.php @@ -30,6 +30,7 @@ * @ingroup SpecialPage */ class SpecialImport extends SpecialPage { + private $sourceName = false; private $interwiki = false; private $subproject; private $fullInterwikiPrefix; @@ -46,17 +47,20 @@ class SpecialImport extends SpecialPage { */ public function __construct() { parent::__construct( 'Import', 'import' ); - $this->namespace = $this->getConfig()->get( 'ImportTargetNamespace' ); } /** * Execute * @param string|null $par + * @throws PermissionsError + * @throws ReadOnlyError */ function execute( $par ) { $this->setHeaders(); $this->outputHeader(); + $this->namespace = $this->getConfig()->get( 'ImportTargetNamespace' ); + $this->getOutput()->addModules( 'mediawiki.special.import' ); $user = $this->getUser(); @@ -98,7 +102,7 @@ class SpecialImport extends SpecialPage { $isUpload = false; $request = $this->getRequest(); $this->namespace = $request->getIntOrNull( 'namespace' ); - $sourceName = $request->getVal( "source" ); + $this->sourceName = $request->getVal( "source" ); $this->logcomment = $request->getText( 'log-comment' ); $this->pageLinkDepth = $this->getConfig()->get( 'ExportMaxLinkDepth' ) == 0 @@ -109,14 +113,14 @@ class SpecialImport extends SpecialPage { $user = $this->getUser(); if ( !$user->matchEditToken( $request->getVal( 'editToken' ) ) ) { $source = Status::newFatal( 'import-token-mismatch' ); - } elseif ( $sourceName == 'upload' ) { + } elseif ( $this->sourceName == 'upload' ) { $isUpload = true; if ( $user->isAllowed( 'importupload' ) ) { $source = ImportStreamSource::newFromUpload( "xmlimport" ); } else { throw new PermissionsError( 'importupload' ); } - } elseif ( $sourceName == "interwiki" ) { + } elseif ( $this->sourceName == "interwiki" ) { if ( !$user->isAllowed( 'import' ) ) { throw new PermissionsError( 'import' ); } @@ -156,7 +160,7 @@ class SpecialImport extends SpecialPage { array( 'importfailed', $source->getWikiText() ) ); } else { - $importer = new WikiImporter( $source->value ); + $importer = new WikiImporter( $source->value, $this->getConfig() ); if ( !is_null( $this->namespace ) ) { $importer->setTargetNamespace( $this->namespace ); } @@ -190,7 +194,7 @@ class SpecialImport extends SpecialPage { $reporter->open(); try { $importer->doImport(); - } catch ( MWException $e ) { + } catch ( Exception $e ) { $exception = $e; } $result = $reporter->close(); @@ -250,7 +254,8 @@ class SpecialImport extends SpecialPage { Xml::label( $this->msg( 'import-comment' )->text(), 'mw-import-comment' ) . "</td> <td class='mw-input'>" . - Xml::input( 'log-comment', 50, '', + Xml::input( 'log-comment', 50, + ( $this->sourceName == 'upload' ? $this->logcomment : '' ), array( 'id' => 'mw-import-comment', 'type' => 'text' ) ) . ' ' . "</td> </tr> @@ -430,7 +435,8 @@ class SpecialImport extends SpecialPage { Xml::label( $this->msg( 'import-comment' )->text(), 'mw-interwiki-comment' ) . "</td> <td class='mw-input'>" . - Xml::input( 'log-comment', 50, '', + Xml::input( 'log-comment', 50, + ( $this->sourceName == 'interwiki' ? $this->logcomment : '' ), array( 'id' => 'mw-interwiki-comment', 'type' => 'text' ) ) . ' ' . "</td> </tr> @@ -515,13 +521,14 @@ class ImportReporter extends ContextSource { /** * @param Title $title - * @param Title $origTitle + * @param ForeignTitle $foreignTitle * @param int $revisionCount * @param int $successCount * @param array $pageInfo * @return void */ - function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) { + function reportPage( $title, $foreignTitle, $revisionCount, + $successCount, $pageInfo ) { $args = func_get_args(); call_user_func_array( $this->mOriginalPageOutCallback, $args ); @@ -539,7 +546,6 @@ class ImportReporter extends ContextSource { "</li>\n" ); - $log = new LogPage( 'import' ); if ( $this->mIsUpload ) { $detail = $this->msg( 'import-logentry-upload-detail' )->numParams( $successCount )->inContentLanguage()->text(); @@ -547,19 +553,26 @@ class ImportReporter extends ContextSource { $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason; } - $log->addEntry( 'upload', $title, $detail, array(), $this->getUser() ); + $action = 'upload'; } else { $interwiki = '[[:' . $this->mInterwiki . ':' . - $origTitle->getPrefixedText() . ']]'; + $foreignTitle->getFullText() . ']]'; $detail = $this->msg( 'import-logentry-interwiki-detail' )->numParams( $successCount )->params( $interwiki )->inContentLanguage()->text(); if ( $this->reason ) { $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason; } - $log->addEntry( 'interwiki', $title, $detail, array(), $this->getUser() ); + $action = 'interwiki'; } + $logEntry = new ManualLogEntry( 'import', $action ); + $logEntry->setTarget( $title ); + $logEntry->setComment( $detail ); + $logEntry->setPerformer( $this->getUser() ); + $logid = $logEntry->insert(); + $logEntry->publish( $logid ); + $comment = $detail; // quick $dbw = wfGetDB( DB_MASTER ); $latest = $title->getLatestRevID(); @@ -576,7 +589,7 @@ class ImportReporter extends ContextSource { $page = WikiPage::factory( $title ); # Update page record $page->updateRevisionOn( $dbw, $nullRevision ); - wfRunHooks( + Hooks::run( 'NewRevisionFromEditComplete', array( $page, $nullRevision, $latest, $this->getUser() ) ); diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php index 7d745a50..ecb166a4 100644 --- a/includes/specials/SpecialJavaScriptTest.php +++ b/includes/specials/SpecialJavaScriptTest.php @@ -150,12 +150,11 @@ class SpecialJavaScriptTest extends SpecialPage { */ private function viewQUnit() { $out = $this->getOutput(); - $testConfig = $this->getConfig()->get( 'JavaScriptTestConfig' ); $modules = $out->getResourceLoader()->getTestModuleNames( 'qunit' ); $summary = $this->msg( 'javascripttest-qunit-intro' ) - ->params( $testConfig['qunit']['documentation'] ) + ->params( 'https://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing' ) ->parseAsBlock(); $baseHtml = <<<HTML @@ -164,13 +163,6 @@ class SpecialJavaScriptTest extends SpecialPage { </div> HTML; - // Used in ./tests/qunit/data/testrunner.js, see also documentation of - // $wgJavaScriptTestConfig in DefaultSettings.php - $out->addJsConfigVars( - 'QUnitTestSwarmInjectJSPath', - $testConfig['qunit']['testswarm-injectjs'] - ); - $out->addHtml( $this->wrapSummaryHtml( $summary ) . $baseHtml ); // The testrunner configures QUnit and essentially depends on it. However, test suites @@ -200,7 +192,6 @@ HTML; */ private function exportQUnit() { $out = $this->getOutput(); - $out->disable(); $rl = $out->getResourceLoader(); @@ -251,9 +242,13 @@ HTML; 'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false', ) ); - $styles = $out->makeResourceLoaderLink( 'jquery.qunit', ResourceLoaderModule::TYPE_STYLES, false ); + $styles = $out->makeResourceLoaderLink( + 'jquery.qunit', ResourceLoaderModule::TYPE_STYLES, false + ); // Use 'raw' since this is a plain HTML page without ResourceLoader - $scripts = $out->makeResourceLoaderLink( 'jquery.qunit', ResourceLoaderModule::TYPE_SCRIPTS, false, array( 'raw' => 'true' ) ); + $scripts = $out->makeResourceLoaderLink( + 'jquery.qunit', ResourceLoaderModule::TYPE_SCRIPTS, false, array( 'raw' => 'true' ) + ); $head = trim( $styles['html'] . $scripts['html'] ); $html = <<<HTML @@ -269,18 +264,12 @@ HTML; } /** - * Return an array of subpages beginning with $search that this special page will accept. + * Return an array of subpages that this special page will accept. * - * @param string $search Prefix to search for - * @param int $limit Maximum number of results to return - * @return string[] Matching subpages + * @return string[] subpages */ - public function prefixSearchSubpages( $search, $limit = 10 ) { - return self::prefixSearchArray( - $search, - $limit, - self::$frameworks - ); + public function getSubpagesForPrefixSearch() { + return self::$frameworks; } protected function getGroupName() { diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php index 371469bb..75ff8f30 100644 --- a/includes/specials/SpecialLinkSearch.php +++ b/includes/specials/SpecialLinkSearch.php @@ -27,6 +27,8 @@ * @ingroup SpecialPage */ class LinkSearchPage extends QueryPage { + /** @var array|bool */ + private $mungedQuery = false; /** * @var PageLinkRenderer @@ -66,8 +68,9 @@ class LinkSearchPage extends QueryPage { * This allows for dependency injection even though we don't control object creation. */ private function initServices() { + global $wgLanguageCode; if ( !$this->linkRenderer ) { - $lang = $this->getContext()->getLanguage(); + $lang = Language::factory( $wgLanguageCode ); $titleFormatter = new MediaWikiTitleCodec( $lang, GenderCache::singleton() ); $this->linkRenderer = new MediaWikiPageLinkRenderer( $titleFormatter ); } @@ -88,7 +91,7 @@ class LinkSearchPage extends QueryPage { $request = $this->getRequest(); $target = $request->getVal( 'target', $par ); - $namespace = $request->getIntorNull( 'namespace', null ); + $namespace = $request->getIntOrNull( 'namespace', null ); $protocols_list = array(); foreach ( $this->getConfig()->get( 'UrlProtocols' ) as $prot ) { @@ -162,7 +165,7 @@ class LinkSearchPage extends QueryPage { 'namespace' => $namespace, 'protocol' => $protocol ) ); parent::execute( $par ); - if ( $this->mMungedQuery === false ) { + if ( $this->mungedQuery === false ) { $out->addWikiMsg( 'linksearch-error' ); } } @@ -221,13 +224,13 @@ class LinkSearchPage extends QueryPage { $dbr = wfGetDB( DB_SLAVE ); // strip everything past first wildcard, so that // index-based-only lookup would be done - list( $this->mMungedQuery, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt ); - if ( $this->mMungedQuery === false ) { + list( $this->mungedQuery, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt ); + if ( $this->mungedQuery === false ) { // Invalid query; return no results return array( 'tables' => 'page', 'fields' => 'page_id', 'conds' => '0=1' ); } - $stripped = LinkFilter::keepOneWildcard( $this->mMungedQuery ); + $stripped = LinkFilter::keepOneWildcard( $this->mungedQuery ); $like = $dbr->buildLike( $stripped ); $retval = array( 'tables' => array( 'page', 'externallinks' ), @@ -252,6 +255,25 @@ class LinkSearchPage extends QueryPage { } /** + * Pre-fill the link cache + * + * @param IDatabase $db + * @param ResultWrapper $res + */ + function preprocessResults( $db, $res ) { + if ( $res->numRows() > 0 ) { + $linkBatch = new LinkBatch(); + + foreach ( $res as $row ) { + $linkBatch->add( $row->namespace, $row->title ); + } + + $res->seek( 0 ); + $linkBatch->execute(); + } + } + + /** * @param Skin $skin * @param object $result Result row * @return string @@ -267,24 +289,6 @@ class LinkSearchPage extends QueryPage { } /** - * Override to check query validity. - * - * @param mixed $offset Numerical offset or false for no offset - * @param mixed $limit Numerical limit or false for no limit - */ - function doQuery( $offset = false, $limit = false ) { - list( $this->mMungedQuery, ) = LinkSearchPage::mungeQuery( $this->mQuery, $this->mProt ); - if ( $this->mMungedQuery === false ) { - $this->getOutput()->addWikiMsg( 'linksearch-error' ); - } else { - // For debugging - // Generates invalid xhtml with patterns that contain -- - //$this->getOutput()->addHTML( "\n<!-- " . htmlspecialchars( $this->mMungedQuery ) . " -->\n" ); - parent::doQuery( $offset, $limit ); - } - } - - /** * Override to squash the ORDER BY. * We do a truncated index search, so the optimizer won't trust * it as good enough for optimizing sort. The implicit ordering diff --git a/includes/specials/SpecialListDuplicatedFiles.php b/includes/specials/SpecialListDuplicatedFiles.php index 26672706..1e3dff6f 100644 --- a/includes/specials/SpecialListDuplicatedFiles.php +++ b/includes/specials/SpecialListDuplicatedFiles.php @@ -71,7 +71,7 @@ class ListDuplicatedFilesPage extends QueryPage { /** * Pre-fill the link cache * - * @param DatabaseBase $db + * @param IDatabase $db * @param ResultWrapper $res */ function preprocessResults( $db, $res ) { diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php index 04a83c8f..d4b45fb3 100644 --- a/includes/specials/SpecialListfiles.php +++ b/includes/specials/SpecialListfiles.php @@ -87,7 +87,7 @@ class ImageListPager extends TablePager { $this->mIncluding = $including; $this->mShowAll = $showAll; - if ( $userName ) { + if ( $userName !== null && $userName !== '' ) { $nt = Title::newFromText( $userName, NS_USER ); if ( !is_null( $nt ) ) { $this->mUserName = $nt->getText(); @@ -203,7 +203,9 @@ class ImageListPager extends TablePager { } else { return false; } - } elseif ( $this->getConfig()->get( 'MiserMode' ) && $this->mShowAll /* && mUserName === null */ ) { + } elseif ( $this->getConfig()->get( 'MiserMode' ) + && $this->mShowAll /* && mUserName === null */ + ) { // no oi_timestamp index, so only alphabetical sorting in this case. if ( $field === 'img_name' ) { return true; @@ -300,6 +302,7 @@ class ImageListPager extends TablePager { * @param int $limit * @param bool $asc * @return array + * @throws MWException */ function reallyDoQuery( $offset, $limit, $asc ) { $prevTableName = $this->mTableName; @@ -422,7 +425,7 @@ class ImageListPager extends TablePager { function formatValue( $field, $value ) { switch ( $field ) { case 'thumb': - $opt = array( 'time' => $this->mCurrentRow->img_timestamp ); + $opt = array( 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ); $file = RepoGroup::singleton()->getLocalRepo()->findFile( $value, $opt ); // If statement for paranoia if ( $file ) { @@ -519,6 +522,7 @@ class ImageListPager extends TablePager { ); } + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); $fields['user'] = array( 'type' => 'text', 'name' => 'user', @@ -527,6 +531,7 @@ class ImageListPager extends TablePager { 'default' => $this->mUserName, 'size' => '40', 'maxlength' => '255', + 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest ); $fields['ilshowall'] = array( @@ -541,6 +546,7 @@ class ImageListPager extends TablePager { unset( $query['title'] ); unset( $query['limit'] ); unset( $query['ilsearch'] ); + unset( $query['ilshowall'] ); unset( $query['user'] ); $form = new HTMLForm( $fields, $this->getContext() ); diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php index 5bae28f0..828a93b9 100644 --- a/includes/specials/SpecialListgrouprights.php +++ b/includes/specials/SpecialListgrouprights.php @@ -86,13 +86,14 @@ class SpecialListGroupRights extends SpecialPage { $grouppageLocalized = !$msg->isBlank() ? $msg->text() : MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname; + $grouppageLocalizedTitle = Title::newFromText( $grouppageLocalized ); - if ( $group == '*' ) { - // Do not make a link for the generic * group + if ( $group == '*' || !$grouppageLocalizedTitle ) { + // Do not make a link for the generic * group or group with invalid group page $grouppage = htmlspecialchars( $groupnameLocalized ); } else { $grouppage = Linker::link( - Title::newFromText( $grouppageLocalized ), + $grouppageLocalizedTitle, htmlspecialchars( $groupnameLocalized ) ); } @@ -235,20 +236,18 @@ class SpecialListGroupRights extends SpecialPage { foreach ( $permissions as $permission => $granted ) { //show as granted only if it isn't revoked to prevent duplicate display of permissions if ( $granted && ( !isset( $revoke[$permission] ) || !$revoke[$permission] ) ) { - $description = $this->msg( 'listgrouprights-right-display', + $r[] = $this->msg( 'listgrouprights-right-display', User::getRightDescription( $permission ), '<span class="mw-listgrouprights-right-name">' . $permission . '</span>' )->parse(); - $r[] = $description; } } foreach ( $revoke as $permission => $revoked ) { if ( $revoked ) { - $description = $this->msg( 'listgrouprights-right-revoked', + $r[] = $this->msg( 'listgrouprights-right-revoked', User::getRightDescription( $permission ), '<span class="mw-listgrouprights-right-name">' . $permission . '</span>' )->parse(); - $r[] = $description; } } @@ -257,51 +256,28 @@ class SpecialListGroupRights extends SpecialPage { $lang = $this->getLanguage(); $allGroups = User::getAllGroups(); - if ( $add === true ) { - $r[] = $this->msg( 'listgrouprights-addgroup-all' )->escaped(); - } elseif ( is_array( $add ) ) { - $add = array_intersect( array_values( array_unique( $add ) ), $allGroups ); - if ( count( $add ) ) { - $r[] = $this->msg( 'listgrouprights-addgroup', - $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ), - count( $add ) - )->parse(); - } - } - - if ( $remove === true ) { - $r[] = $this->msg( 'listgrouprights-removegroup-all' )->escaped(); - } elseif ( is_array( $remove ) ) { - $remove = array_intersect( array_values( array_unique( $remove ) ), $allGroups ); - if ( count( $remove ) ) { - $r[] = $this->msg( 'listgrouprights-removegroup', - $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ), - count( $remove ) - )->parse(); - } - } - - if ( $addSelf === true ) { - $r[] = $this->msg( 'listgrouprights-addgroup-self-all' )->escaped(); - } elseif ( is_array( $addSelf ) ) { - $addSelf = array_intersect( array_values( array_unique( $addSelf ) ), $allGroups ); - if ( count( $addSelf ) ) { - $r[] = $this->msg( 'listgrouprights-addgroup-self', - $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $addSelf ) ), - count( $addSelf ) - )->parse(); - } - } + $changeGroups = array( + 'addgroup' => $add, + 'removegroup' => $remove, + 'addgroup-self' => $addSelf, + 'removegroup-self' => $removeSelf + ); - if ( $removeSelf === true ) { - $r[] = $this->msg( 'listgrouprights-removegroup-self-all' )->parse(); - } elseif ( is_array( $removeSelf ) ) { - $removeSelf = array_intersect( array_values( array_unique( $removeSelf ) ), $allGroups ); - if ( count( $removeSelf ) ) { - $r[] = $this->msg( 'listgrouprights-removegroup-self', - $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $removeSelf ) ), - count( $removeSelf ) - )->parse(); + foreach ( $changeGroups as $messageKey => $changeGroup ) { + if ( $changeGroup === true ) { + // For grep: listgrouprights-addgroup-all, listgrouprights-removegroup-all, + // listgrouprights-addgroup-self-all, listgrouprights-removegroup-self-all + $r[] = $this->msg( 'listgrouprights-' . $messageKey . '-all' )->escaped(); + } elseif ( is_array( $changeGroup ) ) { + $changeGroup = array_intersect( array_values( array_unique( $changeGroup ) ), $allGroups ); + if ( count( $changeGroup ) ) { + // For grep: listgrouprights-addgroup, listgrouprights-removegroup, + // listgrouprights-addgroup-self, listgrouprights-removegroup-self + $r[] = $this->msg( 'listgrouprights-' . $messageKey, + $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $changeGroup ) ), + count( $changeGroup ) + )->parse(); + } } } diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php index de05be41..2df48347 100644 --- a/includes/specials/SpecialListredirects.php +++ b/includes/specials/SpecialListredirects.php @@ -72,7 +72,7 @@ class ListredirectsPage extends QueryPage { /** * Cache page existence for performance * - * @param DatabaseBase $db + * @param IDatabase $db * @param ResultWrapper $res */ function preprocessResults( $db, $res ) { diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php index dad9074d..56c4eb50 100644 --- a/includes/specials/SpecialListusers.php +++ b/includes/specials/SpecialListusers.php @@ -35,6 +35,11 @@ class UsersPager extends AlphabeticPager { /** + * @var array A array with user ids as key and a array of groups as value + */ + protected $userGroupCache; + + /** * @param IContextSource $context * @param array $par (Default null) * @param bool $including Whether this page is being transcluded in @@ -132,8 +137,6 @@ class UsersPager extends AlphabeticPager { 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name', 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)', 'edits' => 'MAX(user_editcount)', - 'numgroups' => 'COUNT(ug_group)', - 'singlegroup' => 'MAX(ug_group)', // the usergroup if there is only one 'creation' => 'MIN(user_registration)', 'ipb_deleted' => 'MAX(ipb_deleted)' // block/hide status ), @@ -150,7 +153,7 @@ class UsersPager extends AlphabeticPager { 'conds' => $conds ); - wfRunHooks( 'SpecialListusersQueryInfo', array( $this, &$query ) ); + Hooks::run( 'SpecialListusersQueryInfo', array( $this, &$query ) ); return $query; } @@ -176,7 +179,7 @@ class UsersPager extends AlphabeticPager { $lang = $this->getLanguage(); $groups = ''; - $groups_list = self::getGroups( $row->user_id ); + $groups_list = self::getGroups( intval( $row->user_id ), $this->userGroupCache ); if ( !$this->including && count( $groups_list ) > 0 ) { $list = array(); @@ -211,18 +214,45 @@ class UsersPager extends AlphabeticPager { ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : ''; - wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) ); + Hooks::run( 'SpecialListusersFormatRow', array( &$item, $row ) ); return Html::rawElement( 'li', array(), "{$item}{$edits}{$created}{$blocked}" ); } function doBatchLookups() { $batch = new LinkBatch(); + $userIds = array(); # Give some pointers to make user links foreach ( $this->mResult as $row ) { $batch->add( NS_USER, $row->user_name ); $batch->add( NS_USER_TALK, $row->user_name ); + $userIds[] = $row->user_id; + } + + // Lookup groups for all the users + $dbr = wfGetDB( DB_SLAVE ); + $groupRes = $dbr->select( + 'user_groups', + array( 'ug_user', 'ug_group' ), + array( 'ug_user' => $userIds ), + __METHOD__ + ); + $cache = array(); + $groups = array(); + foreach ( $groupRes as $row ) { + $cache[intval( $row->ug_user )][] = $row->ug_group; + $groups[$row->ug_group] = true; } + $this->userGroupCache = $cache; + + // Add page of groups to link batch + foreach ( $groups as $group => $unused ) { + $groupPage = User::getGroupPage( $group ); + if ( $groupPage ) { + $batch->addObj( $groupPage ); + } + } + $batch->execute(); $this->mResult->rewind(); } @@ -284,12 +314,12 @@ class UsersPager extends AlphabeticPager { ); $out .= '<br />'; - wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) ); + Hooks::run( 'SpecialListusersHeaderForm', array( $this, &$out ) ); # Submit button and form bottom $out .= Html::hidden( 'limit', $this->mLimit ); $out .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ); - wfRunHooks( 'SpecialListusersHeader', array( $this, &$out ) ); + Hooks::run( 'SpecialListusersHeader', array( $this, &$out ) ); $out .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ); @@ -322,7 +352,7 @@ class UsersPager extends AlphabeticPager { if ( $this->requestedUser != '' ) { $query['username'] = $this->requestedUser; } - wfRunHooks( 'SpecialListusersDefaultQuery', array( $this, &$query ) ); + Hooks::run( 'SpecialListusersDefaultQuery', array( $this, &$query ) ); return $query; } @@ -331,11 +361,17 @@ class UsersPager extends AlphabeticPager { * Get a list of groups the specified user belongs to * * @param int $uid User id + * @param array|null $cache * @return array */ - protected static function getGroups( $uid ) { - $user = User::newFromId( $uid ); - $groups = array_diff( $user->getEffectiveGroups(), User::getImplicitGroups() ); + protected static function getGroups( $uid, $cache = null ) { + if ( $cache === null ) { + $user = User::newFromId( $uid ); + $effectiveGroups = $user->getEffectiveGroups(); + } else { + $effectiveGroups = isset( $cache[$uid] ) ? $cache[$uid] : array(); + } + $groups = array_diff( $effectiveGroups, User::getImplicitGroups() ); return $groups; } @@ -350,7 +386,7 @@ class UsersPager extends AlphabeticPager { protected static function buildGroupLink( $group, $username ) { return User::makeGroupLinkHtml( $group, - htmlspecialchars( User::getGroupMember( $group, $username ) ) + User::getGroupMember( $group, $username ) ); } } @@ -397,15 +433,12 @@ class SpecialListUsers extends IncludableSpecialPage { } /** - * Return an array of subpages beginning with $search that this special page will accept. + * Return an array of subpages that this special page will accept. * - * @param string $search Prefix to search for - * @param int $limit Maximum number of results to return - * @return string[] Matching subpages + * @return string[] subpages */ - public function prefixSearchSubpages( $search, $limit = 10 ) { - $subpages = User::getAllGroups(); - return self::prefixSearchArray( $search, $limit, $subpages ); + public function getSubpagesForPrefixSearch() { + return User::getAllGroups(); } protected function getGroupName() { diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php index dc33801d..e44ce5f5 100644 --- a/includes/specials/SpecialLog.php +++ b/includes/specials/SpecialLog.php @@ -29,17 +29,6 @@ * @ingroup SpecialPage */ class SpecialLog extends SpecialPage { - /** - * List log type for which the target is a user - * Thus if the given target is in NS_MAIN we can alter it to be an NS_USER - * Title user instead. - */ - private $typeOnUser = array( - 'block', - 'newusers', - 'rights', - ); - public function __construct() { parent::__construct( 'Log' ); } @@ -47,6 +36,7 @@ class SpecialLog extends SpecialPage { public function execute( $par ) { $this->setHeaders(); $this->outputHeader(); + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); $opts = new FormOptions; $opts->add( 'type', '' ); @@ -94,13 +84,18 @@ class SpecialLog extends SpecialPage { } elseif ( $offender && IP::isIPAddress( $offender->getName() ) ) { $qc = array( 'ls_field' => 'target_author_ip', 'ls_value' => $offender->getName() ); } + } else { + // Allow extensions to add relations to their search types + Hooks::run( + 'SpecialLogAddLogSearchRelations', + array( $opts->getValue( 'type' ), $this->getRequest(), &$qc ) + ); } # Some log types are only for a 'User:' title but we might have been given # only the username instead of the full title 'User:username'. This part try # to lookup for a user by that name and eventually fix user input. See bug 1697. - wfRunHooks( 'GetLogTypesOnUser', array( &$this->typeOnUser ) ); - if ( in_array( $opts->getValue( 'type' ), $this->typeOnUser ) ) { + if ( in_array( $opts->getValue( 'type' ), self::getLogTypesOnUser() ) ) { # ok we have a type of log which expect a user title. $target = Title::newFromText( $opts->getValue( 'page' ) ); if ( $target && $target->getNamespace() === NS_MAIN ) { @@ -115,17 +110,38 @@ class SpecialLog extends SpecialPage { } /** - * Return an array of subpages beginning with $search that this special page will accept. + * List log type for which the target is a user + * Thus if the given target is in NS_MAIN we can alter it to be an NS_USER + * Title user instead. * - * @param string $search Prefix to search for - * @param int $limit Maximum number of results to return - * @return string[] Matching subpages + * @since 1.25 + * @return array */ - public function prefixSearchSubpages( $search, $limit = 10 ) { + public static function getLogTypesOnUser() { + static $types = null; + if ( $types !== null ) { + return $types; + } + $types = array( + 'block', + 'newusers', + 'rights', + ); + + Hooks::run( 'GetLogTypesOnUser', array( &$types ) ); + return $types; + } + + /** + * Return an array of subpages that this special page will accept. + * + * @return string[] subpages + */ + public function getSubpagesForPrefixSearch() { $subpages = $this->getConfig()->get( 'LogTypes' ); $subpages[] = 'all'; sort( $subpages ); - return self::prefixSearchArray( $search, $limit, $subpages ); + return $subpages; } private function parseParams( FormOptions $opts, $par ) { @@ -149,7 +165,7 @@ class SpecialLog extends SpecialPage { $loglist = new LogEventsList( $this->getContext(), null, - LogEventsList::USE_REVDEL_CHECKBOXES + LogEventsList::USE_CHECKBOXES ); $pager = new LogPager( $loglist, @@ -187,7 +203,7 @@ class SpecialLog extends SpecialPage { if ( $logBody ) { $this->getOutput()->addHTML( $pager->getNavigationBar() . - $this->getRevisionButton( + $this->getActionButtons( $loglist->beginLogEventsList() . $logBody . $loglist->endLogEventsList() @@ -199,30 +215,50 @@ class SpecialLog extends SpecialPage { } } - private function getRevisionButton( $formcontents ) { - # If the user doesn't have the ability to delete log entries, - # don't bother showing them the button. - if ( !$this->getUser()->isAllowedAll( 'deletedhistory', 'deletelogentry' ) ) { + private function getActionButtons( $formcontents ) { + $user = $this->getUser(); + $canRevDelete = $user->isAllowedAll( 'deletedhistory', 'deletelogentry' ); + $showTagEditUI = ChangeTags::showTagEditingUI( $user ); + # If the user doesn't have the ability to delete log entries nor edit tags, + # don't bother showing them the button(s). + if ( !$canRevDelete && !$showTagEditUI ) { return $formcontents; } - # Show button to hide log entries + # Show button to hide log entries and/or edit change tags $s = Html::openElement( 'form', array( 'action' => wfScript(), 'id' => 'mw-log-deleterevision-submit' ) ) . "\n"; - $s .= Html::hidden( 'title', SpecialPage::getTitleFor( 'Revisiondelete' ) ) . "\n"; - $s .= Html::hidden( 'target', SpecialPage::getTitleFor( 'Log' ) ) . "\n"; + $s .= Html::hidden( 'action', 'historysubmit' ) . "\n"; $s .= Html::hidden( 'type', 'logging' ) . "\n"; - $button = Html::element( - 'button', - array( - 'type' => 'submit', - 'class' => "deleterevision-log-submit mw-log-deleterevision-button" - ), - $this->msg( 'showhideselectedlogentries' )->text() - ) . "\n"; - $s .= $button . $formcontents . $button; + + $buttons = ''; + if ( $canRevDelete ) { + $buttons .= Html::element( + 'button', + array( + 'type' => 'submit', + 'name' => 'revisiondelete', + 'value' => '1', + 'class' => "deleterevision-log-submit mw-log-deleterevision-button" + ), + $this->msg( 'showhideselectedlogentries' )->text() + ) . "\n"; + } + if ( $showTagEditUI ) { + $buttons .= Html::element( + 'button', + array( + 'type' => 'submit', + 'name' => 'editchangetags', + 'value' => '1', + 'class' => "editchangetags-log-submit mw-log-editchangetags-button" + ), + $this->msg( 'log-edit-tags' )->text() + ) . "\n"; + } + $s .= $buttons . $formcontents . $buttons; $s .= Html::closeElement( 'form' ); return $s; @@ -235,8 +271,9 @@ class SpecialLog extends SpecialPage { */ protected function addHeader( $type ) { $page = new LogPage( $type ); - $this->getOutput()->setPageTitle( $page->getName()->text() ); - $this->getOutput()->addHTML( $page->getDescription()->parseAsBlock() ); + $this->getOutput()->setPageTitle( $page->getName() ); + $this->getOutput()->addHTML( $page->getDescription() + ->setContext( $this->getContext() )->parseAsBlock() ); } protected function getGroupName() { diff --git a/includes/specials/SpecialLonelypages.php b/includes/specials/SpecialLonelypages.php index f533234f..c072491b 100644 --- a/includes/specials/SpecialLonelypages.php +++ b/includes/specials/SpecialLonelypages.php @@ -72,7 +72,7 @@ class LonelyPagesPage extends PageQueryPage { ); // Allow extensions to modify the query - wfRunHooks( 'LonelyPagesQuery', array( &$tables, &$conds, &$joinConds ) ); + Hooks::run( 'LonelyPagesQuery', array( &$tables, &$conds, &$joinConds ) ); return array( 'tables' => $tables, diff --git a/includes/specials/SpecialMediaStatistics.php b/includes/specials/SpecialMediaStatistics.php index 681c332f..b62de5d2 100644 --- a/includes/specials/SpecialMediaStatistics.php +++ b/includes/specials/SpecialMediaStatistics.php @@ -73,6 +73,10 @@ class MediaStatisticsPage extends QueryPage { 'namespace' => NS_MEDIA, /* needs to be something */ 'value' => '1' ), + 'conds' => array( + // WMF has a random null row in the db + 'img_media_type IS NOT NULL' + ), 'options' => array( 'GROUP BY' => array( 'img_media_type', @@ -99,7 +103,7 @@ class MediaStatisticsPage extends QueryPage { * * @param $out OutputPage * @param $skin Skin (deprecated presumably) - * @param $dbr DatabaseBase + * @param $dbr IDatabase * @param $res ResultWrapper Results from query * @param $num integer Number of results * @param $offset integer Paging offset (Should always be 0 in our case) @@ -153,7 +157,8 @@ class MediaStatisticsPage extends QueryPage { ); $row .= Html::rawElement( 'td', - array(), + // Make sure js sorts it in numeric order + array( 'data-sort-value' => $count ), $this->msg( 'mediastatistics-nfiles' ) ->numParams( $count ) /** @todo Check to be sure this really should have number formatting */ @@ -185,6 +190,9 @@ class MediaStatisticsPage extends QueryPage { if ( $decimal == 0 ) { return '0'; } + if ( $decimal >= 100 ) { + return '100'; + } $percent = sprintf( "%." . max( 0, 2 - floor( log10( $decimal ) ) ) . "f", $decimal ); // Then remove any trailing 0's return preg_replace( '/\.?0*$/', '', $percent ); @@ -302,6 +310,8 @@ class MediaStatisticsPage extends QueryPage { * * @param $skin Skin * @param $result stdObject Result row + * @return bool|string|void + * @throws MWException */ public function formatResult( $skin, $result ) { throw new MWException( "unimplemented" ); @@ -310,15 +320,15 @@ class MediaStatisticsPage extends QueryPage { /** * Initialize total values so we can figure out percentages later. * - * @param $dbr DatabaseBase + * @param $dbr IDatabase * @param $res ResultWrapper */ public function preprocessResults( $dbr, $res ) { $this->totalCount = $this->totalBytes = 0; foreach ( $res as $row ) { - list( , , $count, $bytes ) = $this->splitFakeTitle( $row->title ); - $this->totalCount += $count; - $this->totalBytes += $bytes; + $mediaStats = $this->splitFakeTitle( $row->title ); + $this->totalCount += isset( $mediaStats[2] ) ? $mediaStats[2] : 0; + $this->totalBytes += isset( $mediaStats[3] ) ? $mediaStats[3] : 0; } $res->seek( 0 ); } diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php index 43f5a1ba..1f0b6d45 100644 --- a/includes/specials/SpecialMergeHistory.php +++ b/includes/specials/SpecialMergeHistory.php @@ -159,9 +159,10 @@ class SpecialMergeHistory extends SpecialPage { } function showMergeForm() { - $this->getOutput()->addWikiMsg( 'mergehistory-header' ); + $out = $this->getOutput(); + $out->addWikiMsg( 'mergehistory-header' ); - $this->getOutput()->addHTML( + $out->addHTML( Xml::openElement( 'form', array( 'method' => 'get', 'action' => wfScript() ) ) . @@ -185,6 +186,8 @@ class SpecialMergeHistory extends SpecialPage { '</fieldset>' . '</form>' ); + + $this->addHelpLink( 'Help:Merge history' ); } private function showHistory() { @@ -469,18 +472,23 @@ class SpecialMergeHistory extends SpecialPage { return false; } # Update our logs - $log = new LogPage( 'merge' ); - $log->addEntry( - 'merge', $targetTitle, $this->mComment, - array( $destTitle->getPrefixedText(), $timestampLimit ), $this->getUser() - ); + $logEntry = new ManualLogEntry( 'merge', 'merge' ); + $logEntry->setPerformer( $this->getUser() ); + $logEntry->setComment( $this->mComment ); + $logEntry->setTarget( $targetTitle ); + $logEntry->setParameters( array( + '4::dest' => $destTitle->getPrefixedText(), + '5::mergepoint' => $timestampLimit + ) ); + $logId = $logEntry->insert(); + $logEntry->publish( $logId ); # @todo message should use redirect=no $this->getOutput()->addWikiText( $this->msg( 'mergehistory-success', $targetTitle->getPrefixedText(), $destTitle->getPrefixedText() )->numParams( $count )->text() ); - wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) ); + Hooks::run( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) ); return true; } @@ -516,7 +524,6 @@ class MergeHistoryPager extends ReverseChronologicalPager { } function getStartBody() { - wfProfileIn( __METHOD__ ); # Do a link batch query $this->mResult->seek( 0 ); $batch = new LinkBatch(); @@ -539,8 +546,6 @@ class MergeHistoryPager extends ReverseChronologicalPager { $batch->execute(); $this->mResult->seek( 0 ); - wfProfileOut( __METHOD__ ); - return ''; } diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php index 9b67f343..c70bbdba 100644 --- a/includes/specials/SpecialMostcategories.php +++ b/includes/specials/SpecialMostcategories.php @@ -65,7 +65,7 @@ class MostcategoriesPage extends QueryPage { } /** - * @param DatabaseBase $db + * @param IDatabase $db * @param ResultWrapper $res */ function preprocessResults( $db, $res ) { diff --git a/includes/specials/SpecialMostimages.php b/includes/specials/SpecialMostimages.php index 98d8da3a..36669641 100644 --- a/includes/specials/SpecialMostimages.php +++ b/includes/specials/SpecialMostimages.php @@ -25,7 +25,7 @@ */ /** - * A special page page that list most used images + * A special page that lists most used images * * @ingroup SpecialPage */ diff --git a/includes/specials/SpecialMostinterwikis.php b/includes/specials/SpecialMostinterwikis.php index 30ccbe5a..ab3d9c91 100644 --- a/includes/specials/SpecialMostinterwikis.php +++ b/includes/specials/SpecialMostinterwikis.php @@ -71,7 +71,7 @@ class MostinterwikisPage extends QueryPage { /** * Pre-fill the link cache * - * @param DatabaseBase $db + * @param IDatabase $db * @param ResultWrapper $res */ function preprocessResults( $db, $res ) { diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php index 99f0ecf5..ae0b0708 100644 --- a/includes/specials/SpecialMostlinked.php +++ b/includes/specials/SpecialMostlinked.php @@ -74,7 +74,7 @@ class MostlinkedPage extends QueryPage { /** * Pre-fill the link cache * - * @param DatabaseBase $db + * @param IDatabase $db * @param ResultWrapper $res */ function preprocessResults( $db, $res ) { diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php index f61a1158..cc718e06 100644 --- a/includes/specials/SpecialMostlinkedcategories.php +++ b/includes/specials/SpecialMostlinkedcategories.php @@ -55,7 +55,7 @@ class MostlinkedCategoriesPage extends QueryPage { /** * Fetch user page links and cache their existence * - * @param DatabaseBase $db + * @param IDatabase $db * @param ResultWrapper $res */ function preprocessResults( $db, $res ) { diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php index 8e6a596d..a924525d 100644 --- a/includes/specials/SpecialMostlinkedtemplates.php +++ b/includes/specials/SpecialMostlinkedtemplates.php @@ -75,7 +75,7 @@ class MostlinkedTemplatesPage extends QueryPage { /** * Pre-cache page existence to speed up link generation * - * @param DatabaseBase $db + * @param IDatabase $db * @param ResultWrapper $res */ public function preprocessResults( $db, $res ) { diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index ec9593f7..ae1fefea 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -140,6 +140,7 @@ class MovePageForm extends UnlistedSpecialPage { $out = $this->getOutput(); $out->setPageTitle( $this->msg( 'move-page', $this->oldTitle->getPrefixedText() ) ); $out->addModules( 'mediawiki.special.movePage' ); + $this->addHelpLink( 'Help:Moving a page' ); $newTitle = $this->newTitle; @@ -165,17 +166,7 @@ class MovePageForm extends UnlistedSpecialPage { $out->addWikiMsg( 'delete_and_move_text', $newTitle->getPrefixedText() ); $movepagebtn = $this->msg( 'delete_and_move' )->text(); $submitVar = 'wpDeleteAndMove'; - $confirm = " - <tr> - <td></td> - <td class='mw-input'>" . - Xml::checkLabel( - $this->msg( 'delete_and_move_confirm' )->text(), - 'wpConfirm', - 'wpConfirm' - ) . - "</td> - </tr>"; + $confirm = true; $err = array(); } else { if ( $this->oldTitle->getNamespace() == NS_USER && !$this->oldTitle->isSubpage() ) { @@ -310,12 +301,15 @@ class MovePageForm extends UnlistedSpecialPage { 'id' => 'movepage' ) ) . - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, $this->msg( 'move-page-legend' )->text() ) . - Xml::openElement( 'table', array( 'id' => 'mw-movepage-table' ) ) . - "<tr> + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, $this->msg( 'move-page-legend' )->text() ) . + Xml::openElement( 'table', array( 'id' => 'mw-movepage-table' ) ) + ); + + $out->addHTML( + "<tr> <td class='mw-label'>" . - $this->msg( 'movearticle' )->escaped() . + $this->msg( 'movearticle' )->escaped() . "</td> <td class='mw-input'> <strong>{$oldTitleLink}</strong> @@ -323,32 +317,32 @@ class MovePageForm extends UnlistedSpecialPage { </tr> <tr> <td class='mw-label'>" . - Xml::label( $this->msg( 'newtitle' )->text(), 'wpNewTitleMain' ) . + Xml::label( $this->msg( 'newtitle' )->text(), 'wpNewTitleMain' ) . "</td> <td class='mw-input'>" . - Html::namespaceSelector( - array( - 'selected' => $newTitle->getNamespace(), - 'exclude' => $immovableNamespaces - ), - array( 'name' => 'wpNewTitleNs', 'id' => 'wpNewTitleNs' ) - ) . - Xml::input( - 'wpNewTitleMain', - 60, - $wgContLang->recodeForEdit( $newTitle->getText() ), - array( - 'type' => 'text', - 'id' => 'wpNewTitleMain', - 'maxlength' => 255 - ) - ) . - Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) . + Html::namespaceSelector( + array( + 'selected' => $newTitle->getNamespace(), + 'exclude' => $immovableNamespaces + ), + array( 'name' => 'wpNewTitleNs', 'id' => 'wpNewTitleNs' ) + ) . + Xml::input( + 'wpNewTitleMain', + 60, + $wgContLang->recodeForEdit( $newTitle->getText() ), + array( + 'type' => 'text', + 'id' => 'wpNewTitleMain', + 'maxlength' => 255 + ) + ) . + Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) . "</td> </tr> <tr> <td class='mw-label'>" . - Xml::label( $this->msg( 'movereason' )->text(), 'wpReason' ) . + Xml::label( $this->msg( 'movereason' )->text(), 'wpReason' ) . "</td> <td class='mw-input'>" . Xml::input( 'wpReason', 60, $this->reason, array( @@ -365,12 +359,12 @@ class MovePageForm extends UnlistedSpecialPage { <tr> <td></td> <td class='mw-input'>" . - Xml::checkLabel( - $this->msg( 'movetalk' )->text(), - 'wpMovetalk', - 'wpMovetalk', - $this->moveTalk - ) . + Xml::checkLabel( + $this->msg( 'movetalk' )->text(), + 'wpMovetalk', + 'wpMovetalk', + $this->moveTalk + ) . "</td> </tr>" ); @@ -389,14 +383,14 @@ class MovePageForm extends UnlistedSpecialPage { $out->addHTML( " <tr> <td></td> - <td class='mw-input' >" . - Xml::checkLabel( - $this->msg( 'move-leave-redirect' )->text(), - 'wpLeaveRedirect', - 'wpLeaveRedirect', - $isChecked, - $options - ) . + <td class='mw-input'>" . + Xml::checkLabel( + $this->msg( 'move-leave-redirect' )->text(), + 'wpLeaveRedirect', + 'wpLeaveRedirect', + $isChecked, + $options + ) . "</td> </tr>" ); @@ -406,13 +400,13 @@ class MovePageForm extends UnlistedSpecialPage { $out->addHTML( " <tr> <td></td> - <td class='mw-input' >" . - Xml::checkLabel( - $this->msg( 'fix-double-redirects' )->text(), - 'wpFixRedirects', - 'wpFixRedirects', - $this->fixRedirects - ) . + <td class='mw-input'>" . + Xml::checkLabel( + $this->msg( 'fix-double-redirects' )->text(), + 'wpFixRedirects', + 'wpFixRedirects', + $this->fixRedirects + ) . "</td> </tr>" ); @@ -423,21 +417,23 @@ class MovePageForm extends UnlistedSpecialPage { $out->addHTML( " <tr> <td></td> - <td class=\"mw-input\">" . - Xml::check( - 'wpMovesubpages', - # Don't check the box if we only have talk subpages to - # move and we aren't moving the talk page. - $this->moveSubpages && ( $this->oldTitle->hasSubpages() || $this->moveTalk ), - array( 'id' => 'wpMovesubpages' ) - ) . ' ' . - Xml::tags( 'label', array( 'for' => 'wpMovesubpages' ), - $this->msg( - ( $this->oldTitle->hasSubpages() - ? 'move-subpages' - : 'move-talk-subpages' ) - )->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse() - ) . + <td class='mw-input'>" . + Xml::check( + 'wpMovesubpages', + # Don't check the box if we only have talk subpages to + # move and we aren't moving the talk page. + $this->moveSubpages && ( $this->oldTitle->hasSubpages() || $this->moveTalk ), + array( 'id' => 'wpMovesubpages' ) + ) . ' ' . + Xml::tags( + 'label', + array( 'for' => 'wpMovesubpages' ), + $this->msg( + ( $this->oldTitle->hasSubpages() + ? 'move-subpages' + : 'move-talk-subpages' ) + )->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse() + ) . "</td> </tr>" ); @@ -448,32 +444,50 @@ class MovePageForm extends UnlistedSpecialPage { # Don't allow watching if user is not logged in if ( $user->isLoggedIn() ) { $out->addHTML( " - <tr> - <td></td> - <td class='mw-input'>" . - Xml::checkLabel( - $this->msg( 'move-watch' )->text(), - 'wpWatch', - 'watch', - $watchChecked - ) . - "</td> - </tr>" ); + <tr> + <td></td> + <td class='mw-input'>" . + Xml::checkLabel( + $this->msg( 'move-watch' )->text(), + 'wpWatch', + 'watch', + $watchChecked + ) . + "</td> + </tr>" + ); + } + + if ( $confirm ) { + $out->addHTML( " + <tr> + <td></td> + <td class='mw-input'>" . + Xml::checkLabel( + $this->msg( 'delete_and_move_confirm' )->text(), + 'wpConfirm', + 'wpConfirm' + ) . + "</td> + </tr>" + ); } $out->addHTML( " - {$confirm} <tr> - <td> </td> + <td></td> <td class='mw-submit'>" . - Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) . + Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) . "</td> - </tr>" . - Xml::closeElement( 'table' ) . - Html::hidden( 'wpEditToken', $user->getEditToken() ) . - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ) . - "\n" + </tr>" + ); + + $out->addHTML( + Xml::closeElement( 'table' ) . + Html::hidden( 'wpEditToken', $user->getEditToken() ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ) . + "\n" ); $this->showLogFragment( $this->oldTitle ); @@ -523,8 +537,9 @@ class MovePageForm extends UnlistedSpecialPage { // Delete an associated image if there is if ( $nt->getNamespace() == NS_FILE ) { $file = wfLocalFile( $nt ); + $file->load( File::READ_LATEST ); if ( $file->exists() ) { - $file->delete( $reason, false ); + $file->delete( $reason, false, $user ); } } @@ -549,10 +564,22 @@ class MovePageForm extends UnlistedSpecialPage { } # Do the actual move. - $error = $ot->moveTo( $nt, true, $this->reason, $createRedirect ); - if ( $error !== true ) { - $this->showForm( $error ); + $mp = new MovePage( $ot, $nt ); + $valid = $mp->isValidMove(); + if ( !$valid->isOK() ) { + $this->showForm( $valid->getErrorsArray() ); + return; + } + + $permStatus = $mp->checkPermissions( $user, $this->reason ); + if ( !$permStatus->isOK() ) { + $this->showForm( $permStatus->getErrorsArray() ); + return; + } + $status = $mp->move( $user, $this->reason, $createRedirect ); + if ( !$status->isOK() ) { + $this->showForm( $status->getErrorsArray() ); return; } @@ -592,7 +619,7 @@ class MovePageForm extends UnlistedSpecialPage { $newLink )->params( $oldText, $newText )->parseAsBlock() ); $out->addWikiMsg( $msgName ); - wfRunHooks( 'SpecialMovepageAfterMove', array( &$this, &$ot, &$nt ) ); + Hooks::run( 'SpecialMovepageAfterMove', array( &$this, &$ot, &$nt ) ); # Now we move extra pages we've been asked to move: subpages and talk # pages. First, if the old page or the new page is a talk page, we @@ -673,7 +700,10 @@ class MovePageForm extends UnlistedSpecialPage { $oldSubpage->getDBkey() ); - if ( $oldSubpage->isTalkPage() ) { + if ( $oldSubpage->isSubpage() && ( $ot->isTalkPage() xor $nt->isTalkPage() ) ) { + // Moving a subpage from a subject namespace to a talk namespace or vice-versa + $newNs = $nt->getNamespace(); + } elseif ( $oldSubpage->isTalkPage() ) { $newNs = $nt->getTalkPage()->getNamespace(); } else { $newNs = $nt->getSubjectPage()->getNamespace(); diff --git a/includes/specials/SpecialMyLanguage.php b/includes/specials/SpecialMyLanguage.php index 71b18930..6cea1581 100644 --- a/includes/specials/SpecialMyLanguage.php +++ b/includes/specials/SpecialMyLanguage.php @@ -80,6 +80,11 @@ class SpecialMyLanguage extends RedirectSpecialArticle { return null; } + if ( $base->isRedirect() ) { + $page = new WikiPage( $base ); + $base = $page->getRedirectTarget(); + } + $uiCode = $this->getLanguage()->getCode(); $proposed = $base->getSubpage( $uiCode ); if ( $uiCode !== $this->getConfig()->get( 'LanguageCode' ) && $proposed && $proposed->exists() ) { diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index 546c1914..00c8e050 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -30,6 +30,9 @@ class SpecialNewFiles extends IncludableSpecialPage { $this->setHeaders(); $this->outputHeader(); + $out = $this->getOutput(); + $this->addHelpLink( 'Help:New images' ); + $pager = new NewFilesPager( $this->getContext(), $par ); if ( !$this->including() ) { @@ -39,9 +42,9 @@ class SpecialNewFiles extends IncludableSpecialPage { $form->displayForm( '' ); } - $this->getOutput()->addHTML( $pager->getBody() ); + $out->addHTML( $pager->getBody() ); if ( !$this->including() ) { - $this->getOutput()->addHTML( $pager->getNavigationBar() ); + $out->addHTML( $pager->getNavigationBar() ); } } @@ -59,7 +62,7 @@ class SpecialNewFiles extends IncludableSpecialPage { if ( !$message->isDisabled() ) { $this->getOutput()->addWikiText( Html::rawElement( 'p', - array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ), + array( 'lang' => $wgContLang->getHtmlCode(), 'dir' => $wgContLang->getDir() ), "\n" . $message->plain() . "\n" ), /* $lineStart */ false, @@ -141,7 +144,7 @@ class NewFilesPager extends ReverseChronologicalPager { $mode = $this->getRequest()->getVal( 'gallerymode', null ); try { $this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() ); - } catch ( MWException $e ) { + } catch ( Exception $e ) { // User specified something invalid, fallback to default. $this->gallery = ImageGalleryBase::factory( false, $this->getContext() ); } @@ -201,7 +204,10 @@ class NewFilesPager extends ReverseChronologicalPager { $context = new DerivativeContext( $this->getContext() ); $context->setTitle( $this->getTitle() ); // Remove subpage $form = new HTMLForm( $fields, $context ); + $form->setSubmitTextMsg( 'ilsubmit' ); + $form->setSubmitProgressive(); + $form->setMethod( 'get' ); $form->setWrapperLegendMsg( 'newimages-legend' ); diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 0b70bb7e..899c7368 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -56,7 +56,7 @@ class SpecialNewpages extends IncludableSpecialPage { $opts->add( 'invert', false ); $this->customFilters = array(); - wfRunHooks( 'SpecialNewPagesFilters', array( $this, &$this->customFilters ) ); + Hooks::run( 'SpecialNewPagesFilters', array( $this, &$this->customFilters ) ); foreach ( $this->customFilters as $key => $params ) { $opts->add( $key, $params['default'] ); } @@ -127,6 +127,8 @@ class SpecialNewpages extends IncludableSpecialPage { $this->showNavigation = !$this->including(); // Maybe changed in setup $this->setup( $par ); + $this->addHelpLink( 'Help:New pages' ); + if ( !$this->including() ) { // Settings $this->form(); @@ -198,6 +200,9 @@ class SpecialNewpages extends IncludableSpecialPage { } protected function form() { + $out = $this->getOutput(); + $out->addModules( 'mediawiki.userSuggest' ); + // Consume values $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW $namespace = $this->opts->consumeValue( 'namespace' ); @@ -216,72 +221,62 @@ class SpecialNewpages extends IncludableSpecialPage { } $hidden = implode( "\n", $hidden ); - $tagFilter = ChangeTags::buildTagFilterSelector( $tagFilterVal ); - if ( $tagFilter ) { - list( $tagFilterLabel, $tagFilterSelector ) = $tagFilter; - } + $form = array( + 'namespace' => array( + 'type' => 'namespaceselect', + 'name' => 'namespace', + 'label-message' => 'namespace', + 'default' => $namespace, + ), + 'nsinvert' => array( + 'type' => 'check', + 'name' => 'invert', + 'label-message' => 'invert', + 'default' => $nsinvert, + 'tooltip' => $this->msg( 'tooltip-invert' )->text(), + ), + 'tagFilter' => array( + 'type' => 'tagfilter', + 'name' => 'tagfilter', + 'label-raw' => $this->msg( 'tag-filter' )->parse(), + 'default' => $tagFilterVal, + ), + 'username' => array( + 'type' => 'text', + 'name' => 'username', + 'label-message' => 'newpages-username', + 'default' => $userText, + 'id' => 'mw-np-username', + 'size' => 30, + 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest + ), + ); + + $htmlForm = new HTMLForm( $form, $this->getContext() ); - $form = Xml::openElement( 'form', array( 'action' => wfScript() ) ) . - Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . - Xml::fieldset( $this->msg( 'newpages' )->text() ) . - Xml::openElement( 'table', array( 'id' => 'mw-newpages-table' ) ) . - '<tr> - <td class="mw-label">' . - Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) . - '</td> - <td class="mw-input">' . - Html::namespaceSelector( - array( - 'selected' => $namespace, - 'all' => 'all', - ), array( - 'name' => 'namespace', - 'id' => 'namespace', - 'class' => 'namespaceselector', - ) - ) . ' ' . - Xml::checkLabel( - $this->msg( 'invert' )->text(), - 'invert', - 'nsinvert', - $nsinvert, - array( 'title' => $this->msg( 'tooltip-invert' )->text() ) + $htmlForm->setSubmitText( $this->msg( 'allpagessubmit' )->text() ); + $htmlForm->setSubmitProgressive(); + // The form should be visible on each request (inclusive requests with submitted forms), so + // return always false here. + $htmlForm->setSubmitCallback( + function () { + return false; + } + ); + $htmlForm->setMethod( 'get' ); + + $out->addHtml( Xml::fieldset( $this->msg( 'newpages' )->text() ) ); + + $htmlForm->show(); + + $out->addHtml( + Html::rawElement( + 'div', + null, + $this->filterLinks() ) . - '</td> - </tr>' . ( $tagFilter ? ( - '<tr> - <td class="mw-label">' . - $tagFilterLabel . - '</td> - <td class="mw-input">' . - $tagFilterSelector . - '</td> - </tr>' ) : '' ) . - '<tr> - <td class="mw-label">' . - Xml::label( $this->msg( 'newpages-username' )->text(), 'mw-np-username' ) . - '</td> - <td class="mw-input">' . - Xml::input( 'username', 30, $userText, array( 'id' => 'mw-np-username' ) ) . - '</td> - </tr>' . - '<tr> <td></td> - <td class="mw-submit">' . - Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . - '</td> - </tr>' . - '<tr> - <td></td> - <td class="mw-input">' . - $this->filterLinks() . - '</td> - </tr>' . - Xml::closeElement( 'table' ) . - Xml::closeElement( 'fieldset' ) . - $hidden . - Xml::closeElement( 'form' ); - - $this->getOutput()->addHTML( $form ); + Xml::closeElement( 'fieldset' ) + ); } /** @@ -340,12 +335,12 @@ class SpecialNewpages extends IncludableSpecialPage { $hist = Html::rawElement( 'span', array( 'class' => 'mw-newpages-history' ), $this->msg( 'parentheses' )->rawParams( $histLink )->escaped() ); - $length = Html::element( + $length = Html::rawElement( 'span', array( 'class' => 'mw-newpages-length' ), - $this->msg( 'brackets' )->params( $this->msg( 'nbytes' ) - ->numParams( $result->length )->text() - ) + $this->msg( 'brackets' )->rawParams( + $this->msg( 'nbytes' )->numParams( $result->length )->escaped() + )->escaped() ); $ulink = Linker::revUserTools( $rev ); @@ -555,7 +550,7 @@ class NewPagesPager extends ReverseChronologicalPager { ); $join_conds = array( 'page' => array( 'INNER JOIN', 'page_id=rc_cur_id' ) ); - wfRunHooks( 'SpecialNewpagesConditions', + Hooks::run( 'SpecialNewpagesConditions', array( &$this, $this->opts, &$conds, &$tables, &$fields, &$join_conds ) ); $options = array(); diff --git a/includes/specials/SpecialPageLanguage.php b/includes/specials/SpecialPageLanguage.php index 2acf23cd..79b2444e 100644 --- a/includes/specials/SpecialPageLanguage.php +++ b/includes/specials/SpecialPageLanguage.php @@ -90,10 +90,12 @@ class SpecialPageLanguage extends FormSpecialPage { return $this->showLogFragment( $this->par ); } + protected function getDisplayFormat() { + return 'vform'; + } + public function alterForm( HTMLForm $form ) { - $form->setDisplayFormat( 'vform' ); - $form->setWrapperLegend( false ); - wfRunHooks( 'LanguageSelector', array( $this->getOutput(), 'mw-languageselector' ) ); + Hooks::run( 'LanguageSelector', array( $this->getOutput(), 'mw-languageselector' ) ); } /** diff --git a/includes/specials/SpecialPagesWithProp.php b/includes/specials/SpecialPagesWithProp.php index f5b19cc6..670a3973 100644 --- a/includes/specials/SpecialPagesWithProp.php +++ b/includes/specials/SpecialPagesWithProp.php @@ -83,11 +83,13 @@ class SpecialPagesWithProp extends QueryPage { * * @param string $search Prefix to search for * @param int $limit Maximum number of results to return + * @param int $offset Number of pages to skip * @return string[] Matching subpages */ - public function prefixSearchSubpages( $search, $limit = 10 ) { - $subpages = array_keys( $this->getExistingPropNames() ); - return self::prefixSearchArray( $search, $limit, $subpages ); + public function prefixSearchSubpages( $search, $limit, $offset ) { + $subpages = array_keys( $this->queryExistingProps( $limit, $offset ) ); + // We've already limited and offsetted, set to N and 0 respectively. + return self::prefixSearchArray( $search, count( $subpages ), $subpages, 0 ); } /** @@ -154,23 +156,38 @@ class SpecialPagesWithProp extends QueryPage { public function getExistingPropNames() { if ( $this->existingPropNames === null ) { - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( - 'page_props', - 'pp_propname', - '', - __METHOD__, - array( 'DISTINCT', 'ORDER BY' => 'pp_propname' ) - ); - $propnames = array(); - foreach ( $res as $row ) { - $propnames[$row->pp_propname] = $row->pp_propname; - } - $this->existingPropNames = $propnames; + $this->existingPropNames = $this->queryExistingProps(); } return $this->existingPropNames; } + protected function queryExistingProps( $limit = null, $offset = 0 ) { + $opts = array( + 'DISTINCT', 'ORDER BY' => 'pp_propname' + ); + if ( $limit ) { + $opts['LIMIT'] = $limit; + } + if ( $offset ) { + $opts['OFFSET'] = $offset; + } + + $res = wfGetDB( DB_SLAVE )->select( + 'page_props', + 'pp_propname', + '', + __METHOD__, + $opts + ); + + $propnames = array(); + foreach ( $res as $row ) { + $propnames[$row->pp_propname] = $row->pp_propname; + } + + return $propnames; + } + protected function getGroupName() { return 'pages'; } diff --git a/includes/specials/SpecialPasswordReset.php b/includes/specials/SpecialPasswordReset.php index 3061c85b..a2dc2add 100644 --- a/includes/specials/SpecialPasswordReset.php +++ b/includes/specials/SpecialPasswordReset.php @@ -103,16 +103,13 @@ class SpecialPasswordReset extends FormSpecialPage { return $a; } + protected function getDisplayFormat() { + return 'vform'; + } + public function alterForm( HTMLForm $form ) { $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' ); - $form->setDisplayFormat( 'vform' ); - // Turn the old-school line around the form off. - // XXX This wouldn't be necessary here if we could set the format of - // the HTMLForm to 'vform' at its creation, but there's no way to do so - // from a FormSpecialPage class. - $form->setWrapperLegend( false ); - $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) ); $i = 0; @@ -195,7 +192,7 @@ class SpecialPasswordReset extends FormSpecialPage { // Check for hooks (captcha etc), and allow them to modify the users list $error = array(); - if ( !wfRunHooks( 'SpecialPasswordResetOnSubmit', array( &$users, $data, &$error ) ) ) { + if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', array( &$users, $data, &$error ) ) ) { return array( $error ); } @@ -246,7 +243,7 @@ class SpecialPasswordReset extends FormSpecialPage { return array( 'badipaddress' ); } $caller = $this->getUser(); - wfRunHooks( 'User::mailPasswordInternal', array( &$caller, &$ip, &$firstUser ) ); + Hooks::run( 'User::mailPasswordInternal', array( &$caller, &$ip, &$firstUser ) ); $username = $caller->getName(); $msg = IP::isValid( $username ) ? 'passwordreset-emailtext-ip' diff --git a/includes/specials/SpecialPopularpages.php b/includes/specials/SpecialPopularpages.php deleted file mode 100644 index 2a80f651..00000000 --- a/includes/specials/SpecialPopularpages.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php -/** - * Implements Special:PopularPages - * - * 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 - */ - -/** - * A special page that list most viewed pages - * - * @ingroup SpecialPage - */ -class PopularPagesPage extends QueryPage { - function __construct( $name = 'Popularpages' ) { - parent::__construct( $name ); - } - - function isExpensive() { - # page_counter is not indexed - return true; - } - - function isSyndicated() { - return false; - } - - function getQueryInfo() { - return array( - 'tables' => array( 'page' ), - 'fields' => array( - 'namespace' => 'page_namespace', - 'title' => 'page_title', - 'value' => 'page_counter' ), - 'conds' => array( - 'page_is_redirect' => 0, - 'page_namespace' => MWNamespace::getContentNamespaces() - ) - ); - } - - /** - * @param Skin $skin - * @param object $result Result row - * @return string - */ - function formatResult( $skin, $result ) { - global $wgContLang; - - $title = Title::makeTitleSafe( $result->namespace, $result->title ); - if ( !$title ) { - return Html::element( - 'span', - array( 'class' => 'mw-invalidtitle' ), - Linker::getInvalidTitleDescription( - $this->getContext(), - $result->namespace, - $result->title ) - ); - } - - $link = Linker::linkKnown( - $title, - htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) - ); - $nv = $this->msg( 'nviews' )->numParams( $result->value )->escaped(); - - return $this->getLanguage()->specialList( $link, $nv ); - } - - protected function getGroupName() { - return 'wiki'; - } -} diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index cea00fa6..7371da74 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -55,6 +55,8 @@ class SpecialPreferences extends SpecialPage { ); } + $this->addHelpLink( 'Help:Preferences' ); + $htmlForm = Preferences::getFormObject( $this->getUser(), $this->getContext() ); $htmlForm->setSubmitCallback( array( 'Preferences', 'tryUISubmit' ) ); diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php index 2e67e2b5..5a67d924 100644 --- a/includes/specials/SpecialPrefixindex.php +++ b/includes/specials/SpecialPrefixindex.php @@ -102,7 +102,10 @@ class SpecialPrefixindex extends SpecialAllPages { */ protected function namespacePrefixForm( $namespace = NS_MAIN, $from = '' ) { $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); - $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getConfig()->get( 'Script' ) ) ); + $out .= Xml::openElement( + 'form', + array( 'method' => 'get', 'action' => $this->getConfig()->get( 'Script' ) ) + ); $out .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ); $out .= Xml::openElement( 'fieldset' ); $out .= Xml::element( 'legend', null, $this->msg( 'allpages' )->text() ); diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php index 0ba73857..00e56c1c 100644 --- a/includes/specials/SpecialProtectedpages.php +++ b/includes/specials/SpecialProtectedpages.php @@ -39,11 +39,6 @@ class SpecialProtectedpages extends SpecialPage { $this->outputHeader(); $this->getOutput()->addModuleStyles( 'mediawiki.special' ); - // Purge expired entries on one in every 10 queries - if ( !mt_rand( 0, 10 ) ) { - Title::purgeExpiredRestrictions(); - } - $request = $this->getRequest(); $type = $request->getVal( $this->IdType ); $level = $request->getVal( $this->IdLevel ); @@ -353,7 +348,7 @@ class ProtectedPagesPager extends TablePager { /** * @param string $field * @param string $value - * @return string + * @return string HTML * @throws MWException */ function formatValue( $field, $value ) { @@ -372,7 +367,8 @@ class ProtectedPagesPager extends TablePager { $this->msg( 'protectedpages-unknown-timestamp' )->escaped() ); } else { - $formatted = $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ); + $formatted = htmlspecialchars( $this->getLanguage()->userTimeAndDate( + $value, $this->getUser() ) ); } break; @@ -402,7 +398,8 @@ class ProtectedPagesPager extends TablePager { break; case 'pr_expiry': - $formatted = $this->getLanguage()->formatExpiry( $value, /* User preference timezone */true ); + $formatted = htmlspecialchars( $this->getLanguage()->formatExpiry( + $value, /* User preference timezone */true ) ); $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); if ( $this->getUser()->isAllowed( 'protect' ) && $title ) { $changeProtection = Linker::linkKnown( @@ -454,7 +451,7 @@ class ProtectedPagesPager extends TablePager { // Messages: restriction-level-sysop, restriction-level-autoconfirmed $params[] = $this->msg( 'restriction-level-' . $row->pr_level )->escaped(); if ( $row->pr_cascade ) { - $params[] = $this->msg( 'protect-summary-cascade' )->text(); + $params[] = $this->msg( 'protect-summary-cascade' )->escaped(); } $formatted = $this->getLanguage()->commaList( $params ); break; @@ -493,7 +490,7 @@ class ProtectedPagesPager extends TablePager { function getQueryInfo() { $conds = $this->mConds; $conds[] = 'pr_expiry > ' . $this->mDb->addQuotes( $this->mDb->timestamp() ) . - 'OR pr_expiry IS NULL'; + ' OR pr_expiry IS NULL'; $conds[] = 'page_id=pr_page'; $conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type ); diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php index a40da87d..dd9198cb 100644 --- a/includes/specials/SpecialProtectedtitles.php +++ b/includes/specials/SpecialProtectedtitles.php @@ -38,11 +38,6 @@ class SpecialProtectedtitles extends SpecialPage { $this->setHeaders(); $this->outputHeader(); - // Purge expired entries on one in every 10 queries - if ( !mt_rand( 0, 10 ) ) { - Title::purgeExpiredRestrictions(); - } - $request = $this->getRequest(); $type = $request->getVal( $this->IdType ); $level = $request->getVal( $this->IdLevel ); @@ -72,7 +67,6 @@ class SpecialProtectedtitles extends SpecialPage { * @return string */ function formatRow( $row ) { - wfProfileIn( __METHOD__ ); static $infinity = null; @@ -82,7 +76,6 @@ class SpecialProtectedtitles extends SpecialPage { $title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title ); if ( !$title ) { - wfProfileOut( __METHOD__ ); return Html::rawElement( 'li', @@ -119,8 +112,6 @@ class SpecialProtectedtitles extends SpecialPage { )->escaped(); } - wfProfileOut( __METHOD__ ); - // @todo i18n: This should use a comma separator instead of a hard coded comma, right? return '<li>' . $lang->specialList( $link, implode( $description_items, ', ' ) ) . "</li>\n"; } @@ -227,7 +218,6 @@ class ProtectedTitlesPager extends AlphabeticPager { } function getStartBody() { - wfProfileIn( __METHOD__ ); # Do a link batch query $this->mResult->seek( 0 ); $lb = new LinkBatch; @@ -237,7 +227,6 @@ class ProtectedTitlesPager extends AlphabeticPager { } $lb->execute(); - wfProfileOut( __METHOD__ ); return ''; } @@ -258,7 +247,8 @@ class ProtectedTitlesPager extends AlphabeticPager { */ function getQueryInfo() { $conds = $this->mConds; - $conds[] = 'pt_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() ); + $conds[] = 'pt_expiry > ' . $this->mDb->addQuotes( $this->mDb->timestamp() ) . + ' OR pt_expiry IS NULL'; if ( $this->level ) { $conds['pt_create_perm'] = $this->level; } diff --git a/includes/specials/SpecialRandomInCategory.php b/includes/specials/SpecialRandomInCategory.php index 570ab3bf..b5c9e19a 100644 --- a/includes/specials/SpecialRandomInCategory.php +++ b/includes/specials/SpecialRandomInCategory.php @@ -68,6 +68,8 @@ class SpecialRandomInCategory extends FormSpecialPage { } protected function getFormFields() { + $this->addHelpLink( 'Help:RandomInCategory' ); + $form = array( 'category' => array( 'type' => 'text', @@ -117,7 +119,7 @@ class SpecialRandomInCategory extends FormSpecialPage { return Status::newFatal( $msg ); } elseif ( !$this->category ) { - return; // no data sent + return false; // no data sent } $title = $this->getRandomTitle(); @@ -179,12 +181,12 @@ class SpecialRandomInCategory extends FormSpecialPage { * @param float $rand Random number between 0 and 1 * @param int $offset Extra offset to fudge randomness * @param bool $up True to get the result above the random number, false for below - * + * @return array Query information. + * @throws MWException * @note The $up parameter is supposed to counteract what would happen if there * was a large gap in the distribution of cl_timestamp values. This way instead * of things to the right of the gap being favoured, both sides of the gap * are favoured. - * @return array Query information. */ protected function getQueryInfo( $rand, $offset, $up ) { $op = $up ? '>=' : '<='; @@ -230,7 +232,7 @@ class SpecialRandomInCategory extends FormSpecialPage { if ( !$this->minTimestamp || !$this->maxTimestamp ) { try { list( $this->minTimestamp, $this->maxTimestamp ) = $this->getMinAndMaxForCat( $this->category ); - } catch ( MWException $e ) { + } catch ( Exception $e ) { // Possibly no entries in category. return false; } diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php index 6d8f59b5..73a88b9e 100644 --- a/includes/specials/SpecialRandompage.php +++ b/includes/specials/SpecialRandompage.php @@ -106,7 +106,7 @@ class RandomPage extends SpecialPage { $randstr = wfRandom(); $title = null; - if ( !wfRunHooks( + if ( !Hooks::run( 'SpecialRandomGetRandomTitle', array( &$randstr, &$this->isRedir, &$this->namespaces, &$this->extra, &$title ) ) ) { diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index e6d8f1c3..64b0ecae 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -95,7 +95,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { protected function getCustomFilters() { if ( $this->customFilters === null ) { $this->customFilters = parent::getCustomFilters(); - wfRunHooks( 'SpecialRecentChangesFilters', array( $this, &$this->customFilters ), '1.23' ); + Hooks::run( 'SpecialRecentChangesFilters', array( $this, &$this->customFilters ), '1.23' ); } return $this->customFilters; @@ -252,9 +252,11 @@ class SpecialRecentChanges extends ChangesListSpecialPage { return $rows; } - protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ) { + protected function runMainQueryHook( &$tables, &$fields, &$conds, + &$query_options, &$join_conds, $opts + ) { return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ) - && wfRunHooks( + && Hooks::run( 'SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ), '1.23' @@ -311,7 +313,9 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $rc = RecentChange::newFromRow( $obj ); $rc->counter = $counter++; # Check if the page has been updated since the last visit - if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) && !empty( $obj->wl_notificationtimestamp ) ) { + if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) + && !empty( $obj->wl_notificationtimestamp ) + ) { $rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp ); } else { $rc->notificationtimestamp = false; // Default @@ -440,11 +444,11 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $message = $this->msg( 'recentchangestext' )->inContentLanguage(); if ( !$message->isDisabled() ) { $this->getOutput()->addWikiText( - Html::rawElement( 'p', - array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ), + Html::rawElement( 'div', + array( 'lang' => $wgContLang->getHtmlCode(), 'dir' => $wgContLang->getDir() ), "\n" . $message->plain() . "\n" ), - /* $lineStart */ false, + /* $lineStart */ true, /* $interface */ false ); } @@ -475,7 +479,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { // Don't fire the hook for subclasses. (Or should we?) if ( $this->getName() === 'Recentchanges' ) { - wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) ); + Hooks::run( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) ); } return $extraOpts; @@ -732,7 +736,8 @@ class SpecialRecentChanges extends ChangesListSpecialPage { $link = $this->makeOptionsLink( $linkMessage->text(), array( $key => 1 - $options[$key] ), $nondefaults ); - $links[] = "<span class=\"$msg rcshowhideoption\">" . $this->msg( $msg )->rawParams( $link )->escaped() . '</span>'; + $links[] = "<span class=\"$msg rcshowhideoption\">" + . $this->msg( $msg )->rawParams( $link )->escaped() . '</span>'; } // show from this onward link diff --git a/includes/specials/SpecialRedirect.php b/includes/specials/SpecialRedirect.php index 2022d748..72d21ebe 100644 --- a/includes/specials/SpecialRedirect.php +++ b/includes/specials/SpecialRedirect.php @@ -2,7 +2,6 @@ /** * Implements Special:Redirect * - * @section LICENSE * 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 @@ -263,6 +262,20 @@ class SpecialRedirect extends FormSpecialPage { $form->setMethod( 'get' ); } + /** + * Return an array of subpages that this special page will accept. + * + * @return string[] subpages + */ + protected function getSubpagesForPrefixSearch() { + return array( + "file", + "page", + "revision", + "user", + ); + } + protected function getGroupName() { return 'redirects'; } diff --git a/includes/specials/SpecialResetTokens.php b/includes/specials/SpecialResetTokens.php index 4add7421..ba2b9a5b 100644 --- a/includes/specials/SpecialResetTokens.php +++ b/includes/specials/SpecialResetTokens.php @@ -44,7 +44,7 @@ class SpecialResetTokens extends FormSpecialPage { $tokens = array( array( 'preference' => 'watchlisttoken', 'label-message' => 'resettokens-watchlist-token' ), ); - wfRunHooks( 'SpecialResetTokensTokens', array( &$tokens ) ); + Hooks::run( 'SpecialResetTokensTokens', array( &$tokens ) ); $hiddenPrefs = $this->getConfig()->get( 'HiddenPrefs' ); $tokens = array_filter( $tokens, function ( $tok ) use ( $hiddenPrefs ) { diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php index 7eea71da..9e2ca277 100644 --- a/includes/specials/SpecialRevisiondelete.php +++ b/includes/specials/SpecialRevisiondelete.php @@ -132,18 +132,8 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { // $this->ids = array_map( 'intval', $this->ids ); $this->ids = array_unique( array_filter( $this->ids ) ); - if ( $request->getVal( 'action' ) == 'historysubmit' - || $request->getVal( 'action' ) == 'revisiondelete' - ) { - // For show/hide form submission from history page - // Since we are access through index.php?title=XXX&action=historysubmit - // getFullTitle() will contain the target title and not our title - $this->targetObj = $this->getFullTitle(); - $this->typeName = 'revision'; - } else { - $this->typeName = $request->getVal( 'type' ); - $this->targetObj = Title::newFromText( $request->getText( 'target' ) ); - } + $this->typeName = $request->getVal( 'type' ); + $this->targetObj = Title::newFromText( $request->getText( 'target' ) ); # For reviewing deleted files... $this->archiveName = $request->getVal( 'file' ); @@ -293,6 +283,8 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { * Show a deleted file version requested by the visitor. * @todo Mostly copied from Special:Undelete. Refactor. * @param string $archiveName + * @throws MWException + * @throws PermissionsError */ protected function tryShowFile( $archiveName ) { $repo = RepoGroup::singleton()->getLocalRepo(); @@ -372,10 +364,12 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $userAllowed = true; // Messages: revdelete-selected-text, revdelete-selected-file, logdelete-selected - $this->getOutput()->wrapWikiMsg( "<strong>$1</strong>", array( $this->typeLabels['selected'], + $out = $this->getOutput(); + $out->wrapWikiMsg( "<strong>$1</strong>", array( $this->typeLabels['selected'], $this->getLanguage()->formatNum( count( $this->ids ) ), $this->targetObj->getPrefixedText() ) ); - $this->getOutput()->addHTML( "<ul>" ); + $this->addHelpLink( 'Help:RevisionDelete' ); + $out->addHTML( "<ul>" ); $numRevisions = 0; // Live revisions... @@ -393,14 +387,14 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { } $numRevisions++; - $this->getOutput()->addHTML( $item->getHTML() ); + $out->addHTML( $item->getHTML() ); } if ( !$numRevisions ) { throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); } - $this->getOutput()->addHTML( "</ul>" ); + $out->addHTML( "</ul>" ); // Explanation text $this->addUsageText(); @@ -411,7 +405,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { // Show form if the user can submit if ( $this->mIsAllowed ) { - $out = Xml::openElement( 'form', array( 'method' => 'post', + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ), 'id' => 'mw-revdel-form-revisions' ) ) . Xml::fieldset( $this->msg( 'revdelete-legend' )->text() ) . @@ -463,12 +457,12 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { array(), array( 'action' => 'edit' ) ); - $out .= Xml::tags( 'p', array( 'class' => 'mw-revdel-editreasons' ), $link ) . "\n"; + $form .= Xml::tags( 'p', array( 'class' => 'mw-revdel-editreasons' ), $link ) . "\n"; } } else { - $out = ''; + $form = ''; } - $this->getOutput()->addHTML( $out ); + $out->addHTML( $form ); } /** diff --git a/includes/specials/SpecialRunJobs.php b/includes/specials/SpecialRunJobs.php index d4a06eb5..8cf93670 100644 --- a/includes/specials/SpecialRunJobs.php +++ b/includes/specials/SpecialRunJobs.php @@ -22,6 +22,8 @@ * @author Aaron Schulz */ +use MediaWiki\Logger\LoggerFactory; + /** * Special page designed for running background tasks (internal use only) * @@ -61,10 +63,11 @@ class SpecialRunJobs extends UnlistedSpecialPage { $squery = $params; unset( $squery['signature'] ); - $cSig = self::getQuerySignature( $squery, $this->getConfig()->get( 'SecretKey' ) ); // correct signature - $rSig = $params['signature']; // provided signature + $correctSignature = self::getQuerySignature( $squery, $this->getConfig()->get( 'SecretKey' ) ); + $providedSignature = $params['signature']; - $verified = is_string( $rSig ) && hash_equals( $cSig, $rSig ); + $verified = is_string( $providedSignature ) + && hash_equals( $correctSignature, $providedSignature ); if ( !$verified || $params['sigexpiry'] < time() ) { header( "HTTP/1.0 400 Bad Request" ); print 'Invalid or stale signature provided'; @@ -88,7 +91,7 @@ class SpecialRunJobs extends UnlistedSpecialPage { // Do all of the specified tasks... if ( in_array( 'jobs', explode( '|', $params['tasks'] ) ) ) { - $runner = new JobRunner(); + $runner = new JobRunner( LoggerFactory::getInstance( 'runJobs' ) ); $response = $runner->run( array( 'type' => $params['type'], 'maxJobs' => $params['maxjobs'] ? $params['maxjobs'] : 1, diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 88ab7d82..608d62e6 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -63,7 +63,7 @@ class SpecialSearch extends SpecialPage { /** * @var string */ - protected $didYouMeanHtml, $fulltext; + protected $fulltext; const NAMESPACES_CURRENT = 'sense'; @@ -165,7 +165,6 @@ class SpecialSearch extends SpecialPage { } } - $this->didYouMeanHtml = ''; # html of did you mean... link $this->fulltext = $request->getVal( 'fulltext' ); $this->profile = $profile; } @@ -196,7 +195,7 @@ class SpecialSearch extends SpecialPage { # No match, generate an edit URL $title = Title::newFromText( $term ); if ( !is_null( $title ) ) { - wfRunHooks( 'SpecialSearchNogomatch', array( &$title ) ); + Hooks::run( 'SpecialSearchNogomatch', array( &$title ) ); } $this->showResults( $term ); } @@ -207,14 +206,14 @@ class SpecialSearch extends SpecialPage { public function showResults( $term ) { global $wgContLang; - $profile = new ProfileSection( __METHOD__ ); $search = $this->getSearchEngine(); $search->setLimitOffset( $this->limit, $this->offset ); $search->setNamespaces( $this->namespaces ); $search->prefix = $this->mPrefix; $term = $search->transformSearchTerm( $term ); + $didYouMeanHtml = ''; - wfRunHooks( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) ); + Hooks::run( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) ); $this->setupPage( $term ); @@ -289,11 +288,14 @@ class SpecialSearch extends SpecialPage { $stParams ); - $this->didYouMeanHtml = '<div class="searchdidyoumean">' - . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '</div>'; + # html of did you mean... search suggestion link + $didYouMeanHtml = + Xml::openElement( 'div', array( 'class' => 'searchdidyoumean' ) ) . + $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . + Xml::closeElement( 'div' ); } - if ( !wfRunHooks( 'SpecialSearchResultsPrepend', array( $this, $out, $term ) ) ) { + if ( !Hooks::run( 'SpecialSearchResultsPrepend', array( $this, $out, $term ) ) ) { # Hook requested termination return; } @@ -303,7 +305,7 @@ class SpecialSearch extends SpecialPage { Xml::openElement( 'form', array( - 'id' => ( $this->profile === 'advanced' ? 'powersearch' : 'search' ), + 'id' => ( $this->isPowerSearch() ? 'powersearch' : 'search' ), 'method' => 'get', 'action' => wfScript(), ) @@ -330,8 +332,10 @@ class SpecialSearch extends SpecialPage { Xml::openElement( 'div', array( 'id' => 'mw-search-top-table' ) ) . $this->shortDialog( $term, $num, $totalRes ) . Xml::closeElement( 'div' ) . - $this->formHeader( $term ) . - Xml::closeElement( 'form' ) + $this->searchProfileTabs( $term ) . + $this->searchOptions( $term ) . + Xml::closeElement( 'form' ) . + $didYouMeanHtml ); $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':'; @@ -360,7 +364,7 @@ class SpecialSearch extends SpecialPage { ); } } - wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); + Hooks::run( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); $out->parserOptions()->setEditSection( false ); if ( $titleMatches ) { @@ -426,21 +430,24 @@ class SpecialSearch extends SpecialPage { return; } + $messageName = 'searchmenu-new-nocreate'; $linkClass = 'mw-search-createlink'; - if ( $title->isKnown() ) { - $messageName = 'searchmenu-exists'; - $linkClass = 'mw-search-exists'; - } elseif ( $title->quickUserCan( 'create', $this->getUser() ) ) { - $messageName = 'searchmenu-new'; - } else { - $messageName = 'searchmenu-new-nocreate'; + + if ( !$title->isExternal() ) { + if ( $title->isKnown() ) { + $messageName = 'searchmenu-exists'; + $linkClass = 'mw-search-exists'; + } elseif ( $title->quickUserCan( 'create', $this->getUser() ) ) { + $messageName = 'searchmenu-new'; + } } + $params = array( $messageName, wfEscapeWikiText( $title->getPrefixedText() ), Message::numParam( $num ) ); - wfRunHooks( 'SpecialSearchCreateLink', array( $title, &$params ) ); + Hooks::run( 'SpecialSearchCreateLink', array( $title, &$params ) ); // Extensions using the hook might still return an empty $messageName if ( $messageName ) { @@ -455,8 +462,6 @@ class SpecialSearch extends SpecialPage { * @param string $term */ protected function setupPage( $term ) { - # Should advanced UI be used? - $this->searchAdvanced = ( $this->profile === 'advanced' ); $out = $this->getOutput(); if ( strval( $term ) !== '' ) { $out->setPageTitle( $this->msg( 'searchresults' ) ); @@ -470,6 +475,15 @@ class SpecialSearch extends SpecialPage { } /** + * Return true if current search is a power (advanced) search + * + * @return bool + */ + protected function isPowerSearch() { + return $this->profile === 'advanced'; + } + + /** * Extract "power search" namespace settings from the request object, * returning a list of index numbers to search. * @@ -494,7 +508,7 @@ class SpecialSearch extends SpecialPage { */ protected function powerSearchOptions() { $opt = array(); - if ( $this->profile !== 'advanced' ) { + if ( !$this->isPowerSearch() ) { $opt['profile'] = $this->profile; } else { foreach ( $this->namespaces as $n ) { @@ -519,7 +533,7 @@ class SpecialSearch extends SpecialPage { $request->getVal( 'nsRemember' ), 'searchnamespace', $request - ) + ) && !wfReadOnly() ) { // Reset namespace preferences: namespaces are not searched // when they're not mentioned in the URL parameters. @@ -549,7 +563,6 @@ class SpecialSearch extends SpecialPage { protected function showMatches( &$matches ) { global $wgContLang; - $profile = new ProfileSection( __METHOD__ ); $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); $out = "<ul class='mw-search-results'>\n"; @@ -575,7 +588,6 @@ class SpecialSearch extends SpecialPage { * @return string */ protected function showHit( $result, $terms ) { - $profile = new ProfileSection( __METHOD__ ); if ( $result->isBrokenTitle() ) { return ''; @@ -583,7 +595,7 @@ class SpecialSearch extends SpecialPage { $title = $result->getTitle(); - $titleSnippet = $result->getTitleSnippet( $terms ); + $titleSnippet = $result->getTitleSnippet(); if ( $titleSnippet == '' ) { $titleSnippet = null; @@ -591,7 +603,7 @@ class SpecialSearch extends SpecialPage { $link_t = clone $title; - wfRunHooks( 'ShowSearchHitTitle', + Hooks::run( 'ShowSearchHitTitle', array( &$link_t, &$titleSnippet, $result, $terms, $this ) ); $link = Linker::linkKnown( @@ -615,11 +627,12 @@ class SpecialSearch extends SpecialPage { // format redirects / relevant sections $redirectTitle = $result->getRedirectTitle(); - $redirectText = $result->getRedirectSnippet( $terms ); + $redirectText = $result->getRedirectSnippet(); $sectionTitle = $result->getSectionTitle(); - $sectionText = $result->getSectionSnippet( $terms ); - $redirect = ''; + $sectionText = $result->getSectionSnippet(); + $categorySnippet = $result->getCategorySnippet(); + $redirect = ''; if ( !is_null( $redirectTitle ) ) { if ( $redirectText == '' ) { $redirectText = null; @@ -632,7 +645,6 @@ class SpecialSearch extends SpecialPage { } $section = ''; - if ( !is_null( $sectionTitle ) ) { if ( $sectionText == '' ) { $sectionText = null; @@ -644,6 +656,13 @@ class SpecialSearch extends SpecialPage { "</span>"; } + $category = ''; + if ( $categorySnippet ) { + $category = "<span class='searchalttitle'>" . + $this->msg( 'search-category' )->rawParams( $categorySnippet )->text() . + "</span>"; + } + // format text extract $extract = "<div class='searchresult'>" . $result->getTextSnippet( $terms ) . "</div>"; @@ -688,7 +707,7 @@ class SpecialSearch extends SpecialPage { $thumb->toHtml( array( 'desc-link' => true ) ) . '</td>' . '<td style="vertical-align: top;">' . - "{$link} {$redirect} {$section} {$fileMatch}" . + "{$link} {$redirect} {$category} {$section} {$fileMatch}" . $extract . "<div class='mw-search-result-data'>{$desc} - {$date}</div>" . '</td>' . @@ -702,14 +721,14 @@ class SpecialSearch extends SpecialPage { $html = null; $score = ''; - if ( wfRunHooks( 'ShowSearchHit', array( + if ( Hooks::run( 'ShowSearchHit', array( $this, $result, $terms, &$link, &$redirect, &$section, &$extract, &$score, &$size, &$date, &$related, &$html ) ) ) { $html = "<li><div class='mw-search-result-heading'>" . - "{$link} {$redirect} {$section} {$fileMatch}</div> {$extract}\n" . + "{$link} {$redirect} {$category} {$section} {$fileMatch}</div> {$extract}\n" . "<div class='mw-search-result-data'>{$size} - {$date}</div>" . "</li>\n"; } @@ -727,7 +746,6 @@ class SpecialSearch extends SpecialPage { */ protected function showInterwiki( $matches, $query ) { global $wgContLang; - $profile = new ProfileSection( __METHOD__ ); $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>" . $this->msg( 'search-interwiki-caption' )->text() . "</div>\n"; @@ -778,7 +796,6 @@ class SpecialSearch extends SpecialPage { * @return string */ protected function showInterwikiHit( $result, $lastInterwiki, $query, $customCaptions ) { - $profile = new ProfileSection( __METHOD__ ); if ( $result->isBrokenTitle() ) { return ''; @@ -885,10 +902,7 @@ class SpecialSearch extends SpecialPage { // be arranged nicely while still accommodating different screen widths $namespaceTables = ''; for ( $i = 0; $i < $numRows; $i += 4 ) { - $namespaceTables .= Xml::openElement( - 'table', - array( 'cellpadding' => 0, 'cellspacing' => 0 ) - ); + $namespaceTables .= Xml::openElement( 'table' ); for ( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) { $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] ); @@ -899,7 +913,7 @@ class SpecialSearch extends SpecialPage { $showSections = array( 'namespaceTables' => $namespaceTables ); - wfRunHooks( 'SpecialSearchPowerBox', array( &$showSections, $term, $opts ) ); + Hooks::run( 'SpecialSearchPowerBox', array( &$showSections, $term, $opts ) ); $hidden = ''; foreach ( $opts as $key => $value ) { @@ -911,7 +925,7 @@ class SpecialSearch extends SpecialPage { $user = $this->getUser(); if ( $user->isLoggedIn() ) { $remember .= Xml::checkLabel( - wfMessage( 'powersearch-remember' )->text(), + $this->msg( 'powersearch-remember' )->text(), 'nsRemember', 'mw-search-powersearch-remember', false, @@ -970,7 +984,7 @@ class SpecialSearch extends SpecialPage { ) ); - wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) ); + Hooks::run( 'SpecialSearchProfiles', array( &$profiles ) ); foreach ( $profiles as &$data ) { if ( !is_array( $data['namespaces'] ) ) { @@ -986,8 +1000,8 @@ class SpecialSearch extends SpecialPage { * @param string $term * @return string */ - protected function formHeader( $term ) { - $out = Xml::openElement( 'div', array( 'class' => 'mw-search-formheader' ) ); + protected function searchProfileTabs( $term ) { + $out = Xml::openElement( 'div', array( 'class' => 'mw-search-profile-tabs' ) ); $bareterm = $term; if ( $this->startsWithImage( $term ) ) { @@ -1028,15 +1042,23 @@ class SpecialSearch extends SpecialPage { $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false ); $out .= Xml::closeElement( 'div' ); - // Hidden stuff + return $out; + } + + /** + * @param string $term Search term + * @return string + */ + protected function searchOptions( $term ) { + $out = ''; $opts = array(); $opts['profile'] = $this->profile; - if ( $this->profile === 'advanced' ) { + if ( $this->isPowerSearch() ) { $out .= $this->powerSearchBox( $term, $opts ); } else { $form = ''; - wfRunHooks( 'SpecialSearchProfileForm', array( $this, &$form, $this->profile, $term, $opts ) ); + Hooks::run( 'SpecialSearchProfileForm', array( $this, &$form, $this->profile, $term, $opts ) ); $out .= $form; } @@ -1054,15 +1076,16 @@ class SpecialSearch extends SpecialPage { $out .= Html::hidden( 'profile', $this->profile ) . "\n"; // Term box $out .= Html::input( 'search', $term, 'search', array( - 'id' => $this->profile === 'advanced' ? 'powerSearchText' : 'searchText', + 'id' => $this->isPowerSearch() ? 'powerSearchText' : 'searchText', 'size' => '50', - 'autofocus', + 'autofocus' => trim( $term ) === '', 'class' => 'mw-ui-input mw-ui-input-inline', ) ) . "\n"; $out .= Html::hidden( 'fulltext', 'Search' ) . "\n"; - $out .= Xml::submitButton( + $out .= Html::submitButton( $this->msg( 'searchbutton' )->text(), - array( 'class' => array( 'mw-ui-button', 'mw-ui-progressive' ) ) + array( 'class' => 'mw-ui-button mw-ui-progressive' ), + array( 'mw-ui-progressive' ) ) . "\n"; // Results-info @@ -1075,7 +1098,7 @@ class SpecialSearch extends SpecialPage { Xml::element( 'div', array( 'style' => 'clear:both' ), '', false ); } - return $out . $this->didYouMeanHtml; + return $out; } /** diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php index 782d9a17..7ec69e06 100644 --- a/includes/specials/SpecialShortpages.php +++ b/includes/specials/SpecialShortpages.php @@ -58,7 +58,7 @@ class ShortPagesPage extends QueryPage { } /** - * @param DatabaseBase $db + * @param IDatabase $db * @param ResultWrapper $res */ function preprocessResults( $db, $res ) { diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php index eff06f46..eaa90072 100644 --- a/includes/specials/SpecialSpecialpages.php +++ b/includes/specials/SpecialSpecialpages.php @@ -45,6 +45,7 @@ class SpecialSpecialpages extends UnlistedSpecialPage { return; } + $this->addHelpLink( 'Help:Special pages' ); $this->outputPageList( $groups ); } diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php index f0e360e8..c35de241 100644 --- a/includes/specials/SpecialStatistics.php +++ b/includes/specials/SpecialStatistics.php @@ -28,7 +28,7 @@ * @ingroup SpecialPage */ class SpecialStatistics extends SpecialPage { - private $views, $edits, $good, $images, $total, $users, + private $edits, $good, $images, $total, $users, $activeUsers = 0; public function __construct() { @@ -38,13 +38,11 @@ class SpecialStatistics extends SpecialPage { public function execute( $par ) { global $wgMemc; - $disableCounters = $this->getConfig()->get( 'DisableCounters' ); $miserMode = $this->getConfig()->get( 'MiserMode' ); $this->setHeaders(); $this->getOutput()->addModuleStyles( 'mediawiki.special' ); - $this->views = SiteStats::views(); $this->edits = SiteStats::edits(); $this->good = SiteStats::articles(); $this->images = SiteStats::images(); @@ -53,12 +51,6 @@ class SpecialStatistics extends SpecialPage { $this->activeUsers = SiteStats::activeUsers(); $this->hook = ''; - # Staticic - views - $viewsStats = ''; - if ( !$disableCounters ) { - $viewsStats = $this->getViewsStats(); - } - # Set active user count if ( !$miserMode ) { $key = wfMemcKey( 'sitestats', 'activeusers-updated' ); @@ -83,16 +75,10 @@ class SpecialStatistics extends SpecialPage { # Statistic - usergroups $text .= $this->getGroupStats(); - $text .= $viewsStats; - - # Statistic - popular pages - if ( !$disableCounters && !$miserMode ) { - $text .= $this->getMostViewedPages(); - } # Statistic - other $extraStats = array(); - if ( wfRunHooks( 'SpecialStatsAddExtra', array( &$extraStats ) ) ) { + if ( Hooks::run( 'SpecialStatsAddExtra', array( &$extraStats, $this->getContext() ) ) ) { $text .= $this->getOtherStats( $extraStats ); } @@ -213,10 +199,16 @@ class SpecialStatistics extends SpecialPage { $grouppageLocalized = $msg->text(); } $linkTarget = Title::newFromText( $grouppageLocalized ); - $grouppage = Linker::link( - $linkTarget, - htmlspecialchars( $groupnameLocalized ) - ); + + if ( $linkTarget ) { + $grouppage = Linker::link( + $linkTarget, + htmlspecialchars( $groupnameLocalized ) + ); + } else { + $grouppage = htmlspecialchars( $groupnameLocalized ); + } + $grouplink = Linker::linkKnown( SpecialPage::getTitleFor( 'Listusers' ), $this->msg( 'listgrouprights-members' )->escaped(), @@ -237,63 +229,6 @@ class SpecialStatistics extends SpecialPage { return $text; } - private function getViewsStats() { - return Xml::openElement( 'tr' ) . - Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-views' )->parse() ) . - Xml::closeElement( 'tr' ) . - $this->formatRow( $this->msg( 'statistics-views-total' )->parse(), - $this->getLanguage()->formatNum( $this->views ), - array( 'class' => 'mw-statistics-views-total' ), 'statistics-views-total-desc' ) . - $this->formatRow( $this->msg( 'statistics-views-peredit' )->parse(), - $this->getLanguage()->formatNum( sprintf( '%.2f', $this->edits ? - $this->views / $this->edits : 0 ) ), - array( 'class' => 'mw-statistics-views-peredit' ) ); - } - - private function getMostViewedPages() { - $text = ''; - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( - 'page', - array( - 'page_namespace', - 'page_title', - 'page_counter', - ), - array( - 'page_is_redirect' => 0, - 'page_counter > 0', - ), - __METHOD__, - array( - 'ORDER BY' => 'page_counter DESC', - 'LIMIT' => 10, - ) - ); - - if ( $res->numRows() > 0 ) { - $text .= Xml::openElement( 'tr' ); - $text .= Xml::tags( - 'th', - array( 'colspan' => '2' ), - $this->msg( 'statistics-mostpopular' )->parse() - ); - $text .= Xml::closeElement( 'tr' ); - - foreach ( $res as $row ) { - $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); - - if ( $title instanceof Title ) { - $text .= $this->formatRow( Linker::link( $title ), - $this->getLanguage()->formatNum( $row->page_counter ) ); - } - } - $res->free(); - } - - return $text; - } - /** * Conversion of external statistics into an internal representation * Following a ([<header-message>][<item-message>] = number) pattern @@ -315,12 +250,17 @@ class SpecialStatistics extends SpecialPage { // Collect all items that belong to the same header foreach ( $items as $key => $value ) { - $name = $this->msg( $key )->parse(); - $number = htmlspecialchars( $value ); + if ( is_array( $value ) ) { + $name = $value['name']; + $number = $value['number']; + } else { + $name = $this->msg( $key )->parse(); + $number = $value; + } $return .= $this->formatRow( $name, - $this->getLanguage()->formatNum( $number ), + $this->getLanguage()->formatNum( htmlspecialchars( $number ) ), array( 'class' => 'mw-statistics-hook', 'id' => 'mw-' . $key ) ); } diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php index b7627285..0b8147e1 100644 --- a/includes/specials/SpecialTags.php +++ b/includes/specials/SpecialTags.php @@ -31,6 +31,10 @@ class SpecialTags extends SpecialPage { * @var array List of defined tags */ public $definedTags; + /** + * @var array List of active tags + */ + public $activeTags; function __construct() { parent::__construct( 'Tags' ); @@ -40,33 +44,114 @@ class SpecialTags extends SpecialPage { $this->setHeaders(); $this->outputHeader(); + $request = $this->getRequest(); + switch ( $par ) { + case 'delete': + $this->showDeleteTagForm( $request->getVal( 'tag' ) ); + break; + case 'activate': + $this->showActivateDeactivateForm( $request->getVal( 'tag' ), true ); + break; + case 'deactivate': + $this->showActivateDeactivateForm( $request->getVal( 'tag' ), false ); + break; + case 'create': + // fall through, thanks to HTMLForm's logic + default: + $this->showTagList(); + break; + } + } + + function showTagList() { $out = $this->getOutput(); $out->setPageTitle( $this->msg( 'tags-title' ) ); $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' ); + $user = $this->getUser(); + + // Show form to create a tag + if ( $user->isAllowed( 'managechangetags' ) ) { + $fields = array( + 'Tag' => array( + 'type' => 'text', + 'label' => $this->msg( 'tags-create-tag-name' )->plain(), + 'required' => true, + ), + 'Reason' => array( + 'type' => 'text', + 'label' => $this->msg( 'tags-create-reason' )->plain(), + 'size' => 50, + ), + 'IgnoreWarnings' => array( + 'type' => 'hidden', + ), + ); + + $form = new HTMLForm( $fields, $this->getContext() ); + $form->setAction( $this->getPageTitle( 'create' )->getLocalURL() ); + $form->setWrapperLegendMsg( 'tags-create-heading' ); + $form->setHeaderText( $this->msg( 'tags-create-explanation' )->plain() ); + $form->setSubmitCallback( array( $this, 'processCreateTagForm' ) ); + $form->setSubmitTextMsg( 'tags-create-submit' ); + $form->show(); + + // If processCreateTagForm generated a redirect, there's no point + // continuing with this, as the user is just going to end up getting sent + // somewhere else. Additionally, if we keep going here, we end up + // populating the memcache of tag data (see ChangeTags::listDefinedTags) + // with out-of-date data from the slave, because the slave hasn't caught + // up to the fact that a new tag has been created as part of an implicit, + // as yet uncommitted transaction on master. + if ( $out->getRedirect() !== '' ) { + return; + } + } + + // Whether to show the "Actions" column in the tag list + // If any actions added in the future require other user rights, add those + // rights here + $showActions = $user->isAllowed( 'managechangetags' ); + // Write the headers + $tagUsageStatistics = ChangeTags::tagUsageStatistics(); + + // Show header only if there exists atleast one tag + if ( !$tagUsageStatistics ) { + return; + } $html = Xml::tags( 'tr', null, Xml::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) . Xml::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) . Xml::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) . + Xml::tags( 'th', null, $this->msg( 'tags-source-header' )->parse() ) . Xml::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) . - Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) + Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) . + ( $showActions ? + Xml::tags( 'th', array( 'class' => 'unsortable' ), + $this->msg( 'tags-actions-header' )->parse() ) : + '' ) ); // Used in #doTagRow() - $this->definedTags = array_fill_keys( ChangeTags::listDefinedTags(), true ); + $this->explicitlyDefinedTags = array_fill_keys( + ChangeTags::listExplicitlyDefinedTags(), true ); + $this->extensionDefinedTags = array_fill_keys( + ChangeTags::listExtensionDefinedTags(), true ); + $this->extensionActivatedTags = array_fill_keys( + ChangeTags::listExtensionActivatedTags(), true ); - foreach ( ChangeTags::tagUsageStatistics() as $tag => $hitcount ) { - $html .= $this->doTagRow( $tag, $hitcount ); + foreach ( $tagUsageStatistics as $tag => $hitcount ) { + $html .= $this->doTagRow( $tag, $hitcount, $showActions ); } $out->addHTML( Xml::tags( 'table', - array( 'class' => 'wikitable sortable mw-tags-table' ), + array( 'class' => 'mw-datatable sortable mw-tags-table' ), $html ) ); } - function doTagRow( $tag, $hitcount ) { + function doTagRow( $tag, $hitcount, $showActions ) { $user = $this->getUser(); $newRow = ''; $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) ); @@ -94,9 +179,23 @@ class SpecialTags extends SpecialPage { } $newRow .= Xml::tags( 'td', null, $desc ); - $active = isset( $this->definedTags[$tag] ) ? 'tags-active-yes' : 'tags-active-no'; - $active = $this->msg( $active )->escaped(); - $newRow .= Xml::tags( 'td', null, $active ); + $sourceMsgs = array(); + $isExtension = isset( $this->extensionDefinedTags[$tag] ); + $isExplicit = isset( $this->explicitlyDefinedTags[$tag] ); + if ( $isExtension ) { + $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped(); + } + if ( $isExplicit ) { + $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped(); + } + if ( !$sourceMsgs ) { + $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped(); + } + $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) ); + + $isActive = $isExplicit || isset( $this->extensionActivatedTags[$tag] ); + $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' ); + $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() ); $hitcountLabel = $this->msg( 'tags-hitcount' )->numParams( $hitcount )->escaped(); $hitcountLink = Linker::link( @@ -109,9 +208,228 @@ class SpecialTags extends SpecialPage { // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters $newRow .= Xml::tags( 'td', array( 'data-sort-value' => $hitcount ), $hitcountLink ); + // actions + $actionLinks = array(); + if ( $showActions ) { + // delete + if ( ChangeTags::canDeleteTag( $tag, $user )->isOK() ) { + $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'delete' ), + $this->msg( 'tags-delete' )->escaped(), + array(), + array( 'tag' => $tag ) ); + } + + // activate + if ( ChangeTags::canActivateTag( $tag, $user )->isOK() ) { + $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'activate' ), + $this->msg( 'tags-activate' )->escaped(), + array(), + array( 'tag' => $tag ) ); + } + + // deactivate + if ( ChangeTags::canDeactivateTag( $tag, $user )->isOK() ) { + $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'deactivate' ), + $this->msg( 'tags-deactivate' )->escaped(), + array(), + array( 'tag' => $tag ) ); + } + + $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) ); + } + return Xml::tags( 'tr', null, $newRow ) . "\n"; } + public function processCreateTagForm( array $data, HTMLForm $form ) { + $context = $form->getContext(); + $out = $context->getOutput(); + + $tag = trim( strval( $data['Tag'] ) ); + $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1'; + $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'], + $context->getUser(), $ignoreWarnings ); + + if ( $status->isGood() ) { + $out->redirect( $this->getPageTitle()->getLocalURL() ); + return true; + } elseif ( $status->isOK() ) { + // we have some warnings, so we show a confirmation form + $fields = array( + 'Tag' => array( + 'type' => 'hidden', + 'default' => $data['Tag'], + ), + 'Reason' => array( + 'type' => 'hidden', + 'default' => $data['Reason'], + ), + 'IgnoreWarnings' => array( + 'type' => 'hidden', + 'default' => '1', + ), + ); + + // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise + // we get into an infinite loop! + $context->getRequest()->unsetVal( 'wpEditToken' ); + + $headerText = $this->msg( 'tags-create-warnings-above', $tag, + count( $status->getWarningsArray() ) )->parseAsBlock() . + $out->parse( $status->getWikitext() ) . + $this->msg( 'tags-create-warnings-below' )->parseAsBlock(); + + $subform = new HTMLForm( $fields, $this->getContext() ); + $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() ); + $subform->setWrapperLegendMsg( 'tags-create-heading' ); + $subform->setHeaderText( $headerText ); + $subform->setSubmitCallback( array( $this, 'processCreateTagForm' ) ); + $subform->setSubmitTextMsg( 'htmlform-yes' ); + $subform->show(); + + $out->addBacklinkSubtitle( $this->getPageTitle() ); + return true; + } else { + $out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() . + "\n</div>" ); + return false; + } + } + + protected function showDeleteTagForm( $tag ) { + $user = $this->getUser(); + if ( !$user->isAllowed( 'managechangetags' ) ) { + throw new PermissionsError( 'managechangetags' ); + } + + $out = $this->getOutput(); + $out->setPageTitle( $this->msg( 'tags-delete-title' ) ); + $out->addBacklinkSubtitle( $this->getPageTitle() ); + + // is the tag actually able to be deleted? + $canDeleteResult = ChangeTags::canDeleteTag( $tag, $user ); + if ( !$canDeleteResult->isGood() ) { + $out->addWikiText( "<div class=\"error\">\n" . $canDeleteResult->getWikiText() . + "\n</div>" ); + if ( !$canDeleteResult->isOK() ) { + return; + } + } + + $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock(); + $tagUsage = ChangeTags::tagUsageStatistics(); + if ( $tagUsage[$tag] > 0 ) { + $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag, + $tagUsage[$tag] )->parseAsBlock(); + } + $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock(); + + // see if the tag is in use + $this->extensionActivatedTags = array_fill_keys( + ChangeTags::listExtensionActivatedTags(), true ); + if ( isset( $this->extensionActivatedTags[$tag] ) ) { + $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock(); + } + + $fields = array(); + $fields['Reason'] = array( + 'type' => 'text', + 'label' => $this->msg( 'tags-delete-reason' )->plain(), + 'size' => 50, + ); + $fields['HiddenTag'] = array( + 'type' => 'hidden', + 'name' => 'tag', + 'default' => $tag, + 'required' => true, + ); + + $form = new HTMLForm( $fields, $this->getContext() ); + $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() ); + $form->tagAction = 'delete'; // custom property on HTMLForm object + $form->setSubmitCallback( array( $this, 'processTagForm' ) ); + $form->setSubmitTextMsg( 'tags-delete-submit' ); + $form->setSubmitDestructive(); // nasty! + $form->addPreText( $preText ); + $form->show(); + } + + protected function showActivateDeactivateForm( $tag, $activate ) { + $actionStr = $activate ? 'activate' : 'deactivate'; + + $user = $this->getUser(); + if ( !$user->isAllowed( 'managechangetags' ) ) { + throw new PermissionsError( 'managechangetags' ); + } + + $out = $this->getOutput(); + // tags-activate-title, tags-deactivate-title + $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) ); + $out->addBacklinkSubtitle( $this->getPageTitle() ); + + // is it possible to do this? + $func = $activate ? 'canActivateTag' : 'canDeactivateTag'; + $result = ChangeTags::$func( $tag, $user ); + if ( !$result->isGood() ) { + $out->wrapWikiMsg( "<div class=\"error\">\n$1" . $result->getWikiText() . + "\n</div>" ); + if ( !$result->isOK() ) { + return; + } + } + + // tags-activate-question, tags-deactivate-question + $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock(); + + $fields = array(); + // tags-activate-reason, tags-deactivate-reason + $fields['Reason'] = array( + 'type' => 'text', + 'label' => $this->msg( "tags-$actionStr-reason" )->plain(), + 'size' => 50, + ); + $fields['HiddenTag'] = array( + 'type' => 'hidden', + 'name' => 'tag', + 'default' => $tag, + 'required' => true, + ); + + $form = new HTMLForm( $fields, $this->getContext() ); + $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() ); + $form->tagAction = $actionStr; + $form->setSubmitCallback( array( $this, 'processTagForm' ) ); + // tags-activate-submit, tags-deactivate-submit + $form->setSubmitTextMsg( "tags-$actionStr-submit" ); + $form->addPreText( $preText ); + $form->show(); + } + + public function processTagForm( array $data, HTMLForm $form ) { + $context = $form->getContext(); + $out = $context->getOutput(); + + $tag = $data['HiddenTag']; + $status = call_user_func( array( 'ChangeTags', "{$form->tagAction}TagWithChecks" ), + $tag, $data['Reason'], $context->getUser(), true ); + + if ( $status->isGood() ) { + $out->redirect( $this->getPageTitle()->getLocalURL() ); + return true; + } elseif ( $status->isOK() && $form->tagAction === 'delete' ) { + // deletion succeeded, but hooks raised a warning + $out->addWikiText( $this->msg( 'tags-delete-warnings-after-delete', $tag, + count( $status->getWarningsArray() ) )->text() . "\n" . + $status->getWikitext() ); + $out->addReturnTo( $this->getPageTitle() ); + return true; + } else { + $out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() . + "\n</div>" ); + return false; + } + } + protected function getGroupName() { return 'changes'; } diff --git a/includes/specials/SpecialTrackingCategories.php b/includes/specials/SpecialTrackingCategories.php index 552031f1..d219c99d 100644 --- a/includes/specials/SpecialTrackingCategories.php +++ b/includes/specials/SpecialTrackingCategories.php @@ -36,6 +36,24 @@ class SpecialTrackingCategories extends SpecialPage { parent::__construct( 'TrackingCategories' ); } + /** + * Tracking categories that exist in core + * + * @var array + */ + private static $coreTrackingCategories = array( + 'index-category', + 'noindex-category', + 'duplicate-args-category', + 'expensive-parserfunction-category', + 'post-expand-template-argument-category', + 'post-expand-template-inclusion-category', + 'hidden-category-category', + 'broken-file-category', + 'node-count-exceeded-category', + 'expansion-depth-exceeded-category', + ); + function execute( $par ) { $this->setHeaders(); $this->outputHeader(); @@ -56,23 +74,88 @@ class SpecialTrackingCategories extends SpecialPage { </tr></thead>" ); - foreach ( $this->getConfig()->get( 'TrackingCategories' ) as $catMsg ) { + $trackingCategories = $this->prepareTrackingCategoriesData(); + + $batch = new LinkBatch(); + foreach ( $trackingCategories as $catMsg => $data ) { + $batch->addObj( $data['msg'] ); + foreach ( $data['cats'] as $catTitle ) { + $batch->addObj( $catTitle ); + } + } + $batch->execute(); + + foreach ( $trackingCategories as $catMsg => $data ) { + $allMsgs = array(); + $catDesc = $catMsg . '-desc'; + + $catMsgTitleText = Linker::link( + $data['msg'], + htmlspecialchars( $catMsg ) + ); + + foreach ( $data['cats'] as $catTitle ) { + $catTitleText = Linker::link( + $catTitle, + htmlspecialchars( $catTitle->getText() ) + ); + $allMsgs[] = $catTitleText; + } + + # Extra message, when no category was found + if ( !count( $allMsgs ) ) { + $allMsgs[] = $this->msg( 'trackingcategories-disabled' )->parse(); + } + + /* + * Show category description if it exists as a system message + * as category-name-desc + */ + $descMsg = $this->msg( $catDesc ); + if ( $descMsg->isBlank() ) { + $descMsg = $this->msg( 'trackingcategories-nodesc' ); + } + + $this->getOutput()->addHTML( + Html::openElement( 'tr' ) . + Html::openElement( 'td', array( 'class' => 'mw-trackingcategories-name' ) ) . + $this->getLanguage()->commaList( array_unique( $allMsgs ) ) . + Html::closeElement( 'td' ) . + Html::openElement( 'td', array( 'class' => 'mw-trackingcategories-msg' ) ) . + $catMsgTitleText . + Html::closeElement( 'td' ) . + Html::openElement( 'td', array( 'class' => 'mw-trackingcategories-desc' ) ) . + $descMsg->parse() . + Html::closeElement( 'td' ) . + Html::closeElement( 'tr' ) + ); + } + $this->getOutput()->addHTML( Html::closeElement( 'table' ) ); + } + + /** + * Read the global and extract title objects from the corresponding messages + * @return array Array( 'msg' => Title, 'cats' => Title[] ) + */ + private function prepareTrackingCategoriesData() { + $categories = array_merge( + self::$coreTrackingCategories, + ExtensionRegistry::getInstance()->getAttribute( 'TrackingCategories' ), + $this->getConfig()->get( 'TrackingCategories' ) // deprecated + ); + $trackingCategories = array(); + foreach ( $categories as $catMsg ) { /* * Check if the tracking category varies by namespace * Otherwise only pages in the current namespace will be displayed * If it does vary, show pages considering all namespaces */ $msgObj = $this->msg( $catMsg )->inContentLanguage(); - $allMsgs = array(); - $catDesc = $catMsg . '-desc'; + $allCats = array(); $catMsgTitle = Title::makeTitleSafe( NS_MEDIAWIKI, $catMsg ); if ( !$catMsgTitle ) { continue; } - $catMsgTitleText = Linker::link( - $catMsgTitle, - htmlspecialchars( $catMsg ) - ); // Match things like {{NAMESPACE}} and {{NAMESPACENUMBER}}. // False positives are ok, this is just an efficiency shortcut @@ -88,11 +171,7 @@ class SpecialTrackingCategories extends SpecialPage { if ( $catName !== '-' ) { $catTitle = Title::makeTitleSafe( NS_CATEGORY, $catName ); if ( $catTitle ) { - $catTitleText = Linker::link( - $catTitle, - htmlspecialchars( $catName ) - ); - $allMsgs[] = $catTitleText; + $allCats[] = $catTitle; } } } @@ -102,44 +181,17 @@ class SpecialTrackingCategories extends SpecialPage { if ( $catName !== '-' ) { $catTitle = Title::makeTitleSafe( NS_CATEGORY, $catName ); if ( $catTitle ) { - $catTitleText = Linker::link( - $catTitle, - htmlspecialchars( $catName ) - ); - $allMsgs[] = $catTitleText; + $allCats[] = $catTitle; } } } - - # Extra message, when no category was found - if ( !count( $allMsgs ) ) { - $allMsgs[] = $this->msg( 'trackingcategories-disabled' )->parse(); - } - - /* - * Show category description if it exists as a system message - * as category-name-desc - */ - $descMsg = $this->msg( $catDesc ); - if ( $descMsg->isBlank() ) { - $descMsg = $this->msg( 'trackingcategories-nodesc' ); - } - - $this->getOutput()->addHTML( - Html::openElement( 'tr' ) . - Html::openElement( 'td', array( 'class' => 'mw-trackingcategories-name' ) ) . - $this->getLanguage()->commaList( array_unique( $allMsgs ) ) . - Html::closeElement( 'td' ) . - Html::openElement( 'td', array( 'class' => 'mw-trackingcategories-msg' ) ) . - $catMsgTitleText . - Html::closeElement( 'td' ) . - Html::openElement( 'td', array( 'class' => 'mw-trackingcategories-desc' ) ) . - $descMsg->parse() . - Html::closeElement( 'td' ) . - Html::closeElement( 'tr' ) + $trackingCategories[$catMsg] = array( + 'cats' => $allCats, + 'msg' => $catMsgTitle, ); } - $this->getOutput()->addHTML( Html::closeElement( 'table' ) ); + + return $trackingCategories; } protected function getGroupName() { diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php index 244b8894..f81f1c30 100644 --- a/includes/specials/SpecialUnblock.php +++ b/includes/specials/SpecialUnblock.php @@ -53,7 +53,7 @@ class SpecialUnblock extends SpecialPage { $out = $this->getOutput(); $out->setPageTitle( $this->msg( 'unblockip' ) ); - $out->addModules( 'mediawiki.special' ); + $out->addModules( array( 'mediawiki.special', 'mediawiki.userSuggest' ) ); $form = new HTMLForm( $this->getFields(), $this->getContext() ); $form->setWrapperLegendMsg( 'unblockip' ); @@ -88,6 +88,7 @@ class SpecialUnblock extends SpecialPage { 'autofocus' => true, 'size' => '45', 'required' => true, + 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest ), 'Name' => array( 'type' => 'info', @@ -226,8 +227,12 @@ class SpecialUnblock extends SpecialPage { } # Make log entry - $log = new LogPage( 'block' ); - $log->addEntry( 'unblock', $page, $data['Reason'], array(), $performer ); + $logEntry = new ManualLogEntry( 'block', 'unblock' ); + $logEntry->setTarget( $page ); + $logEntry->setComment( $data['Reason'] ); + $logEntry->setPerformer( $performer ); + $logId = $logEntry->insert(); + $logEntry->publish( $logId ); return true; } diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index c3e871b8..f2362a18 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -94,7 +94,7 @@ class PageArchive { } /** - * @param DatabaseBase $dbr + * @param IDatabase $dbr * @param string|array $condition * @return bool|ResultWrapper */ @@ -370,6 +370,7 @@ class PageArchive { if ( $restoreFiles && $this->title->getNamespace() == NS_FILE ) { $img = wfLocalFile( $this->title ); + $img->load( File::READ_LATEST ); $this->fileStatus = $img->restore( $fileVersions, $unsuppress ); if ( !$this->fileStatus->isOK() ) { return false; @@ -421,7 +422,7 @@ class PageArchive { $logEntry->setTarget( $this->title ); $logEntry->setComment( $reason ); - wfRunHooks( 'ArticleUndeleteLogEntry', array( $this, &$logEntry, $user ) ); + Hooks::run( 'ArticleUndeleteLogEntry', array( $this, &$logEntry, $user ) ); $logid = $logEntry->insert(); $logEntry->publish( $logid ); @@ -550,7 +551,7 @@ class PageArchive { 'title' => $article->getTitle(), // used to derive default content model ) ); - $user = User::newFromName( $revision->getRawUserText(), false ); + $user = User::newFromName( $revision->getUserText( Revision::RAW ), false ); $content = $revision->getContent( Revision::RAW ); //NOTE: article ID may not be known yet. prepareSave() should not modify the database. @@ -605,7 +606,7 @@ class PageArchive { $revision->insertOn( $dbw ); $restored++; - wfRunHooks( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) ); + Hooks::run( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) ); } # Now that it's safely stored, take it out of the archive $dbw->delete( 'archive', @@ -623,7 +624,7 @@ class PageArchive { $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId ); if ( $created || $wasnew ) { // Update site stats, link tables, etc - $user = User::newFromName( $revision->getRawUserText(), false ); + $user = User::newFromName( $revision->getUserText( Revision::RAW ), false ); $article->doEditUpdates( $revision, $user, @@ -631,7 +632,7 @@ class PageArchive { ); } - wfRunHooks( 'ArticleUndelete', array( &$this->title, $created, $comment, $oldPageId ) ); + Hooks::run( 'ArticleUndelete', array( &$this->title, $created, $comment, $oldPageId ) ); if ( $this->title->getNamespace() == NS_FILE ) { $update = new HTMLCacheUpdate( $this->title, 'imagelinks' ); @@ -790,6 +791,7 @@ class SpecialUndelete extends SpecialPage { return; } + $this->addHelpLink( 'Help:Undelete' ); if ( $this->mAllowed ) { $out->setPageTitle( $this->msg( 'undeletepage' ) ); } else { @@ -839,7 +841,7 @@ class SpecialUndelete extends SpecialPage { 'prefix', 20, $this->mSearchPrefix, - array( 'id' => 'prefix', 'autofocus' => true ) + array( 'id' => 'prefix', 'autofocus' => '' ) ) . ' ' . Xml::submitButton( $this->msg( 'undelete-search-submit' )->text() ) . Xml::closeElement( 'fieldset' ) . @@ -908,7 +910,7 @@ class SpecialUndelete extends SpecialPage { } $archive = new PageArchive( $this->mTargetObj, $this->getConfig() ); - if ( !wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ) ) { + if ( !Hooks::run( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ) ) { return; } $rev = $archive->getRevision( $timestamp ); @@ -992,7 +994,7 @@ class SpecialUndelete extends SpecialPage { $out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params( $time )->rawParams( $userLink )->params( $d, $t )->parse() . '</div>' ); - if ( !wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ) ) { + if ( !Hooks::run( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ) ) { return; } @@ -1215,7 +1217,7 @@ class SpecialUndelete extends SpecialPage { ); $archive = new PageArchive( $this->mTargetObj, $this->getConfig() ); - wfRunHooks( 'UndeleteForm::showHistory', array( &$archive, $this->mTargetObj ) ); + Hooks::run( 'UndeleteForm::showHistory', array( &$archive, $this->mTargetObj ) ); /* $text = $archive->getLastRevisionText(); if( is_null( $text ) ) { @@ -1312,7 +1314,7 @@ class SpecialUndelete extends SpecialPage { 'wpComment', 50, $this->mComment, - array( 'id' => 'wpComment', 'autofocus' => true ) + array( 'id' => 'wpComment', 'autofocus' => '' ) ) . "</td> </tr> @@ -1628,7 +1630,9 @@ class SpecialUndelete extends SpecialPage { } function undelete() { - if ( $this->getConfig()->get( 'UploadMaintenance' ) && $this->mTargetObj->getNamespace() == NS_FILE ) { + if ( $this->getConfig()->get( 'UploadMaintenance' ) + && $this->mTargetObj->getNamespace() == NS_FILE + ) { throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' ); } @@ -1638,7 +1642,7 @@ class SpecialUndelete extends SpecialPage { $out = $this->getOutput(); $archive = new PageArchive( $this->mTargetObj, $this->getConfig() ); - wfRunHooks( 'UndeleteForm::undelete', array( &$archive, $this->mTargetObj ) ); + Hooks::run( 'UndeleteForm::undelete', array( &$archive, $this->mTargetObj ) ); $ok = $archive->undelete( $this->mTargetTimestamp, $this->mComment, @@ -1649,7 +1653,7 @@ class SpecialUndelete extends SpecialPage { if ( is_array( $ok ) ) { if ( $ok[1] ) { // Undeleted file count - wfRunHooks( 'FileUndeleteComplete', array( + Hooks::run( 'FileUndeleteComplete', array( $this->mTargetObj, $this->mFileVersions, $this->getUser(), $this->mComment ) ); } diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index 55d09dd6..640562e4 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -143,6 +143,13 @@ class SpecialUpload extends SpecialPage { /** * Special page entry point * @param string $par + * @throws ErrorPageError + * @throws Exception + * @throws FatalError + * @throws MWException + * @throws PermissionsError + * @throws ReadOnlyError + * @throws UserBlockedError */ public function execute( $par ) { $this->setHeaders(); @@ -153,6 +160,8 @@ class SpecialUpload extends SpecialPage { throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' ); } + $this->addHelpLink( 'Help:Managing files' ); + # Check permissions $user = $this->getUser(); $permissionRequired = UploadBase::isAllowed( $user ); @@ -186,7 +195,7 @@ class SpecialUpload extends SpecialPage { $this->processUpload(); } else { # Backwards compatibility hook - if ( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) { + if ( !Hooks::run( 'UploadForm:initial', array( &$this ) ) ) { wfDebug( "Hook 'UploadForm:initial' broke output of the upload form\n" ); return; @@ -414,7 +423,7 @@ class SpecialUpload extends SpecialPage { return; } - if ( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) ) { + if ( !Hooks::run( 'UploadForm:BeforeProcessing', array( &$this ) ) ) { wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" ); // This code path is deprecated. If you want to break upload processing // do so by hooking into the appropriate hooks in UploadBase::verifyUpload @@ -454,7 +463,7 @@ class SpecialUpload extends SpecialPage { // Get the page text if this is not a reupload if ( !$this->mForReUpload ) { $pageText = self::getInitialPageText( $this->mComment, $this->mLicense, - $this->mCopyrightStatus, $this->mCopyrightSource ); + $this->mCopyrightStatus, $this->mCopyrightSource, $this->getConfig() ); } else { $pageText = false; } @@ -474,7 +483,7 @@ class SpecialUpload extends SpecialPage { // Success, redirect to description page $this->mUploadSuccessful = true; - wfRunHooks( 'SpecialUploadComplete', array( &$this ) ); + Hooks::run( 'SpecialUploadComplete', array( &$this ) ); $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() ); } @@ -484,28 +493,32 @@ class SpecialUpload extends SpecialPage { * @param string $license * @param string $copyStatus * @param string $source + * @param Config $config Configuration object to load data from * @return string - * @todo Use Config obj instead of globals */ public static function getInitialPageText( $comment = '', $license = '', - $copyStatus = '', $source = '' + $copyStatus = '', $source = '', Config $config = null ) { - global $wgUseCopyrightUpload, $wgForceUIMsgAsContentMsg; + if ( $config === null ) { + wfDebug( __METHOD__ . ' called without a Config instance passed to it' ); + $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); + } $msg = array(); + $forceUIMsgAsContentMsg = (array)$config->get( 'ForceUIMsgAsContentMsg' ); /* These messages are transcluded into the actual text of the description page. * Thus, forcing them as content messages makes the upload to produce an int: template * instead of hardcoding it there in the uploader language. */ foreach ( array( 'license-header', 'filedesc', 'filestatus', 'filesource' ) as $msgName ) { - if ( in_array( $msgName, (array)$wgForceUIMsgAsContentMsg ) ) { + if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) { $msg[$msgName] = "{{int:$msgName}}"; } else { $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text(); } } - if ( $wgUseCopyrightUpload ) { + if ( $config->get( 'UseCopyrightUpload' ) ) { $licensetxt = ''; if ( $license != '' ) { $licensetxt = '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n"; @@ -731,8 +744,8 @@ class SpecialUpload extends SpecialPage { } return '<li>' . - wfMessage( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() . - $gallery->toHtml() . "</li>\n"; + $this->msg( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() . + $gallery->toHTML() . "</li>\n"; } protected function getGroupName() { @@ -746,7 +759,7 @@ class SpecialUpload extends SpecialPage { * * @todo What about non-BitmapHandler handled files? */ - static public function rotationEnabled() { + public static function rotationEnabled() { $bitmapHandler = new BitmapHandler(); return $bitmapHandler->autoRotateEnabled(); } @@ -795,7 +808,7 @@ class UploadForm extends HTMLForm { + $this->getDescriptionSection() + $this->getOptionsSection(); - wfRunHooks( 'UploadFormInitDescriptor', array( &$descriptor ) ); + Hooks::run( 'UploadFormInitDescriptor', array( &$descriptor ) ); parent::__construct( $descriptor, $context, 'upload' ); # Add a link to edit MediaWik:Licenses @@ -872,6 +885,17 @@ class UploadForm extends HTMLForm { ); } + $help = $this->msg( 'upload-maxfilesize', + $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] ) + )->parse(); + + // If the user can also upload by URL, there are 2 different file size limits. + // This extra message helps stress which limit corresponds to what. + if ( $canUploadByUrl ) { + $help .= $this->msg( 'word-separator' )->escaped(); + $help .= $this->msg( 'upload_source_file' )->parse(); + } + $descriptor['UploadFile'] = array( 'class' => 'UploadSourceField', 'section' => 'source', @@ -881,11 +905,7 @@ class UploadForm extends HTMLForm { 'label-message' => 'sourcefilename', 'upload-type' => 'File', 'radio' => &$radio, - 'help' => $this->msg( 'upload-maxfilesize', - $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] ) - )->parse() . - $this->msg( 'word-separator' )->escaped() . - $this->msg( 'upload_source_file' )->escaped(), + 'help' => $help, 'checked' => $selectedSourceType == 'file', ); @@ -903,11 +923,11 @@ class UploadForm extends HTMLForm { $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] ) )->parse() . $this->msg( 'word-separator' )->escaped() . - $this->msg( 'upload_source_url' )->escaped(), + $this->msg( 'upload_source_url' )->parse(), 'checked' => $selectedSourceType == 'url', ); } - wfRunHooks( 'UploadFormSourceDescriptors', array( &$descriptor, &$radio, $selectedSourceType ) ); + Hooks::run( 'UploadFormSourceDescriptors', array( &$descriptor, &$radio, $selectedSourceType ) ); $descriptor['Extensions'] = array( 'type' => 'info', @@ -930,35 +950,31 @@ class UploadForm extends HTMLForm { $config = $this->getConfig(); if ( $config->get( 'CheckFileExtensions' ) ) { + $fileExtensions = array_unique( $config->get( 'FileExtensions' ) ); if ( $config->get( 'StrictFileExtensions' ) ) { # Everything not permitted is banned $extensionsList = '<div id="mw-upload-permitted">' . - $this->msg( - 'upload-permitted', - $this->getContext()->getLanguage()->commaList( - array_unique( $config->get( 'FileExtensions' ) ) - ) - )->parseAsBlock() . + $this->msg( 'upload-permitted' ) + ->params( $this->getLanguage()->commaList( $fileExtensions ) ) + ->numParams( count( $fileExtensions ) ) + ->parseAsBlock() . "</div>\n"; } else { # We have to list both preferred and prohibited + $fileBlacklist = array_unique( $config->get( 'FileBlacklist' ) ); $extensionsList = '<div id="mw-upload-preferred">' . - $this->msg( - 'upload-preferred', - $this->getContext()->getLanguage()->commaList( - array_unique( $config->get( 'FileExtensions' ) ) - ) - )->parseAsBlock() . + $this->msg( 'upload-preferred' ) + ->params( $this->getLanguage()->commaList( $fileExtensions ) ) + ->numParams( count( $fileExtensions ) ) + ->parseAsBlock() . "</div>\n" . '<div id="mw-upload-prohibited">' . - $this->msg( - 'upload-prohibited', - $this->getContext()->getLanguage()->commaList( - array_unique( $config->get( 'FileBlacklist' ) ) - ) - )->parseAsBlock() . + $this->msg( 'upload-prohibited' ) + ->params( $this->getLanguage()->commaList( $fileBlacklist ) ) + ->numParams( count( $fileBlacklist ) ) + ->parseAsBlock() . "</div>\n"; } } else { @@ -978,10 +994,10 @@ class UploadForm extends HTMLForm { protected function getDescriptionSection() { $config = $this->getConfig(); if ( $this->mSessionKey ) { - $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash(); + $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() ); try { $file = $stash->getFile( $this->mSessionKey ); - } catch ( MWException $e ) { + } catch ( Exception $e ) { $file = null; } if ( $file ) { @@ -1139,9 +1155,12 @@ class UploadForm extends HTMLForm { // the wpDestFile textbox $this->mDestFile === '', 'wgUploadSourceIds' => $this->mSourceIds, + 'wgCheckFileExtensions' => $config->get( 'CheckFileExtensions' ), 'wgStrictFileExtensions' => $config->get( 'StrictFileExtensions' ), + 'wgFileExtensions' => array_values( array_unique( $config->get( 'FileExtensions' ) ) ), 'wgCapitalizeUploads' => MWNamespace::isCapitalized( NS_FILE ), 'wgMaxUploadSize' => $this->mMaxUploadSize, + 'wgFileCanRotate' => SpecialUpload::rotationEnabled(), ); $out = $this->getOutput(); diff --git a/includes/specials/SpecialUploadStash.php b/includes/specials/SpecialUploadStash.php index ddb435d9..12e103e7 100644 --- a/includes/specials/SpecialUploadStash.php +++ b/includes/specials/SpecialUploadStash.php @@ -48,10 +48,6 @@ class SpecialUploadStash extends UnlistedSpecialPage { public function __construct() { parent::__construct( 'UploadStash', 'upload' ); - try { - $this->stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash(); - } catch ( UploadStashNotAvailableException $e ) { - } } /** @@ -62,6 +58,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { * @return bool Success */ public function execute( $subPage ) { + $this->stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() ); $this->checkPermissions(); if ( $subPage === null || $subPage === '' ) { @@ -250,9 +247,9 @@ class SpecialUploadStash extends UnlistedSpecialPage { // make a curl call to the scaler to create a thumbnail $httpOptions = array( 'method' => 'GET', - 'timeout' => 'default' + 'timeout' => 5 // T90599 attempt to time out cleanly ); - $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions ); + $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions, __METHOD__ ); $status = $req->execute(); if ( !$status->isOK() ) { $errors = $status->getErrorsArray(); @@ -331,11 +328,12 @@ class SpecialUploadStash extends UnlistedSpecialPage { * This works, because there really is only one stash per logged-in user, despite appearances. * * @param array $formData + * @param HTMLForm $form * @return Status */ - public static function tryClearStashedUploads( $formData ) { + public static function tryClearStashedUploads( $formData, $form ) { if ( isset( $formData['Clear'] ) ) { - $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash(); + $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $form->getUser() ); wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) . "\n" ); if ( !$stash->clear() ) { diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php index 24b636b1..10edbcfb 100644 --- a/includes/specials/SpecialUserlogin.php +++ b/includes/specials/SpecialUserlogin.php @@ -105,9 +105,27 @@ class LoginForm extends SpecialPage { * @param WebRequest $request */ public function __construct( $request = null ) { + global $wgUseMediaWikiUIEverywhere; parent::__construct( 'Userlogin' ); $this->mOverrideRequest = $request; + // Override UseMediaWikiEverywhere to true, to force login and create form to use mw ui + $wgUseMediaWikiUIEverywhere = true; + } + + /** + * Returns an array of all valid error messages. + * + * @return array + */ + public static function getValidErrorMessages() { + static $messages = null; + if ( !$messages ) { + $messages = self::$validErrorMessages; + Hooks::run( 'LoginFormValidErrorMessages', array( &$messages ) ); + } + + return $messages; } /** @@ -142,7 +160,8 @@ class LoginForm extends SpecialPage { $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' ); $this->mAction = $request->getVal( 'action' ); $this->mRemember = $request->getCheck( 'wpRemember' ); - $this->mFromHTTP = $request->getBool( 'fromhttp', false ); + $this->mFromHTTP = $request->getBool( 'fromhttp', false ) + || $request->getBool( 'wpFromhttp', false ); $this->mStickHTTPS = ( !$this->mFromHTTP && $request->getProtocol() === 'https' ) || $request->getBool( 'wpForceHttps', false ); $this->mLanguage = $request->getText( 'uselang' ); @@ -172,13 +191,13 @@ class LoginForm extends SpecialPage { // Only show valid error or warning messages. if ( $entryError->exists() - && in_array( $entryError->getKey(), self::$validErrorMessages ) + && in_array( $entryError->getKey(), self::getValidErrorMessages() ) ) { $this->mEntryErrorType = 'error'; $this->mEntryError = $entryError->rawParams( $loginreqlink )->escaped(); } elseif ( $entryWarning->exists() - && in_array( $entryWarning->getKey(), self::$validErrorMessages ) + && in_array( $entryWarning->getKey(), self::getValidErrorMessages() ) ) { $this->mEntryErrorType = 'warning'; $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->escaped(); @@ -333,7 +352,7 @@ class LoginForm extends SpecialPage { $u->saveSettings(); $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' ); - wfRunHooks( 'AddNewAccount', array( $u, true ) ); + Hooks::run( 'AddNewAccount', array( $u, true ) ); $u->addNewUserLogEntry( 'byemail', $this->mReason ); $out = $this->getOutput(); @@ -408,7 +427,7 @@ class LoginForm extends SpecialPage { // which is needed or the personal links will be // wrong. $this->getContext()->setUser( $u ); - wfRunHooks( 'AddNewAccount', array( $u, false ) ); + Hooks::run( 'AddNewAccount', array( $u, false ) ); $u->addNewUserLogEntry( 'create' ); if ( $this->hasSessionCookie() ) { $this->successfulCreation(); @@ -420,7 +439,7 @@ class LoginForm extends SpecialPage { $out->setPageTitle( $this->msg( 'accountcreated' ) ); $out->addWikiMsg( 'accountcreatedtext', $u->getName() ); $out->addReturnTo( $this->getPageTitle() ); - wfRunHooks( 'AddNewAccount', array( $u, false ) ); + Hooks::run( 'AddNewAccount', array( $u, false ) ); $u->addNewUserLogEntry( 'create2', $this->mReason ); } @@ -509,20 +528,8 @@ class LoginForm extends SpecialPage { return Status::newFatal( 'sorbs_create_account_reason' ); } - // Normalize the name so that silly things don't cause "invalid username" - // errors. User::newFromName does some rather strict checking, rejecting - // e.g. leading/trailing/multiple spaces. But first we need to reject - // usernames that would be treated as titles with a fragment part. - if ( strpos( $this->mUsername, '#' ) !== false ) { - return Status::newFatal( 'noname' ); - } - $title = Title::makeTitleSafe( NS_USER, $this->mUsername ); - if ( !is_object( $title ) ) { - return Status::newFatal( 'noname' ); - } - # Now create a dummy user ($u) and check if it is valid - $u = User::newFromName( $title->getText(), 'creatable' ); + $u = User::newFromName( $this->mUsername, 'creatable' ); if ( !is_object( $u ) ) { return Status::newFatal( 'noname' ); } elseif ( 0 != $u->idForName() ) { @@ -563,7 +570,7 @@ class LoginForm extends SpecialPage { $abortError = ''; $abortStatus = null; - if ( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError, &$abortStatus ) ) ) { + if ( !Hooks::run( 'AbortNewAccount', array( $u, &$abortError, &$abortStatus ) ) ) { // Hook point to add extra creation throttles and blocks wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" ); if ( $abortStatus === null ) { @@ -583,7 +590,7 @@ class LoginForm extends SpecialPage { } // Hook point to check for exempt from account creation throttle - if ( !wfRunHooks( 'ExemptFromAccountCreationThrottle', array( $ip ) ) ) { + if ( !Hooks::run( 'ExemptFromAccountCreationThrottle', array( $ip ) ) ) { wfDebug( "LoginForm::exemptFromAccountCreationThrottle: a hook " . "allowed account creation w/o throttle\n" ); } else { @@ -706,7 +713,7 @@ class LoginForm extends SpecialPage { // Give extensions a way to indicate the username has been updated, // rather than telling the user the account doesn't exist. - if ( !wfRunHooks( 'LoginUserMigrated', array( $u, &$msg ) ) ) { + if ( !Hooks::run( 'LoginUserMigrated', array( $u, &$msg ) ) ) { $this->mAbortLoginErrorMsg = $msg; return self::USER_MIGRATED; } @@ -730,7 +737,7 @@ class LoginForm extends SpecialPage { // Give general extensions, such as a captcha, a chance to abort logins $abort = self::ABORTED; $msg = null; - if ( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort, &$msg ) ) ) { + if ( !Hooks::run( 'AbortLogin', array( $u, $this->mPassword, &$abort, &$msg ) ) ) { $this->mAbortLoginErrorMsg = $msg; return $abort; @@ -755,7 +762,7 @@ class LoginForm extends SpecialPage { // As a side-effect, we can authenticate the user's e-mail ad- // dress if it's not already done, since the temporary password // was sent via e-mail. - if ( !$u->isEmailConfirmed() ) { + if ( !$u->isEmailConfirmed() && !wfReadOnly() ) { $u->confirmEmail(); $u->saveSettings(); } @@ -791,12 +798,12 @@ class LoginForm extends SpecialPage { if ( $isAutoCreated ) { // Must be run after $wgUser is set, for correct new user log - wfRunHooks( 'AuthPluginAutoCreate', array( $u ) ); + Hooks::run( 'AuthPluginAutoCreate', array( $u ) ); } $retval = self::SUCCESS; } - wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) ); + Hooks::run( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) ); return $retval; } @@ -877,7 +884,7 @@ class LoginForm extends SpecialPage { } $abortError = ''; - if ( !wfRunHooks( 'AbortAutoAccount', array( $user, &$abortError ) ) ) { + if ( !Hooks::run( 'AbortAutoAccount', array( $user, &$abortError ) ) ) { // Hook point to add extra creation throttles and blocks wfDebug( "LoginForm::attemptAutoCreate: a hook blocked creation: $abortError\n" ); $this->mAbortLoginErrorMsg = $abortError; @@ -906,7 +913,7 @@ class LoginForm extends SpecialPage { case self::SUCCESS: # We've verified now, update the real record $user = $this->getUser(); - $user->invalidateCache(); + $user->touch(); if ( $user->requiresHTTPS() ) { $this->mStickHTTPS = true; @@ -1030,7 +1037,7 @@ class LoginForm extends SpecialPage { */ protected function resetLoginForm( Message $msg ) { // Allow hooks to explain this password reset in more detail - wfRunHooks( 'LoginPasswordResetMessage', array( &$msg, $this->mUsername ) ); + Hooks::run( 'LoginPasswordResetMessage', array( &$msg, $this->mUsername ) ); $reset = new SpecialChangePassword(); $derivative = new DerivativeContext( $this->getContext() ); $derivative->setTitle( $reset->getPageTitle() ); @@ -1063,7 +1070,7 @@ class LoginForm extends SpecialPage { } $currentUser = $this->getUser(); - wfRunHooks( 'User::mailPasswordInternal', array( &$currentUser, &$ip, &$u ) ); + Hooks::run( 'User::mailPasswordInternal', array( &$currentUser, &$ip, &$u ) ); $np = $u->randomPassword(); $u->setNewpassword( $np, $throttle ); @@ -1094,7 +1101,7 @@ class LoginForm extends SpecialPage { # Run any hooks; display injected HTML if any, else redirect $currentUser = $this->getUser(); $injected_html = ''; - wfRunHooks( 'UserLoginComplete', array( &$currentUser, &$injected_html ) ); + Hooks::run( 'UserLoginComplete', array( &$currentUser, &$injected_html ) ); if ( $injected_html !== '' ) { $this->displaySuccessfulAction( 'success', $this->msg( 'loginsuccesstitle' ), @@ -1116,14 +1123,14 @@ class LoginForm extends SpecialPage { $injected_html = ''; $welcome_creation_msg = 'welcomecreation-msg'; - wfRunHooks( 'UserLoginComplete', array( &$currentUser, &$injected_html ) ); + Hooks::run( 'UserLoginComplete', array( &$currentUser, &$injected_html ) ); /** * Let any extensions change what message is shown. * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeWelcomeCreation * @since 1.18 */ - wfRunHooks( 'BeforeWelcomeCreation', array( &$welcome_creation_msg, &$injected_html ) ); + Hooks::run( 'BeforeWelcomeCreation', array( &$welcome_creation_msg, &$injected_html ) ); $this->displaySuccessfulAction( 'signup', @@ -1233,7 +1240,7 @@ class LoginForm extends SpecialPage { } // Allow modification of redirect behavior - wfRunHooks( 'PostLoginRedirect', array( &$returnTo, &$returnToQuery, &$type ) ); + Hooks::run( 'PostLoginRedirect', array( &$returnTo, &$returnToQuery, &$type ) ); $returnToTitle = Title::newFromText( $returnTo ); if ( !$returnToTitle ) { @@ -1262,6 +1269,12 @@ class LoginForm extends SpecialPage { /** * @param string $msg * @param string $msgtype + * @throws ErrorPageError + * @throws Exception + * @throws FatalError + * @throws MWException + * @throws PermissionsError + * @throws ReadOnlyError * @private */ function mainLoginForm( $msg, $msgtype = 'error' ) { @@ -1325,7 +1338,7 @@ class LoginForm extends SpecialPage { 'mediawiki.special.userlogin.signup.styles' ) ); - $template = new UsercreateTemplate(); + $template = new UsercreateTemplate( $this->getConfig() ); // Must match number of benefits defined in messages $template->set( 'benefitCount', 3 ); @@ -1338,7 +1351,7 @@ class LoginForm extends SpecialPage { 'mediawiki.special.userlogin.login.styles' ) ); - $template = new UserloginTemplate(); + $template = new UserloginTemplate( $this->getConfig() ); $q = 'action=submitlogin&type=login'; $linkq = 'type=signup'; @@ -1420,28 +1433,26 @@ class LoginForm extends SpecialPage { } $template->set( 'secureLoginUrl', $this->mSecureLoginUrl ); - // Use loginend-https for HTTPS requests if it's not blank, loginend otherwise - // Ditto for signupend. New forms use neither. + // Use signupend-https for HTTPS requests if it's not blank, signupend otherwise $usingHTTPS = $this->mRequest->getProtocol() == 'https'; - $loginendHTTPS = $this->msg( 'loginend-https' ); $signupendHTTPS = $this->msg( 'signupend-https' ); - if ( $usingHTTPS && !$loginendHTTPS->isBlank() ) { - $template->set( 'loginend', $loginendHTTPS->parse() ); - } else { - $template->set( 'loginend', $this->msg( 'loginend' )->parse() ); - } if ( $usingHTTPS && !$signupendHTTPS->isBlank() ) { $template->set( 'signupend', $signupendHTTPS->parse() ); } else { $template->set( 'signupend', $this->msg( 'signupend' )->parse() ); } + // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved + if ( $usingHTTPS ) { + $template->set( 'fromhttp', $this->mFromHTTP ); + } + // Give authentication and captcha plugins a chance to modify the form $wgAuth->modifyUITemplate( $template, $this->mType ); if ( $this->mType == 'signup' ) { - wfRunHooks( 'UserCreateForm', array( &$template ) ); + Hooks::run( 'UserCreateForm', array( &$template ) ); } else { - wfRunHooks( 'UserLoginForm', array( &$template ) ); + Hooks::run( 'UserLoginForm', array( &$template ) ); } $out->disallowUserJs(); // just in case... diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php index d65ac852..080dc119 100644 --- a/includes/specials/SpecialUserlogout.php +++ b/includes/specials/SpecialUserlogout.php @@ -56,7 +56,7 @@ class SpecialUserlogout extends UnlistedSpecialPage { // Hook. $injected_html = ''; - wfRunHooks( 'UserLogoutComplete', array( &$user, &$injected_html, $oldName ) ); + Hooks::run( 'UserLogoutComplete', array( &$user, &$injected_html, $oldName ) ); $out->addHTML( $injected_html ); $out->returnToMain(); diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index cefdad07..758e3c05 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -106,7 +106,7 @@ class UserrightsPage extends SpecialPage { } } - if ( User::getCanonicalName( $this->mTarget ) == $user->getName() ) { + if ( User::getCanonicalName( $this->mTarget ) === $user->getName() ) { $this->isself = true; } @@ -135,6 +135,7 @@ class UserrightsPage extends SpecialPage { $out = $this->getOutput(); $out->addModuleStyles( 'mediawiki.special' ); + $this->addHelpLink( 'Help:Assigning permissions' ); // show the general form if ( count( $available['add'] ) || count( $available['remove'] ) ) { @@ -218,7 +219,7 @@ class UserrightsPage extends SpecialPage { /** * Save user groups changes in the database. * - * @param User $user + * @param User|UserRightsProxy $user * @param array $add Array of groups to add * @param array $remove Array of groups to remove * @param string $reason Reason for group change @@ -228,7 +229,7 @@ class UserrightsPage extends SpecialPage { global $wgAuth; // Validate input set... - $isself = ( $user->getName() == $this->getUser()->getName() ); + $isself = $user->getName() == $this->getUser()->getName(); $groups = $user->getGroups(); $changeable = $this->changeableGroups(); $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : array() ); @@ -244,18 +245,22 @@ class UserrightsPage extends SpecialPage { $oldGroups = $user->getGroups(); $newGroups = $oldGroups; - // remove then add groups + // Remove then add groups if ( $remove ) { - $newGroups = array_diff( $newGroups, $remove ); - foreach ( $remove as $group ) { - $user->removeGroup( $group ); + foreach ( $remove as $index => $group ) { + if ( !$user->removeGroup( $group ) ) { + unset($remove[$index]); + } } + $newGroups = array_diff( $newGroups, $remove ); } if ( $add ) { - $newGroups = array_merge( $newGroups, $add ); - foreach ( $add as $group ) { - $user->addGroup( $group ); + foreach ( $add as $index => $group ) { + if ( !$user->addGroup( $group ) ) { + unset($add[$index]); + } } + $newGroups = array_merge( $newGroups, $add ); } $newGroups = array_unique( $newGroups ); @@ -267,7 +272,7 @@ class UserrightsPage extends SpecialPage { wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) . "\n" ); wfDebug( 'newGroups: ' . print_r( $newGroups, true ) . "\n" ); - wfRunHooks( 'UserRights', array( &$user, $add, $remove ) ); + Hooks::run( 'UserRights', array( &$user, $add, $remove ) ); if ( $newGroups != $oldGroups ) { $this->addLogEntry( $user, $oldGroups, $newGroups, $reason ); @@ -415,6 +420,8 @@ class UserrightsPage extends SpecialPage { * Output a form to allow searching for a user */ function switchForm() { + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); + $this->getOutput()->addHTML( Html::openElement( 'form', @@ -433,7 +440,10 @@ class UserrightsPage extends SpecialPage { 'username', 30, str_replace( '_', ' ', $this->mTarget ), - array( 'autofocus' => true ) + array( + 'autofocus' => '', + 'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest + ) ) . ' ' . Xml::submitButton( $this->msg( 'editusergroup' )->text() ) . Html::closeElement( 'fieldset' ) . @@ -488,25 +498,32 @@ class UserrightsPage extends SpecialPage { } $language = $this->getLanguage(); - $displayedList = $this->msg( 'userrights-groupsmember-type', - $language->listToText( $list ), - $language->listToText( $membersList ) - )->plain(); - $displayedAutolist = $this->msg( 'userrights-groupsmember-type', - $language->listToText( $autoList ), - $language->listToText( $autoMembersList ) - )->plain(); + $displayedList = $this->msg( 'userrights-groupsmember-type' ) + ->rawParams( + $language->listToText( $list ), + $language->listToText( $membersList ) + )->escaped(); + $displayedAutolist = $this->msg( 'userrights-groupsmember-type' ) + ->rawParams( + $language->listToText( $autoList ), + $language->listToText( $autoMembersList ) + )->escaped(); $grouplist = ''; $count = count( $list ); if ( $count > 0 ) { - $grouplist = $this->msg( 'userrights-groupsmember', $count, $user->getName() )->parse(); + $grouplist = $this->msg( 'userrights-groupsmember' ) + ->numParams( $count ) + ->params( $user->getName() ) + ->parse(); $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n"; } $count = count( $autoList ); if ( $count > 0 ) { - $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto', $count, $user->getName() ) + $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' ) + ->numParams( $count ) + ->params( $user->getName() ) ->parse(); $grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n"; } @@ -664,9 +681,9 @@ class UserrightsPage extends SpecialPage { $member = User::getGroupMember( $group, $user->getName() ); if ( $checkbox['irreversible'] ) { - $text = $this->msg( 'userrights-irreversible-marker', $member )->escaped(); + $text = $this->msg( 'userrights-irreversible-marker', $member )->text(); } else { - $text = htmlspecialchars( $member ); + $text = $member; } $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group, "wpGroup-" . $group, $checkbox['set'], $attr ); diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php index cb3fc118..c1a95939 100644 --- a/includes/specials/SpecialVersion.php +++ b/includes/specials/SpecialVersion.php @@ -109,12 +109,7 @@ class SpecialVersion extends SpecialPage { $file = $this->getExtLicenseFileName( dirname( $extNode['path'] ) ); if ( $file ) { $wikiText = file_get_contents( $file ); - if ( !isset( $extNode['license-name'] ) ) { - // If the developer did not explicitly set license-name they probably - // are unaware that we're now sucking this file in and thus it's probably - // not wikitext friendly. - $wikiText = "<pre>$wikiText</pre>"; - } + $wikiText = "<pre>$wikiText</pre>"; } } @@ -132,6 +127,7 @@ class SpecialVersion extends SpecialPage { $out->addHtml( $this->getSkinCredits() . $this->getExtensionCredits() . + $this->getExternalLibraries() . $this->getParserTags() . $this->getParserFunctionHooks() ); @@ -191,8 +187,8 @@ class SpecialVersion extends SpecialPage { 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe', 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed', 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso', - 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', $othersLink, - $translatorsLink + 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch', + $othersLink, $translatorsLink ); return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ), @@ -220,7 +216,7 @@ class SpecialVersion extends SpecialPage { $software[$dbr->getSoftwareLink()] = $dbr->getServerInfo(); // Allow a hook to add/remove items. - wfRunHooks( 'SoftwareInfo', array( &$software ) ); + Hooks::run( 'SoftwareInfo', array( &$software ) ); $out = Xml::element( 'h2', @@ -251,7 +247,6 @@ class SpecialVersion extends SpecialPage { */ public static function getVersion( $flags = '' ) { global $wgVersion, $IP; - wfProfileIn( __METHOD__ ); $gitInfo = self::getGitHeadSha1( $IP ); $svnInfo = self::getSvnInfo( $IP ); @@ -275,8 +270,6 @@ class SpecialVersion extends SpecialPage { )->text(); } - wfProfileOut( __METHOD__ ); - return $version; } @@ -290,7 +283,6 @@ class SpecialVersion extends SpecialPage { */ public static function getVersionLinked() { global $wgVersion; - wfProfileIn( __METHOD__ ); $gitVersion = self::getVersionLinkedGit(); if ( $gitVersion ) { @@ -304,8 +296,6 @@ class SpecialVersion extends SpecialPage { } } - wfProfileOut( __METHOD__ ); - return $v; } @@ -341,7 +331,7 @@ class SpecialVersion extends SpecialPage { private static function getwgVersionLinked() { global $wgVersion; $versionUrl = ""; - if ( wfRunHooks( 'SpecialVersionVersionUrl', array( $wgVersion, &$versionUrl ) ) ) { + if ( Hooks::run( 'SpecialVersionVersionUrl', array( $wgVersion, &$versionUrl ) ) ) { $versionParts = array(); preg_match( "/^(\d+\.\d+)/", $wgVersion, $versionParts ); $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}"; @@ -402,7 +392,7 @@ class SpecialVersion extends SpecialPage { 'other' => wfMessage( 'version-other' )->text(), ); - wfRunHooks( 'ExtensionTypes', array( &self::$extensionTypes ) ); + Hooks::run( 'ExtensionTypes', array( &self::$extensionTypes ) ); } return self::$extensionTypes; @@ -504,6 +494,57 @@ class SpecialVersion extends SpecialPage { } /** + * Generate an HTML table for external libraries that are installed + * + * @return string + */ + protected function getExternalLibraries() { + global $IP; + $path = "$IP/composer.lock"; + if ( !file_exists( $path ) ) { + // Maybe they're using mediawiki/vendor? + $path = "$IP/vendor/composer.lock"; + if ( !file_exists( $path ) ) { + return ''; + } + } + + $lock = new ComposerLock( $path ); + $out = Html::element( + 'h2', + array( 'id' => 'mw-version-libraries' ), + $this->msg( 'version-libraries' )->text() + ); + $out .= Html::openElement( + 'table', + array( 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ) + ); + $out .= Html::openElement( 'tr' ) + . Html::element( 'th', array(), $this->msg( 'version-libraries-library' )->text() ) + . Html::element( 'th', array(), $this->msg( 'version-libraries-version' )->text() ) + . Html::closeElement( 'tr' ); + + foreach ( $lock->getInstalledDependencies() as $name => $info ) { + if ( strpos( $info['type'], 'mediawiki-' ) === 0 ) { + // Skip any extensions or skins since they'll be listed + // in their proper section + continue; + } + $out .= Html::openElement( 'tr' ) + . Html::rawElement( + 'td', + array(), + Linker::makeExternalLink( "https://packagist.org/packages/$name", $name ) + ) + . Html::element( 'td', array(), $info['version'] ) + . Html::closeElement( 'tr' ); + } + $out .= Html::closeElement( 'table' ); + + return $out; + } + + /** * Obtains a list of installed parser tags and the associated H2 header * * @return string HTML output @@ -516,7 +557,7 @@ class SpecialVersion extends SpecialPage { if ( count( $tags ) ) { $out = Html::rawElement( 'h2', - array( 'class' => 'mw-headline' ), + array( 'class' => 'mw-headline plainlinks' ), Linker::makeExternalLink( '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions', $this->msg( 'version-parser-extensiontags' )->parse(), @@ -525,8 +566,17 @@ class SpecialVersion extends SpecialPage { ); array_walk( $tags, function ( &$value ) { - $value = '<' . htmlspecialchars( $value ) . '>'; + // Bidirectional isolation improves readability in RTL wikis + $value = Html::element( + 'bdi', + // Prevent < and > from slipping to another line + array( + 'style' => 'white-space: nowrap;', + ), + "<$value>" + ); } ); + $out .= $this->listToText( $tags ); } else { $out = ''; @@ -545,11 +595,15 @@ class SpecialVersion extends SpecialPage { $fhooks = $wgParser->getFunctionHooks(); if ( count( $fhooks ) ) { - $out = Html::rawElement( 'h2', array( 'class' => 'mw-headline' ), Linker::makeExternalLink( - '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions', - $this->msg( 'version-parser-function-hooks' )->parse(), - false /* msg()->parse() already escapes */ - ) ); + $out = Html::rawElement( + 'h2', + array( 'class' => 'mw-headline plainlinks' ), + Linker::makeExternalLink( + '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions', + $this->msg( 'version-parser-function-hooks' )->parse(), + false /* msg()->parse() already escapes */ + ) + ); $out .= $this->listToText( $fhooks ); } else { @@ -680,7 +734,7 @@ class SpecialVersion extends SpecialPage { list( $vcsVersion, $vcsLink, $vcsDate ) = $cache->get( $memcKey ); if ( !$vcsVersion ) { - wfDebug( "Getting VCS info for extension $extensionName" ); + wfDebug( "Getting VCS info for extension {$extension['name']}" ); $gitInfo = new GitInfo( $extensionPath ); $vcsVersion = $gitInfo->getHeadSHA1(); if ( $vcsVersion !== false ) { @@ -696,7 +750,7 @@ class SpecialVersion extends SpecialPage { } $cache->set( $memcKey, array( $vcsVersion, $vcsLink, $vcsDate ), 60 * 60 * 24 ); } else { - wfDebug( "Pulled VCS info for extension $extensionName from cache" ); + wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" ); } } @@ -739,18 +793,23 @@ class SpecialVersion extends SpecialPage { // ... and license information; if a license file exists we // will link to it $licenseLink = ''; - if ( isset( $extension['license-name'] ) ) { - $licenseLink = Linker::link( - $this->getPageTitle( 'License/' . $extensionName ), - $out->parseInline( $extension['license-name'] ), - array( 'class' => 'mw-version-ext-license' ) - ); - } elseif ( $this->getExtLicenseFileName( $extensionPath ) ) { - $licenseLink = Linker::link( - $this->getPageTitle( 'License/' . $extensionName ), - $this->msg( 'version-ext-license' ), - array( 'class' => 'mw-version-ext-license' ) - ); + if ( isset( $extension['name'] ) ) { + $licenseName = null; + if ( isset( $extension['license-name'] ) ) { + $licenseName = $out->parseInline( $extension['license-name'] ); + } elseif ( $this->getExtLicenseFileName( $extensionPath ) ) { + $licenseName = $this->msg( 'version-ext-license' ); + } + if ( $licenseName !== null ) { + $licenseLink = Linker::link( + $this->getPageTitle( 'License/' . $extension['name'] ), + $licenseName, + array( + 'class' => 'mw-version-ext-license', + 'dir' => 'auto', + ) + ); + } } // ... and generate the description; which can be a parameterized l10n message @@ -778,12 +837,12 @@ class SpecialVersion extends SpecialPage { // ... now get the authors for this extension $authors = isset( $extension['author'] ) ? $extension['author'] : array(); - $authors = $this->listAuthors( $authors, $extensionName, $extensionPath ); + $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath ); // Finally! Create the table $html = Html::openElement( 'tr', array( 'class' => 'mw-version-ext', - 'id' => "mw-version-ext-{$extensionName}" + 'id' => "mw-version-ext-{$extension['name']}" ) ); @@ -793,7 +852,7 @@ class SpecialVersion extends SpecialPage { $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-description' ), $description ); $html .= Html::rawElement( 'td', array( 'class' => 'mw-version-ext-authors' ), $authors ); - $html .= Html::closeElement( 'td' ); + $html .= Html::closeElement( 'tr' ); return $html; } @@ -916,10 +975,10 @@ class SpecialVersion extends SpecialPage { if ( $this->getExtAuthorsFileName( $extDir ) ) { $text = Linker::link( $this->getPageTitle( "Credits/$extName" ), - $this->msg( 'version-poweredby-others' )->text() + $this->msg( 'version-poweredby-others' )->escaped() ); } else { - $text = $this->msg( 'version-poweredby-others' )->text(); + $text = $this->msg( 'version-poweredby-others' )->escaped(); } $list[] = $text; } elseif ( substr( $item, -5 ) == ' ...]' ) { @@ -935,7 +994,7 @@ class SpecialVersion extends SpecialPage { if ( !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) { $list[] = $text = Linker::link( $this->getPageTitle( "Credits/$extName" ), - $this->msg( 'version-poweredby-others' )->text() + $this->msg( 'version-poweredby-others' )->escaped() ); } @@ -1024,7 +1083,7 @@ class SpecialVersion extends SpecialPage { * Convert an array or object to a string for display. * * @param mixed $list Will convert an array to string if given and return - * the paramater unaltered otherwise + * the parameter unaltered otherwise * * @return mixed */ @@ -1188,7 +1247,7 @@ class SpecialVersion extends SpecialPage { $language = $this->getLanguage(); $thAttribures = array( 'dir' => $language->getDir(), - 'lang' => $language->getCode() + 'lang' => $language->getHtmlCode() ); $out = Html::element( 'h2', diff --git a/includes/specials/SpecialWantedcategories.php b/includes/specials/SpecialWantedcategories.php index b8c0bb28..7ddafae4 100644 --- a/includes/specials/SpecialWantedcategories.php +++ b/includes/specials/SpecialWantedcategories.php @@ -109,6 +109,7 @@ class WantedCategoriesPage extends WantedQueryPage { $currentValue = isset( $this->currentCategoryCounts[$result->title] ) ? $this->currentCategoryCounts[$result->title] : 0; + $cachedValue = intval( $result->value ); // T76910 // If the category has been created or emptied since the list was refreshed, strike it if ( $nt->isKnown() || $currentValue === 0 ) { @@ -116,11 +117,11 @@ class WantedCategoriesPage extends WantedQueryPage { } // Show the current number of category entries if it changed - if ( $currentValue !== $result->value ) { + if ( $currentValue !== $cachedValue ) { $nlinks = $this->msg( 'nmemberschanged' ) - ->numParams( $result->value, $currentValue )->escaped(); + ->numParams( $cachedValue, $currentValue )->escaped(); } else { - $nlinks = $this->msg( 'nmembers' )->numParams( $result->value )->escaped(); + $nlinks = $this->msg( 'nmembers' )->numParams( $cachedValue )->escaped(); } } diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php index 937a503c..8a1a6c6c 100644 --- a/includes/specials/SpecialWantedfiles.php +++ b/includes/specials/SpecialWantedfiles.php @@ -99,10 +99,10 @@ class WantedFilesPage extends WantedQueryPage { * Use wfFindFile so we still think file namespace pages without * files are missing, but valid file redirects and foreign files are ok. * - * @return boolean + * @return bool */ protected function existenceCheck( Title $title ) { - return (bool) wfFindFile( $title ); + return (bool)wfFindFile( $title ); } function getQueryInfo() { diff --git a/includes/specials/SpecialWantedpages.php b/includes/specials/SpecialWantedpages.php index 38f1808f..dd4eb0a8 100644 --- a/includes/specials/SpecialWantedpages.php +++ b/includes/specials/SpecialWantedpages.php @@ -72,7 +72,10 @@ class WantedPagesPage extends WantedQueryPage { "pg2.page_namespace != '" . NS_MEDIAWIKI . "'" ), 'options' => array( - 'HAVING' => "COUNT(*) > $count", + 'HAVING' => array( + "COUNT(*) > $count", + "COUNT(*) > SUM(pg2.page_is_redirect)" + ), 'GROUP BY' => array( 'pl_namespace', 'pl_title' ) ), 'join_conds' => array( @@ -86,7 +89,7 @@ class WantedPagesPage extends WantedQueryPage { ) ); // Replacement for the WantedPages::getSQL hook - wfRunHooks( 'WantedPages::getQueryInfo', array( &$this, &$query ) ); + Hooks::run( 'WantedPages::getQueryInfo', array( &$this, &$query ) ); return $query; } diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index 8f2f86b9..df9d3639 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -79,22 +79,16 @@ class SpecialWatchlist extends ChangesListSpecialPage { } /** - * Return an array of subpages beginning with $search that this special page will accept. + * Return an array of subpages that this special page will accept. * - * @param string $search Prefix to search for - * @param int $limit Maximum number of results to return - * @return string[] Matching subpages + * @see also SpecialEditWatchlist::getSubpagesForPrefixSearch + * @return string[] subpages */ - public function prefixSearchSubpages( $search, $limit = 10 ) { - // See also SpecialEditWatchlist::prefixSearchSubpages - return self::prefixSearchArray( - $search, - $limit, - array( - 'clear', - 'edit', - 'raw', - ) + public function getSubpagesForPrefixSearch() { + return array( + 'clear', + 'edit', + 'raw', ); } @@ -129,7 +123,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { protected function getCustomFilters() { if ( $this->customFilters === null ) { $this->customFilters = parent::getCustomFilters(); - wfRunHooks( 'SpecialWatchlistFilters', array( $this, &$this->customFilters ), '1.23' ); + Hooks::run( 'SpecialWatchlistFilters', array( $this, &$this->customFilters ), '1.23' ); } return $this->customFilters; @@ -207,7 +201,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { } else { # Top log Ids for a page are not stored $nonRevisionTypes = array( RC_LOG ); - wfRunHooks( 'SpecialWatchlistGetNonRevisionTypes', array( &$nonRevisionTypes ) ); + Hooks::run( 'SpecialWatchlistGetNonRevisionTypes', array( &$nonRevisionTypes ) ); if ( $nonRevisionTypes ) { $conds[] = $dbr->makeList( array( @@ -288,9 +282,11 @@ class SpecialWatchlist extends ChangesListSpecialPage { ); } - protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ) { + protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options, + &$join_conds, $opts + ) { return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ) - && wfRunHooks( + && Hooks::run( 'SpecialWatchlistQuery', array( &$conds, &$tables, &$join_conds, &$fields, $opts ), '1.23' @@ -298,9 +294,9 @@ class SpecialWatchlist extends ChangesListSpecialPage { } /** - * Return a DatabaseBase object for reading + * Return a IDatabase object for reading * - * @return DatabaseBase + * @return IDatabase */ protected function getDB() { return wfGetDB( DB_SLAVE, 'watchlist' ); @@ -367,7 +363,9 @@ class SpecialWatchlist extends ChangesListSpecialPage { $updated = false; } - if ( $this->getConfig()->get( 'RCShowWatchingUsers' ) && $user->getOption( 'shownumberswatching' ) ) { + if ( $this->getConfig()->get( 'RCShowWatchingUsers' ) + && $user->getOption( 'shownumberswatching' ) + ) { $rc->numberofWatchingusers = $dbr->selectField( 'watchlist', 'COUNT(*)', array( @@ -430,7 +428,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { $filters[$key] = $params['msg']; } // Disable some if needed - if ( !$user->useNPPatrol() ) { + if ( !$user->useRCPatrol() ) { unset( $filters['hidepatrolled'] ); } @@ -503,7 +501,9 @@ class SpecialWatchlist extends ChangesListSpecialPage { $form .= $this->msg( 'nowatchlist' )->parse() . "\n"; } else { $form .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n"; - if ( $this->getConfig()->get( 'EnotifWatchlist' ) && $user->getOption( 'enotifwatchlistpages' ) ) { + if ( $this->getConfig()->get( 'EnotifWatchlist' ) + && $user->getOption( 'enotifwatchlistpages' ) + ) { $form .= $this->msg( 'wlheader-enotif' )->parse() . "\n"; } if ( $showUpdatedMarker ) { @@ -562,12 +562,10 @@ class SpecialWatchlist extends ChangesListSpecialPage { protected function daysLink( $d, $options = array() ) { $options['days'] = $d; - $message = $d ? $this->getLanguage()->formatNum( $d ) - : $this->msg( 'watchlistall2' )->escaped(); return Linker::linkKnown( $this->getPageTitle(), - $message, + $this->getLanguage()->formatNum( $d ), array(), $options ); @@ -581,8 +579,11 @@ class SpecialWatchlist extends ChangesListSpecialPage { * @return string */ protected function cutoffLinks( $days, $options = array() ) { + global $wgRCMaxAge; + $watchlistMaxDays = ceil( $wgRCMaxAge / ( 3600 * 24 ) ); + $hours = array( 1, 2, 6, 12 ); - $days = array( 1, 3, 7 ); + $days = array( 1, 3, 7, $watchlistMaxDays ); $i = 0; foreach ( $hours as $h ) { $hours[$i++] = $this->hoursLink( $h, $options ); @@ -594,14 +595,13 @@ class SpecialWatchlist extends ChangesListSpecialPage { return $this->msg( 'wlshowlast' )->rawParams( $this->getLanguage()->pipeList( $hours ), - $this->getLanguage()->pipeList( $days ), - $this->daysLink( 0, $options ) )->parse(); + $this->getLanguage()->pipeList( $days ) )->parse(); } /** * Count the number of items on a user's watchlist * - * @param DatabaseBase $dbr A database connection + * @param IDatabase $dbr A database connection * @return int */ protected function countItems( $dbr ) { diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php index 7dc6da1f..0b3175a6 100644 --- a/includes/specials/SpecialWhatlinkshere.php +++ b/includes/specials/SpecialWhatlinkshere.php @@ -58,6 +58,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $opts->add( 'hidetrans', false ); $opts->add( 'hidelinks', false ); $opts->add( 'hideimages', false ); + $opts->add( 'invert', false ); $opts->fetchValuesFromRequest( $this->getRequest() ); $opts->validateIntBounds( 'limit', 0, 5000 ); @@ -72,7 +73,9 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $this->target = Title::newFromURL( $opts->getValue( 'target' ) ); if ( !$this->target ) { - $out->addHTML( $this->whatlinkshereForm() ); + if ( !$this->including() ) { + $out->addHTML( $this->whatlinkshereForm() ); + } return; } @@ -125,15 +128,17 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $useLinkNamespaceDBFields = $this->getConfig()->get( 'UseLinkNamespaceDBFields' ); $namespace = $this->opts->getValue( 'namespace' ); + $invert = $this->opts->getValue( 'invert' ); + $nsComparison = ( $invert ? '!= ' : '= ' ) . $dbr->addQuotes( $namespace ); if ( is_int( $namespace ) ) { if ( $useLinkNamespaceDBFields ) { - $conds['pagelinks']['pl_from_namespace'] = $namespace; - $conds['templatelinks']['tl_from_namespace'] = $namespace; - $conds['imagelinks']['il_from_namespace'] = $namespace; + $conds['pagelinks'][] = "pl_from_namespace $nsComparison"; + $conds['templatelinks'][] = "tl_from_namespace $nsComparison"; + $conds['imagelinks'][] = "il_from_namespace $nsComparison"; } else { - $conds['pagelinks']['page_namespace'] = $namespace; - $conds['templatelinks']['page_namespace'] = $namespace; - $conds['imagelinks']['page_namespace'] = $namespace; + $conds['pagelinks'][] = "page_namespace $nsComparison"; + $conds['templatelinks'][] = "page_namespace $nsComparison"; + $conds['imagelinks'][] = "page_namespace $nsComparison"; } } @@ -149,7 +154,9 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $conds['pagelinks'][] = 'rd_from is NOT NULL'; } - $queryFunc = function ( $dbr, $table, $fromCol ) use ( $conds, $target, $limit, $useLinkNamespaceDBFields ) { + $queryFunc = function ( $dbr, $table, $fromCol ) use ( + $conds, $target, $limit, $useLinkNamespaceDBFields + ) { // Read an extra row as an at-end check $queryLimit = $limit + 1; $on = array( @@ -174,7 +181,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { ); return $dbr->select( array( 'page', 'temp_backlink_range' => "($subQuery)" ), - array( 'page_id', 'page_namespace', 'page_title', 'rd_from' ), + array( 'page_id', 'page_namespace', 'page_title', 'rd_from', 'page_is_redirect' ), array(), __CLASS__ . '::showIndirectLinks', array( 'ORDER BY' => 'page_id', 'LIMIT' => $queryLimit ), @@ -275,7 +282,11 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { if ( $row->rd_from && $level < 2 ) { $out->addHTML( $this->listItem( $row, $nt, $target, true ) ); - $this->showIndirectLinks( $level + 1, $nt, $this->getConfig()->get( 'MaxRedirectLinksRetrieved' ) ); + $this->showIndirectLinks( + $level + 1, + $nt, + $this->getConfig()->get( 'MaxRedirectLinksRetrieved' ) + ); $out->addHTML( Xml::closeElement( 'li' ) ); } else { $out->addHTML( $this->listItem( $row, $nt, $target ) ); @@ -318,7 +329,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $link = Linker::linkKnown( $nt, null, - array(), + $row->page_is_redirect ? array( 'class' => 'mw-redirect' ) : array(), $query ); @@ -335,7 +346,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $props[] = $msgcache['isimage']; } - wfRunHooks( 'WhatLinksHereProps', array( $row, $nt, $target, &$props ) ); + Hooks::run( 'WhatLinksHereProps', array( $row, $nt, $target, &$props ) ); if ( count( $props ) ) { $propsText = $this->msg( 'parentheses' ) @@ -419,6 +430,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $target = $this->target ? $this->target->getPrefixedText() : ''; $namespace = $this->opts->consumeValue( 'namespace' ); + $nsinvert = $this->opts->consumeValue( 'invert' ); # Build up the form $f = Xml::openElement( 'form', array( 'action' => wfScript() ) ); @@ -431,9 +443,9 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $f .= Xml::fieldset( $this->msg( 'whatlinkshere' )->text() ); - # Target input + # Target input (.mw-searchInput enables suggestions) $f .= Xml::inputLabel( $this->msg( 'whatlinkshere-page' )->text(), 'target', - 'mw-whatlinkshere-target', 40, $target ); + 'mw-whatlinkshere-target', 40, $target, array( 'class' => 'mw-searchInput' ) ); $f .= ' '; @@ -450,6 +462,15 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { ) ); + $f .= ' ' . + Xml::checkLabel( + $this->msg( 'invert' )->text(), + 'invert', + 'nsinvert', + $nsinvert, + array( 'title' => $this->msg( 'tooltip-whatlinkshere-invert' )->text() ) + ); + $f .= ' '; # Submit @@ -496,6 +517,24 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { ); } + /** + * Return an array of subpages beginning with $search that this special page will accept. + * + * @param string $search Prefix to search for + * @param int $limit Maximum number of results to return (usually 10) + * @param int $offset Number of results to skip (usually 0) + * @return string[] Matching subpages + */ + public function prefixSearchSubpages( $search, $limit, $offset ) { + if ( $search === '' ) { + return array(); + } + // Autocomplete subpage the same as a normal search + $prefixSearcher = new StringPrefixSearch; + $result = $prefixSearcher->search( $search, $limit, array(), $offset ); + return $result; + } + protected function getGroupName() { return 'pagetools'; } |