diff options
Diffstat (limited to 'includes/specials')
75 files changed, 1588 insertions, 884 deletions
diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php index 5e2ee1c2..047e9413 100644 --- a/includes/specials/SpecialActiveusers.php +++ b/includes/specials/SpecialActiveusers.php @@ -267,21 +267,24 @@ class SpecialActiveUsers extends SpecialPage { $out->wrapWikiMsg( "<div class='mw-activeusers-intro'>\n$1\n</div>", array( 'activeusers-intro', $this->getLanguage()->formatNum( $days ) ) ); - // Get the timestamp of the last cache update + // Mention the level of cache staleness... $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', + $rcMax = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)' ); + if ( $rcMax ) { + $cTime = $dbr->selectField( 'querycache_info', + 'qci_timestamp', + array( 'qci_type' => 'activeusers' ) + ); + if ( $cTime ) { + $secondsOld = wfTimestamp( TS_UNIX, $rcMax ) - wfTimestamp( TS_UNIX, $cTime ); + } else { + $rcMin = $dbr->selectField( 'recentchanges', 'MIN(rc_timestamp)' ); + $secondsOld = time() - wfTimestamp( TS_UNIX, $rcMin ); + } + if ( $secondsOld > 0 ) { + $out->addWikiMsg( 'cachedspecial-viewing-cached-ttl', $this->getLanguage()->formatDuration( $secondsOld ) ); + } } $up = new ActiveUsersPager( $this->getContext(), null, $par ); diff --git a/includes/specials/SpecialAllMessages.php b/includes/specials/SpecialAllMessages.php index 7b596bb0..762658c5 100644 --- a/includes/specials/SpecialAllMessages.php +++ b/includes/specials/SpecialAllMessages.php @@ -29,7 +29,7 @@ */ class SpecialAllMessages extends SpecialPage { /** - * @var AllmessagesTablePager + * @var AllMessagesTablePager */ protected $table; @@ -61,7 +61,7 @@ class SpecialAllMessages extends SpecialPage { $out->addModuleStyles( 'mediawiki.special' ); $this->addHelpLink( 'Help:System message' ); - $this->table = new AllmessagesTablePager( + $this->table = new AllMessagesTablePager( $this, array(), wfGetLangObj( $request->getVal( 'lang', $par ) ) @@ -130,7 +130,7 @@ class AllMessagesTablePager extends TablePager { if ( $prefix !== null ) { $this->displayPrefix = $prefix->getDBkey(); - $this->prefix = '/^' . preg_quote( $this->displayPrefix ) . '/i'; + $this->prefix = '/^' . preg_quote( $this->displayPrefix, '/' ) . '/i'; } else { $this->displayPrefix = false; $this->prefix = false; @@ -206,7 +206,7 @@ class AllMessagesTablePager extends TablePager { Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) . '</td> <td class="mw-input">' . - $this->getLimitSelect() . + $this->getLimitSelect( array( 'id' => 'mw-table_pager_limit_label' ) ) . '</td> <tr> <td></td> @@ -392,10 +392,10 @@ class AllMessagesTablePager extends TablePager { ); } - return $title . ' ' - . $this->msg( 'parentheses' )->rawParams( $talk )->escaped() - . ' ' - . $this->msg( 'parentheses' )->rawParams( $translation )->escaped(); + return $title . ' ' . + $this->msg( 'parentheses' )->rawParams( $talk )->escaped() . + ' ' . + $this->msg( 'parentheses' )->rawParams( $translation )->escaped(); case 'am_default' : case 'am_actual' : @@ -445,7 +445,7 @@ class AllMessagesTablePager extends TablePager { } elseif ( $field === 'am_title' ) { return array( 'class' => $field ); } else { - return array( 'lang' => $this->langcode, 'dir' => $this->lang->getDir(), 'class' => $field ); + return array( 'lang' => wfBCP47( $this->langcode ), 'dir' => $this->lang->getDir(), 'class' => $field ); } } diff --git a/includes/specials/SpecialAllPages.php b/includes/specials/SpecialAllPages.php index 74b1f7bb..c4a67c0c 100644 --- a/includes/specials/SpecialAllPages.php +++ b/includes/specials/SpecialAllPages.php @@ -25,6 +25,7 @@ * Implements Special:Allpages * * @ingroup SpecialPage + * @todo Rewrite using IndexPager */ class SpecialAllPages extends IncludableSpecialPage { @@ -179,6 +180,7 @@ class SpecialAllPages extends IncludableSpecialPage { $toList = $this->getNamespaceKeyAndText( $namespace, $to ); $namespaces = $this->getContext()->getLanguage()->getNamespaces(); $n = 0; + $prevTitle = null; if ( !$fromList || !$toList ) { $out = $this->msg( 'allpagesbadtitle' )->parseAsBlock(); @@ -191,15 +193,13 @@ class SpecialAllPages extends IncludableSpecialPage { list( , $toKey, $to ) = $toList; $dbr = wfGetDB( DB_SLAVE ); - $conds = array( - 'page_namespace' => $namespace, - 'page_title >= ' . $dbr->addQuotes( $fromKey ) - ); - + $filterConds = array( 'page_namespace' => $namespace ); if ( $hideredirects ) { - $conds['page_is_redirect'] = 0; + $filterConds['page_is_redirect'] = 0; } + $conds = $filterConds; + $conds[] = 'page_title >= ' . $dbr->addQuotes( $fromKey ); if ( $toKey !== "" ) { $conds[] = 'page_title <= ' . $dbr->addQuotes( $toKey ); } @@ -234,6 +234,35 @@ class SpecialAllPages extends IncludableSpecialPage { } else { $out = ''; } + + if ( $fromKey !== '' && !$this->including() ) { + # Get the first title from previous chunk + $prevConds = $filterConds; + $prevConds[] = 'page_title < ' . $dbr->addQuotes( $fromKey ); + $prevKey = $dbr->selectField( + 'page', + 'page_title', + $prevConds, + __METHOD__, + array( 'ORDER BY' => 'page_title DESC', 'OFFSET' => $this->maxPerPage - 1 ) + ); + + if ( $prevKey === false ) { + # The previous chunk is not complete, need to link to the very first title + # available in the database + $prevKey = $dbr->selectField( + 'page', + 'page_title', + $prevConds, + __METHOD__, + array( 'ORDER BY' => 'page_title' ) + ); + } + + if ( $prevKey !== false ) { + $prevTitle = Title::makeTitle( $namespace, $prevKey ); + } + } } if ( $this->including() ) { @@ -241,44 +270,6 @@ class SpecialAllPages extends IncludableSpecialPage { return; } - if ( $from == '' ) { - // First chunk; no previous link. - $prevTitle = null; - } else { - # Get the last title from previous chunk - $dbr = wfGetDB( DB_SLAVE ); - $res_prev = $dbr->select( - 'page', - 'page_title', - array( 'page_namespace' => $namespace, 'page_title < ' . $dbr->addQuotes( $from ) ), - __METHOD__, - array( 'ORDER BY' => 'page_title DESC', - 'LIMIT' => $this->maxPerPage, 'OFFSET' => ( $this->maxPerPage - 1 ) - ) - ); - - # Get first title of previous complete chunk - if ( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) { - $pt = $dbr->fetchObject( $res_prev ); - $prevTitle = Title::makeTitle( $namespace, $pt->page_title ); - } else { - # The previous chunk is not complete, need to link to the very first title - # available in the database - $options = array( 'LIMIT' => 1 ); - if ( !$dbr->implicitOrderby() ) { - $options['ORDER BY'] = 'page_title'; - } - $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', - array( 'page_namespace' => $namespace ), __METHOD__, $options ); - # Show the previous link if it s not the current requested chunk - if ( $from != $reallyFirstPage_title ) { - $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title ); - } else { - $prevTitle = null; - } - } - } - $self = $this->getPageTitle(); $topLinks = array( @@ -287,7 +278,7 @@ class SpecialAllPages extends IncludableSpecialPage { $bottomLinks = array(); # Do we put a previous link ? - if ( $prevTitle && $pt = $prevTitle->getText() ) { + if ( $prevTitle ) { $query = array( 'from' => $prevTitle->getText() ); if ( $namespace ) { @@ -300,7 +291,7 @@ class SpecialAllPages extends IncludableSpecialPage { $prevLink = Linker::linkKnown( $self, - $this->msg( 'prevpage', $pt )->escaped(), + $this->msg( 'prevpage', $prevTitle->getText() )->escaped(), array(), $query ); diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php index b0830327..2da24a8e 100644 --- a/includes/specials/SpecialAncientpages.php +++ b/includes/specials/SpecialAncientpages.php @@ -32,7 +32,7 @@ class AncientPagesPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -40,7 +40,7 @@ class AncientPagesPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'page', 'revision' ), 'fields' => array( @@ -56,7 +56,7 @@ class AncientPagesPage extends QueryPage { ); } - function usesTimestamps() { + public function usesTimestamps() { return true; } diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index b80d921d..cd6cc76e 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -97,7 +97,6 @@ class SpecialBlock extends FormSpecialPage { protected function alterForm( HTMLForm $form ) { $form->setWrapperLegendMsg( 'blockip-legend' ); $form->setHeaderText( '' ); - $form->setSubmitCallback( array( __CLASS__, 'processUIForm' ) ); $form->setSubmitDestructive(); $msg = $this->alreadyBlocked ? 'ipb-change-block' : 'ipbsubmit'; @@ -394,8 +393,8 @@ class SpecialBlock extends FormSpecialPage { # Link to edit the block dropdown reasons, if applicable if ( $user->isAllowed( 'editinterface' ) ) { - $links[] = Linker::link( - Title::makeTitle( NS_MEDIAWIKI, 'Ipbreason-dropdown' ), + $links[] = Linker::linkKnown( + $this->msg( 'ipbreason-dropdown' )->inContentLanguage()->getTitle(), $this->msg( 'ipb-edit-dropdown' )->escaped(), array(), array( 'action' => 'edit' ) @@ -597,17 +596,8 @@ class SpecialBlock extends FormSpecialPage { } /** - * Submit callback for an HTMLForm object, will simply pass - * @param array $data - * @param HTMLForm $form - * @return bool|string - */ - public static function processUIForm( array $data, HTMLForm $form ) { - return self::processForm( $data, $form->getContext() ); - } - - /** - * Given the form data, actually implement a block + * Given the form data, actually implement a block. This is also called from ApiBlock. + * * @param array $data * @param IContextSource $context * @return bool|string @@ -672,8 +662,8 @@ class SpecialBlock extends FormSpecialPage { if ( $data['HideUser'] ) { if ( !$performer->isAllowed( 'hideuser' ) ) { # this codepath is unreachable except by a malicious user spoofing forms, - # or by race conditions (user has oversight and sysop, loads block form, - # and is de-oversighted before submission); so need to fail completely + # or by race conditions (user has hideuser and block rights, loads block form, + # and loses hideuser rights before submission); so need to fail completely # rather than just silently disable hiding return array( 'badaccess-group0' ); } @@ -797,7 +787,7 @@ class SpecialBlock extends FormSpecialPage { $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 + # Make log entry, if the name is hidden, put it in the suppression log $log_type = $data['HideUser'] ? 'suppress' : 'block'; $logEntry = new ManualLogEntry( $log_type, $logaction ); $logEntry->setTarget( Title::makeTitle( NS_USER, $target ) ); @@ -848,16 +838,11 @@ class SpecialBlock extends FormSpecialPage { * Convert a submitted expiry time, which may be relative ("2 weeks", etc) or absolute * ("24 May 2034", etc), into an absolute timestamp we can put into the database. * @param string $expiry Whatever was typed into the form - * @return string Timestamp or "infinity" string for the DB implementation + * @return string Timestamp or 'infinity' */ public static function parseExpiryInput( $expiry ) { - static $infinity; - if ( $infinity == null ) { - $infinity = wfGetDB( DB_SLAVE )->getInfinity(); - } - if ( wfIsInfinity( $expiry ) ) { - $expiry = $infinity; + $expiry = 'infinity'; } else { $expiry = strtotime( $expiry ); @@ -967,11 +952,11 @@ class SpecialBlock extends FormSpecialPage { /** * Process the form on POST submission. * @param array $data + * @param HTMLForm $form * @return bool|array True for success, false for didn't-try, array of errors on failure */ - public function onSubmit( array $data ) { - // This isn't used since we need that HTMLForm that's passed in the - // second parameter. See alterForm for the real function + public function onSubmit( array $data, HTMLForm $form = null ) { + return self::processForm( $data, $form->getContext() ); } /** diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php index 0ec144a2..9defaba7 100644 --- a/includes/specials/SpecialBlockList.php +++ b/includes/specials/SpecialBlockList.php @@ -65,6 +65,9 @@ class SpecialBlockList extends SpecialPage { return; } + # setup BlockListPager here to get the actual default Limit + $pager = $this->getBlockListPager(); + # Just show the block list $fields = array( 'Target' => array( @@ -77,11 +80,11 @@ class SpecialBlockList extends SpecialPage { ), 'Options' => array( 'type' => 'multiselect', - 'options' => array( - $this->msg( 'blocklist-userblocks' )->text() => 'userblocks', - $this->msg( 'blocklist-tempblocks' )->text() => 'tempblocks', - $this->msg( 'blocklist-addressblocks' )->text() => 'addressblocks', - $this->msg( 'blocklist-rangeblocks' )->text() => 'rangeblocks', + 'options-messages' => array( + 'blocklist-userblocks' => 'userblocks', + 'blocklist-tempblocks' => 'tempblocks', + 'blocklist-addressblocks' => 'addressblocks', + 'blocklist-rangeblocks' => 'rangeblocks', ), 'flatlist' => true, ), @@ -96,7 +99,7 @@ class SpecialBlockList extends SpecialPage { $lang->formatNum( 500 ) => 500, ), 'name' => 'limit', - 'default' => 50, + 'default' => $pager->getLimit(), ), ); $context = new DerivativeContext( $this->getContext() ); @@ -109,10 +112,14 @@ class SpecialBlockList extends SpecialPage { $form->prepareForm(); $form->displayForm( '' ); - $this->showList(); + $this->showList( $pager ); } - function showList() { + /** + * Setup a new BlockListPager instance. + * @return BlockListPager + */ + protected function getBlockListPager() { $conds = array(); # Is the user allowed to see hidden blocks? if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { @@ -163,12 +170,20 @@ class SpecialBlockList extends SpecialPage { $conds[] = "ipb_range_end = ipb_range_start"; } + return new BlockListPager( $this, $conds ); + } + + /** + * Show the list of blocked accounts matching the actual filter. + * @param BlockListPager $pager The BlockListPager instance for this page + */ + protected function showList( BlockListPager $pager ) { + $out = $this->getOutput(); + # Check for other blocks, i.e. global/tor blocks $otherBlockLink = array(); Hooks::run( 'OtherBlockLogLink', array( &$otherBlockLink, $this->target ) ); - $out = $this->getOutput(); - # Show additional header for the local block only when other blocks exists. # Not necessary in a standard installation without such extensions enabled if ( count( $otherBlockLink ) ) { @@ -177,7 +192,6 @@ class SpecialBlockList extends SpecialPage { ); } - $pager = new BlockListPager( $this, $conds ); if ( $pager->getNumRows() ) { $out->addParserOutputContent( $pager->getFullOutput() ); } elseif ( $this->target ) { @@ -249,7 +263,7 @@ class BlockListPager extends TablePager { function formatValue( $name, $value ) { static $msg = null; if ( $msg === null ) { - $msg = array( + $keys = array( 'anononlyblock', 'createaccountblock', 'noautoblockblock', @@ -258,17 +272,22 @@ class BlockListPager extends TablePager { 'unblocklink', 'change-blocklink', ); - $msg = array_combine( $msg, array_map( array( $this, 'msg' ), $msg ) ); + + foreach ( $keys as $key ) { + $msg[$key] = $this->msg( $key )->escaped(); + } } /** @var $row object */ $row = $this->mCurrentRow; + $language = $this->getLanguage(); + $formatted = ''; switch ( $name ) { case 'ipb_timestamp': - $formatted = $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ); + $formatted = htmlspecialchars( $language->userTimeAndDate( $value, $this->getUser() ) ); break; case 'ipb_target': @@ -294,7 +313,10 @@ class BlockListPager extends TablePager { break; case 'ipb_expiry': - $formatted = $this->getLanguage()->formatExpiry( $value, /* User preference timezone */true ); + $formatted = htmlspecialchars( $language->formatExpiry( + $value, + /* User preference timezone */true + ) ); if ( $this->getUser()->isAllowed( 'block' ) ) { if ( $row->ipb_auto ) { $links[] = Linker::linkKnown( @@ -317,7 +339,7 @@ class BlockListPager extends TablePager { 'span', array( 'class' => 'mw-blocklist-actions' ), $this->msg( 'parentheses' )->rawParams( - $this->getLanguage()->pipeList( $links ) )->escaped() + $language->pipeList( $links ) )->escaped() ); } break; @@ -355,7 +377,7 @@ class BlockListPager extends TablePager { $properties[] = $msg['blocklist-nousertalk']; } - $formatted = $this->getLanguage()->commaList( $properties ); + $formatted = $language->commaList( $properties ); break; default: @@ -430,23 +452,14 @@ class BlockListPager extends TablePager { $lb = new LinkBatch; $lb->setCaller( __METHOD__ ); - $userids = array(); - foreach ( $result as $row ) { - $userids[] = $row->ipb_by; - - # Usernames and titles are in fact related by a simple substitution of space -> underscore - # The last few lines of Title::secureAndSplit() tell the story. - $name = str_replace( ' ', '_', $row->ipb_address ); - $lb->add( NS_USER, $name ); - $lb->add( NS_USER_TALK, $name ); - } + $lb->add( NS_USER, $row->ipb_address ); + $lb->add( NS_USER_TALK, $row->ipb_address ); - $ua = UserArray::newFromIDs( $userids ); - foreach ( $ua as $user ) { - $name = str_replace( ' ', '_', $user->getName() ); - $lb->add( NS_USER, $name ); - $lb->add( NS_USER_TALK, $name ); + if ( isset( $row->by_user_name ) ) { + $lb->add( NS_USER, $row->by_user_name ); + $lb->add( NS_USER_TALK, $row->by_user_name ); + } } $lb->execute(); diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php index 1bbdbeab..701f75f0 100644 --- a/includes/specials/SpecialBrokenRedirects.php +++ b/includes/specials/SpecialBrokenRedirects.php @@ -32,7 +32,7 @@ class BrokenRedirectsPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -48,7 +48,7 @@ class BrokenRedirectsPage extends QueryPage { return $this->msg( 'brokenredirectstext' )->parseAsBlock(); } - function getQueryInfo() { + public function getQueryInfo() { $dbr = wfGetDB( DB_SLAVE ); return array( diff --git a/includes/specials/SpecialChangeContentModel.php b/includes/specials/SpecialChangeContentModel.php new file mode 100644 index 00000000..cce5da5c --- /dev/null +++ b/includes/specials/SpecialChangeContentModel.php @@ -0,0 +1,223 @@ +<?php + +class SpecialChangeContentModel extends FormSpecialPage { + + public function __construct() { + parent::__construct( 'ChangeContentModel', 'editcontentmodel' ); + } + + /** + * @var Title|null + */ + private $title; + + /** + * @var Revision|bool|null + * + * A Revision object, false if no revision exists, null if not loaded yet + */ + private $oldRevision; + + protected function setParameter( $par ) { + $par = $this->getRequest()->getVal( 'pagetitle', $par ); + $title = Title::newFromText( $par ); + if ( $title ) { + $this->title = $title; + $this->par = $title->getPrefixedText(); + } else { + $this->par = ''; + } + } + + protected function getDisplayFormat() { + return 'ooui'; + } + + protected function alterForm( HTMLForm $form ) { + if ( !$this->title ) { + $form->setMethod( 'GET' ); + } + } + + public function validateTitle( $title ) { + if ( !$title ) { + // No form input yet + return true; + } + + // Already validated by HTMLForm, but if not, throw + // and exception instead of a fatal + $titleObj = Title::newFromTextThrow( $title ); + + $this->oldRevision = Revision::newFromTitle( $titleObj ) ?: false; + + if ( $this->oldRevision ) { + $oldContent = $this->oldRevision->getContent(); + if ( !$oldContent->getContentHandler()->supportsDirectEditing() ) { + return $this->msg( 'changecontentmodel-nodirectediting' ) + ->params( ContentHandler::getLocalizedName( $oldContent->getModel() ) ) + ->escaped(); + } + } + + return true; + } + + protected function getFormFields() { + $that = $this; + $fields = array( + 'pagetitle' => array( + 'type' => 'title', + 'creatable' => true, + 'name' => 'pagetitle', + 'default' => $this->par, + 'label-message' => 'changecontentmodel-title-label', + 'validation-callback' => array( $this, 'validateTitle' ), + ), + ); + if ( $this->title ) { + $fields['pagetitle']['readonly'] = true; + $fields += array( + 'model' => array( + 'type' => 'select', + 'name' => 'model', + 'options' => $this->getOptionsForTitle( $this->title ), + 'label-message' => 'changecontentmodel-model-label' + ), + 'reason' => array( + 'type' => 'text', + 'name' => 'reason', + 'validation-callback' => function( $reason ) use ( $that ) { + $match = EditPage::matchSummarySpamRegex( $reason ); + if ( $match ) { + return $that->msg( 'spamprotectionmatch', $match )->parse(); + } + + return true; + }, + 'label-message' => 'changecontentmodel-reason-label', + ), + ); + } + + return $fields; + } + + private function getOptionsForTitle( Title $title = null ) { + $models = ContentHandler::getContentModels(); + $options = array(); + foreach ( $models as $model ) { + $handler = ContentHandler::getForModelID( $model ); + if ( !$handler->supportsDirectEditing() ) { + continue; + } + if ( $title ) { + if ( $title->getContentModel() === $model ) { + continue; + } + if ( !$handler->canBeUsedOn( $title ) ) { + continue; + } + } + $options[ContentHandler::getLocalizedName( $model )] = $model; + } + + return $options; + } + + public function onSubmit( array $data ) { + global $wgContLang; + + if ( $data['pagetitle'] === '' ) { + // Initial form view of special page, pass + return false; + } + + // At this point, it has to be a POST request. This is enforced by HTMLForm, + // but lets be safe verify that. + if ( !$this->getRequest()->wasPosted() ) { + throw new RuntimeException( "Form submission was not POSTed" ); + } + + $this->title = Title::newFromText( $data['pagetitle' ] ); + $user = $this->getUser(); + // Check permissions and make sure the user has permission to edit the specific page + $errors = $this->title->getUserPermissionsErrors( 'editcontentmodel', $user ); + $errors = wfMergeErrorArrays( $errors, $this->title->getUserPermissionsErrors( 'edit', $user ) ); + if ( $errors ) { + $out = $this->getOutput(); + $wikitext = $out->formatPermissionsErrorMessage( $errors ); + // Hack to get our wikitext parsed + return Status::newFatal( new RawMessage( '$1', array( $wikitext ) ) ); + } + + $page = WikiPage::factory( $this->title ); + if ( $this->oldRevision === null ) { + $this->oldRevision = $page->getRevision() ?: false; + } + $oldModel = $this->title->getContentModel(); + if ( $this->oldRevision ) { + $oldContent = $this->oldRevision->getContent(); + try { + $newContent = ContentHandler::makeContent( + $oldContent->getNativeData(), $this->title, $data['model'] + ); + } catch ( MWException $e ) { + return Status::newFatal( + $this->msg( 'changecontentmodel-cannot-convert' ) + ->params( + $this->title->getPrefixedText(), + ContentHandler::getLocalizedName( $data['model'] ) + ) + ); + } + } else { + // Page doesn't exist, create an empty content object + $newContent = ContentHandler::getForModelID( $data['model'] )->makeEmptyContent(); + } + $flags = $this->oldRevision ? EDIT_UPDATE : EDIT_NEW; + if ( $user->isAllowed( 'bot' ) ) { + $flags |= EDIT_FORCE_BOT; + } + + $log = new ManualLogEntry( 'contentmodel', 'change' ); + $log->setPerformer( $user ); + $log->setTarget( $this->title ); + $log->setComment( $data['reason'] ); + $log->setParameters( array( + '4::oldmodel' => $oldModel, + '5::newmodel' => $data['model'] + ) ); + + $formatter = LogFormatter::newFromEntry( $log ); + $formatter->setContext( RequestContext::newExtraneousContext( $this->title ) ); + $reason = $formatter->getPlainActionText(); + if ( $data['reason'] !== '' ) { + $reason .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $data['reason']; + } + # Truncate for whole multibyte characters. + $reason = $wgContLang->truncate( $reason, 255 ); + + $status = $page->doEditContent( + $newContent, + $reason, + $flags, + $this->oldRevision ? $this->oldRevision->getId() : false, + $user + ); + if ( !$status->isOK() ) { + return $status; + } + + $logid = $log->insert(); + $log->publish( $logid ); + + return $status; + } + + public function onSuccess() { + $out = $this->getOutput(); + $out->setPageTitle( $this->msg( 'changecontentmodel-success-title' ) ); + $out->addWikiMsg( 'changecontentmodel-success-text', $this->title ); + } +} diff --git a/includes/specials/SpecialChangeEmail.php b/includes/specials/SpecialChangeEmail.php index eca307d9..22df04e8 100644 --- a/includes/specials/SpecialChangeEmail.php +++ b/includes/specials/SpecialChangeEmail.php @@ -92,14 +92,14 @@ class SpecialChangeEmail extends FormSpecialPage { 'NewEmail' => array( 'type' => 'email', 'label-message' => 'changeemail-newemail', + 'autofocus' => true ), ); if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) { $fields['Password'] = array( 'type' => 'password', - 'label-message' => 'changeemail-password', - 'autofocus' => true, + 'label-message' => 'changeemail-password' ); } @@ -107,7 +107,7 @@ class SpecialChangeEmail extends FormSpecialPage { } protected function getDisplayFormat() { - return 'vform'; + return 'ooui'; } protected function alterForm( HTMLForm $form ) { @@ -129,7 +129,8 @@ class SpecialChangeEmail extends FormSpecialPage { public function onSuccess() { $request = $this->getRequest(); - $titleObj = Title::newFromText( $request->getVal( 'returnto' ) ); + $returnto = $request->getVal( 'returnto' ); + $titleObj = $returnto !== null ? Title::newFromText( $returnto ) : null; if ( !$titleObj instanceof Title ) { $titleObj = Title::newMainPage(); } @@ -159,6 +160,10 @@ class SpecialChangeEmail extends FormSpecialPage { return Status::newFatal( 'invalidemailaddress' ); } + if ( $newaddr === $user->getEmail() ) { + return Status::newFatal( 'changeemail-nochange' ); + } + $throttleCount = LoginForm::incLoginThrottle( $user->getName() ); if ( $throttleCount === true ) { $lang = $this->getLanguage(); diff --git a/includes/specials/SpecialChangePassword.php b/includes/specials/SpecialChangePassword.php index 168095f8..6a4347df 100644 --- a/includes/specials/SpecialChangePassword.php +++ b/includes/specials/SpecialChangePassword.php @@ -179,7 +179,8 @@ class SpecialChangePassword extends FormSpecialPage { } if ( $request->getCheck( 'wpCancel' ) ) { - $titleObj = Title::newFromText( $request->getVal( 'returnto' ) ); + $returnto = $request->getVal( 'returnto' ); + $titleObj = $returnto !== null ? Title::newFromText( $returnto ) : null; if ( !$titleObj instanceof Title ) { $titleObj = Title::newMainPage(); } diff --git a/includes/specials/SpecialComparePages.php b/includes/specials/SpecialComparePages.php index da1a54cd..0f8b7291 100644 --- a/includes/specials/SpecialComparePages.php +++ b/includes/specials/SpecialComparePages.php @@ -50,10 +50,12 @@ class SpecialComparePages extends SpecialPage { $this->setHeaders(); $this->outputHeader(); + # Form (.mw-searchInput enables suggestions) $form = new HTMLForm( array( 'Page1' => array( 'type' => 'text', 'name' => 'page1', + 'cssclass' => 'mw-searchInput', 'label-message' => 'compare-page1', 'size' => '40', 'section' => 'page1', @@ -70,6 +72,7 @@ class SpecialComparePages extends SpecialPage { 'Page2' => array( 'type' => 'text', 'name' => 'page2', + 'cssclass' => 'mw-searchInput', 'label-message' => 'compare-page2', 'size' => '40', 'section' => 'page2', diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php index 63561552..147f67e8 100644 --- a/includes/specials/SpecialConfirmemail.php +++ b/includes/specials/SpecialConfirmemail.php @@ -43,6 +43,10 @@ class EmailConfirmation extends UnlistedSpecialPage { * @throws UserNotLoggedIn */ function execute( $code ) { + // Ignore things like master queries/connections on GET requests. + // It's very convenient to just allow formless link usage. + Profiler::instance()->getTransactionProfiler()->resetExpectations(); + $this->setHeaders(); $this->checkReadOnly(); @@ -151,6 +155,10 @@ class EmailInvalidation extends UnlistedSpecialPage { } function execute( $code ) { + // Ignore things like master queries/connections on GET requests. + // It's very convenient to just allow formless link usage. + Profiler::instance()->getTransactionProfiler()->resetExpectations(); + $this->setHeaders(); $this->checkReadOnly(); $this->checkPermissions(); diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index c2cd8122..9672580d 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -38,6 +38,7 @@ class SpecialContributions extends IncludableSpecialPage { $this->outputHeader(); $out = $this->getOutput(); $out->addModuleStyles( 'mediawiki.special' ); + $this->addHelpLink( 'Help:User contributions' ); $this->opts = array(); $request = $this->getRequest(); @@ -724,7 +725,6 @@ class ContribsPager extends ReverseChronologicalPager { $limit, $descending ); - $pager = $this; /* * This hook will allow extensions to add in additional queries, so they can get their data @@ -749,7 +749,7 @@ class ContribsPager extends ReverseChronologicalPager { ) ); Hooks::run( 'ContribsPager::reallyDoQuery', - array( &$data, $pager, $offset, $limit, $descending ) + array( &$data, $this, $offset, $limit, $descending ) ); $result = array(); @@ -965,14 +965,14 @@ class ContribsPager extends ReverseChronologicalPager { * 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(); + MediaWiki\suppressWarnings(); try { $rev = new Revision( $row ); $validRevision = (bool)$rev->getId(); } catch ( Exception $e ) { $validRevision = false; } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $validRevision ) { $classes = array(); diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php index fb85942d..44352a78 100644 --- a/includes/specials/SpecialDeletedContributions.php +++ b/includes/specials/SpecialDeletedContributions.php @@ -88,15 +88,13 @@ class DeletedContribsPager extends IndexPager { * @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 ) + array( &$data, $this, $offset, $limit, $descending ) ); $result = array(); @@ -203,14 +201,14 @@ class DeletedContribsPager extends IndexPager { * 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(); + MediaWiki\suppressWarnings(); try { $rev = Revision::newFromArchiveRow( $row ); $validRevision = (bool)$rev->getId(); } catch ( Exception $e ) { $validRevision = false; } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $validRevision ) { $ret = $this->formatRevisionRow( $row ); diff --git a/includes/specials/SpecialDiff.php b/includes/specials/SpecialDiff.php index 89c1c021..8b5d31a8 100644 --- a/includes/specials/SpecialDiff.php +++ b/includes/specials/SpecialDiff.php @@ -37,12 +37,16 @@ * @since 1.23 */ class SpecialDiff extends RedirectSpecialPage { - function __construct() { + public function __construct() { parent::__construct( 'Diff' ); $this->mAllowedRedirectParams = array(); } - function getRedirect( $subpage ) { + /** + * @param string|null $subpage + * @return Title|bool + */ + public function getRedirect( $subpage ) { $parts = explode( '/', $subpage ); // Try to parse the values given, generating somewhat pretty URLs if possible diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php index c364f70f..6d40985b 100644 --- a/includes/specials/SpecialDoubleRedirects.php +++ b/includes/specials/SpecialDoubleRedirects.php @@ -32,7 +32,7 @@ class DoubleRedirectsPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -99,7 +99,7 @@ class DoubleRedirectsPage extends QueryPage { return $retval; } - function getQueryInfo() { + public function getQueryInfo() { return $this->reallyGetQueryInfo(); } @@ -156,7 +156,6 @@ class DoubleRedirectsPage extends QueryPage { $this->msg( 'parentheses', $this->msg( 'editlink' )->text() )->escaped(), array(), array( - 'redirect' => 'no', 'action' => 'edit' ) ); diff --git a/includes/specials/SpecialEditTags.php b/includes/specials/SpecialEditTags.php index f41a1f1d..d2b2e708 100644 --- a/includes/specials/SpecialEditTags.php +++ b/includes/specials/SpecialEditTags.php @@ -123,7 +123,7 @@ class SpecialEditTags extends UnlistedSpecialPage { // Either submit or create our form if ( $this->isAllowed && $this->submitClicked ) { - $this->submit( $request ); + $this->submit(); } else { $this->showForm(); } @@ -349,20 +349,18 @@ class SpecialEditTags extends UnlistedSpecialPage { 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', - ) ); + + $select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags ); + $select->setAttribute( 'multiple', 'multiple' ); + $select->setAttribute( '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' ); + // Values of $tags are also used as <option> labels + $select->addOptions( array_combine( $tags, $tags ) ); + + $result[1] = $select->getHTML(); return $result; } diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php index 910fe259..74662aec 100644 --- a/includes/specials/SpecialEditWatchlist.php +++ b/includes/specials/SpecialEditWatchlist.php @@ -102,7 +102,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { case self::EDIT_NORMAL: default: - $this->executeViewEditWatchlist(); + $this->executeViewEditWatchlist(); break; } } @@ -299,7 +299,9 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { */ private function getWatchlist() { $list = array(); - $dbr = wfGetDB( DB_MASTER ); + + $index = $this->getRequest()->wasPosted() ? DB_MASTER : DB_SLAVE; + $dbr = wfGetDB( $index ); $res = $dbr->select( 'watchlist', @@ -312,6 +314,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { ); if ( $res->numRows() > 0 ) { + /** @var Title[] $titles */ $titles = array(); foreach ( $res as $row ) { $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title ); diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index c55fa94c..92cb8bf6 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -356,7 +356,9 @@ class SpecialEmailUser extends UnlistedSpecialPage { $replyTo = null; } - $status = UserMailer::send( $to, $mailFrom, $subject, $text, $replyTo ); + $status = UserMailer::send( $to, $mailFrom, $subject, $text, array( + 'replyTo' => $replyTo, + ) ); if ( !$status->isGood() ) { return $status; @@ -367,7 +369,10 @@ class SpecialEmailUser extends UnlistedSpecialPage { if ( $data['CCMe'] && $to != $from ) { $cc_subject = $context->msg( 'emailccsubject' )->rawParams( $target->getName(), $subject )->text(); + + // target and sender are equal, because this is the CC for the sender Hooks::run( 'EmailUserCC', array( &$from, &$from, &$cc_subject, &$text ) ); + $ccStatus = UserMailer::send( $from, $from, $cc_subject, $text ); $status->merge( $ccStatus ); } diff --git a/includes/specials/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php index b7582e6c..06eb2769 100644 --- a/includes/specials/SpecialExpandTemplates.php +++ b/includes/specials/SpecialExpandTemplates.php @@ -114,7 +114,7 @@ class SpecialExpandTemplates extends SpecialPage { } $config = $this->getConfig(); - if ( ( $config->get( 'UseTidy' ) && $options->getTidy() ) || $config->get( 'AlwaysUseTidy' ) ) { + if ( $config->get( 'UseTidy' ) && $options->getTidy() ) { $tmp = MWTidy::tidy( $tmp ); } diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php index c30d962a..39c4d771 100644 --- a/includes/specials/SpecialExport.php +++ b/includes/specials/SpecialExport.php @@ -30,7 +30,6 @@ */ class SpecialExport extends SpecialPage { private $curonly, $doExport, $pageLinkDepth, $templates; - private $images; public function __construct() { parent::__construct( 'Export' ); @@ -46,7 +45,6 @@ class SpecialExport extends SpecialPage { $this->doExport = false; $request = $this->getRequest(); $this->templates = $request->getCheck( 'templates' ); - $this->images = $request->getCheck( 'images' ); // Doesn't do anything yet $this->pageLinkDepth = $this->validateLinkDepth( $request->getIntOrNull( 'pagelink-depth' ) ); @@ -188,113 +186,122 @@ class SpecialExport extends SpecialPage { $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, - $categoryName - ) . ' '; - $form .= Xml::submitButton( - $this->msg( 'export-addcat' )->text(), - array( 'name' => 'addcat' ) - ) . '<br />'; - + $formDescriptor = array( + 'catname' => array( + 'type' => 'textwithbutton', + 'name' => 'catname', + 'horizontal-label' => true, + 'label-message' => 'export-addcattext', + 'default' => $categoryName, + 'size' => 40, + 'buttontype' => 'submit', + 'buttonname' => 'addcat', + 'buttondefault' => $this->msg( 'export-addcat' )->text(), + ), + ); if ( $config->get( 'ExportFromNamespaces' ) ) { - $form .= Html::namespaceSelector( - array( - 'selected' => $nsindex, - 'label' => $this->msg( 'export-addnstext' )->text() - ), array( + $formDescriptor += array( + 'nsindex' => array( + 'type' => 'namespaceselectwithbutton', + 'default' => $nsindex, + 'label-message' => 'export-addnstext', + 'horizontal-label' => true, 'name' => 'nsindex', 'id' => 'namespace', - 'class' => 'namespaceselector', - ) - ) . ' '; - $form .= Xml::submitButton( - $this->msg( 'export-addns' )->text(), - array( 'name' => 'addns' ) - ) . '<br />'; + 'cssclass' => 'namespaceselector', + 'buttontype' => 'submit', + 'buttonname' => 'addns', + 'buttondefault' => $this->msg( 'export-addns' )->text(), + ), + ); } if ( $config->get( 'ExportAllowAll' ) ) { - $form .= Xml::checkLabel( - $this->msg( 'exportall' )->text(), - 'exportall', - 'exportall', - $request->wasPosted() ? $request->getCheck( 'exportall' ) : false - ) . '<br />'; + $formDescriptor += array( + 'exportall' => array( + 'type' => 'check', + 'label-message' => 'exportall', + 'name' => 'exportall', + 'id' => 'exportall', + 'default' => $request->wasPosted() ? $request->getCheck( 'exportall' ) : false, + ), + ); } - $form .= Xml::element( - 'textarea', - array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ), - $page, - false + $formDescriptor += array( + 'textarea' => array( + 'class' => 'HTMLTextAreaField', + 'name' => 'pages', + 'nodata' => true, + 'cols' => 40, + 'rows' => 10, + 'default' => $page, + ), ); - $form .= '<br />'; if ( $config->get( 'ExportAllowHistory' ) ) { - $form .= Xml::checkLabel( - $this->msg( 'exportcuronly' )->text(), - 'curonly', - 'curonly', - $request->wasPosted() ? $request->getCheck( 'curonly' ) : true - ) . '<br />'; + $formDescriptor += array( + 'curonly' => array( + 'type' => 'check', + 'label-message' => 'exportcuronly', + 'name' => 'curonly', + 'id' => 'curonly', + 'default' => $request->wasPosted() ? $request->getCheck( 'curonly' ) : true, + ), + ); } else { $out->addWikiMsg( 'exportnohistory' ); } - $form .= Xml::checkLabel( - $this->msg( 'export-templates' )->text(), - 'templates', - 'wpExportTemplates', - $request->wasPosted() ? $request->getCheck( 'templates' ) : false - ) . '<br />'; + $formDescriptor += array( + 'templates' => array( + 'type' => 'check', + 'label-message' => 'export-templates', + 'name' => 'templates', + 'id' => 'wpExportTemplates', + 'default' => $request->wasPosted() ? $request->getCheck( 'templates' ) : false, + ), + ); if ( $config->get( 'ExportMaxLinkDepth' ) || $this->userCanOverrideExportDepth() ) { - $form .= Xml::inputLabel( - $this->msg( 'export-pagelinks' )->text(), - 'pagelink-depth', - 'pagelink-depth', - 20, - 0 - ) . '<br />'; + $formDescriptor += array( + 'pagelink-depth' => array( + 'type' => 'text', + 'name' => 'pagelink-depth', + 'id' => 'pagelink-depth', + 'label-message' => 'export-pagelinks', + 'default' => '0', + 'size' => 20, + ), + ); } - /* Enable this when we can do something useful exporting/importing image information. - $form .= Xml::checkLabel( - $this->msg( 'export-images' )->text(), - 'images', - 'wpExportImages', - false - ) . '<br />'; - */ - $form .= Xml::checkLabel( - $this->msg( 'export-download' )->text(), - 'wpDownload', - 'wpDownload', - $request->wasPosted() ? $request->getCheck( 'wpDownload' ) : true - ) . '<br />'; + $formDescriptor += array( + 'wpDownload' => array( + 'type' => 'check', + 'name' =>'wpDownload', + 'id' => 'wpDownload', + 'default' => $request->wasPosted() ? $request->getCheck( 'wpDownload' ) : true, + 'label-message' => 'export-download', + ), + ); if ( $config->get( 'ExportAllowListContributors' ) ) { - $form .= Xml::checkLabel( - $this->msg( 'exportlistauthors' )->text(), - 'listauthors', - 'listauthors', - $request->wasPosted() ? $request->getCheck( 'listauthors' ) : false - ) . '<br />'; + $formDescriptor += array( + 'listauthors' => array( + 'type' => 'check', + 'label-message' => 'exportlistauthors', + 'default' => $request->wasPosted() ? $request->getCheck( 'listauthors' ) : false, + 'name' => 'listauthors', + 'id' => 'listauthors', + ), + ); } - $form .= Xml::submitButton( - $this->msg( 'export-submit' )->text(), - Linker::tooltipAndAccesskeyAttribs( 'export' ) - ); - $form .= Xml::closeElement( 'form' ); - - $out->addHTML( $form ); + $htmlForm = HTMLForm::factory( 'div', $formDescriptor, $this->getContext() ); + $htmlForm->setSubmitTextMsg( 'export-submit' ); + $htmlForm->prepareForm()->displayForm( false ); + $this->addHelpLink( 'Help:Export' ); } /** @@ -319,7 +326,6 @@ class SpecialExport extends SpecialPage { if ( $exportall ) { $history = WikiExporter::FULL; } else { - $pageSet = array(); // Inverted index of all pages to look up // Split up and normalize input @@ -344,11 +350,6 @@ class SpecialExport extends SpecialPage { $pageSet = $this->getPageLinks( $inputPages, $pageSet, $linkDepth ); } - // Enable this when we can do something useful exporting/importing image information. - // if( $this->images ) ) { - // $pageSet = $this->getImages( $inputPages, $pageSet ); - // } - $pages = array_keys( $pageSet ); // Normalize titles to the same format and remove dupes, see bug 17374 @@ -371,9 +372,9 @@ class SpecialExport extends SpecialPage { $buffer = WikiExporter::STREAM; // This might take a while... :D - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); set_time_limit( 0 ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } $exporter = new WikiExporter( $db, $history, $buffer ); @@ -535,24 +536,6 @@ class SpecialExport extends SpecialPage { } /** - * Expand a list of pages to include images used in those pages. - * - * @param array $inputPages List of titles to look up - * @param array $pageSet Associative array indexed by titles for output - * - * @return array Associative array index by titles - */ - private function getImages( $inputPages, $pageSet ) { - return $this->getLinks( - $inputPages, - $pageSet, - 'imagelinks', - array( 'namespace' => NS_FILE, 'title' => 'il_to' ), - array( 'page_id=il_from' ) - ); - } - - /** * Expand a list of pages to include items used in those pages. * @param array $inputPages Array of page titles * @param array $pageSet diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php index dc9d57c2..406233b4 100644 --- a/includes/specials/SpecialFewestrevisions.php +++ b/includes/specials/SpecialFewestrevisions.php @@ -32,7 +32,7 @@ class FewestrevisionsPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -40,7 +40,7 @@ class FewestrevisionsPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'revision', 'page' ), 'fields' => array( diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php index da79bb81..4c0c75fb 100644 --- a/includes/specials/SpecialFileDuplicateSearch.php +++ b/includes/specials/SpecialFileDuplicateSearch.php @@ -48,7 +48,7 @@ class FileDuplicateSearchPage extends QueryPage { return false; } - function isCached() { + public function isCached() { return false; } @@ -82,7 +82,7 @@ class FileDuplicateSearchPage extends QueryPage { $this->getOutput()->addHtml( implode( "\n", $html ) ); } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'image' ), 'fields' => array( @@ -95,7 +95,7 @@ class FileDuplicateSearchPage extends QueryPage { ); } - function execute( $par ) { + public function execute( $par ) { $this->setHeaders(); $this->outputHeader(); diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php index 93232117..542589f3 100644 --- a/includes/specials/SpecialFilepath.php +++ b/includes/specials/SpecialFilepath.php @@ -27,13 +27,18 @@ * @ingroup SpecialPage */ class SpecialFilepath extends RedirectSpecialPage { - function __construct() { + public function __construct() { parent::__construct( 'Filepath' ); $this->mAllowedRedirectParams = array( 'width', 'height' ); } - // implement by redirecting through Special:Redirect/file - function getRedirect( $par ) { + /** + * Implement by redirecting through Special:Redirect/file. + * + * @param string|null $subpage + * @return Title + */ + public function getRedirect( $par ) { $file = $par ?: $this->getRequest()->getText( 'file' ); if ( $file ) { diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php index af869647..4cdf6ddf 100644 --- a/includes/specials/SpecialImport.php +++ b/includes/specials/SpecialImport.php @@ -34,6 +34,7 @@ class SpecialImport extends SpecialPage { private $interwiki = false; private $subproject; private $fullInterwikiPrefix; + private $mapping = 'default'; private $namespace; private $rootpage = ''; private $frompage = ''; @@ -56,6 +57,8 @@ class SpecialImport extends SpecialPage { * @throws ReadOnlyError */ function execute( $par ) { + $this->useTransactionalTimeLimit(); + $this->setHeaders(); $this->outputHeader(); @@ -101,26 +104,33 @@ class SpecialImport extends SpecialPage { private function doImport() { $isUpload = false; $request = $this->getRequest(); - $this->namespace = $request->getIntOrNull( 'namespace' ); $this->sourceName = $request->getVal( "source" ); $this->logcomment = $request->getText( 'log-comment' ); $this->pageLinkDepth = $this->getConfig()->get( 'ExportMaxLinkDepth' ) == 0 ? 0 : $request->getIntOrNull( 'pagelink-depth' ); - $this->rootpage = $request->getText( 'rootpage' ); + + $this->mapping = $request->getVal( 'mapping' ); + if ( $this->mapping === 'namespace' ) { + $this->namespace = $request->getIntOrNull( 'namespace' ); + } elseif ( $this->mapping === 'subpage' ) { + $this->rootpage = $request->getText( 'rootpage' ); + } else { + $this->mapping = 'default'; + } $user = $this->getUser(); if ( !$user->matchEditToken( $request->getVal( 'editToken' ) ) ) { $source = Status::newFatal( 'import-token-mismatch' ); - } elseif ( $this->sourceName == 'upload' ) { + } elseif ( $this->sourceName === 'upload' ) { $isUpload = true; if ( $user->isAllowed( 'importupload' ) ) { $source = ImportStreamSource::newFromUpload( "xmlimport" ); } else { throw new PermissionsError( 'importupload' ); } - } elseif ( $this->sourceName == "interwiki" ) { + } elseif ( $this->sourceName === 'interwiki' ) { if ( !$user->isAllowed( 'import' ) ) { throw new PermissionsError( 'import' ); } @@ -163,8 +173,7 @@ class SpecialImport extends SpecialPage { $importer = new WikiImporter( $source->value, $this->getConfig() ); if ( !is_null( $this->namespace ) ) { $importer->setTargetNamespace( $this->namespace ); - } - if ( !is_null( $this->rootpage ) ) { + } elseif ( !is_null( $this->rootpage ) ) { $statusRootPage = $importer->setTargetRootPage( $this->rootpage ); if ( !$statusRootPage->isGood() ) { $out->wrapWikiMsg( @@ -219,13 +228,88 @@ class SpecialImport extends SpecialPage { } } + private function getMappingFormPart( $sourceName ) { + $isSameSourceAsBefore = ( $this->sourceName === $sourceName ); + $defaultNamespace = $this->getConfig()->get( 'ImportTargetNamespace' ); + return "<tr> + <td> + </td> + <td class='mw-input'>" . + Xml::radioLabel( + $this->msg( 'import-mapping-default' )->text(), + 'mapping', + 'default', + // mw-import-mapping-interwiki-default, mw-import-mapping-upload-default + "mw-import-mapping-$sourceName-default", + ( $isSameSourceAsBefore ? + ( $this->mapping === 'default' ) : + is_null( $defaultNamespace ) ) + ) . + "</td> + </tr> + <tr> + <td> + </td> + <td class='mw-input'>" . + Xml::radioLabel( + $this->msg( 'import-mapping-namespace' )->text(), + 'mapping', + 'namespace', + // mw-import-mapping-interwiki-namespace, mw-import-mapping-upload-namespace + "mw-import-mapping-$sourceName-namespace", + ( $isSameSourceAsBefore ? + ( $this->mapping === 'namespace' ) : + !is_null( $defaultNamespace ) ) + ) . ' ' . + Html::namespaceSelector( + array( + 'selected' => ( $isSameSourceAsBefore ? + $this->namespace : + ( $defaultNamespace || '' ) ), + ), array( + 'name' => "namespace", + // mw-import-namespace-interwiki, mw-import-namespace-upload + 'id' => "mw-import-namespace-$sourceName", + 'class' => 'namespaceselector', + ) + ) . + "</td> + </tr> + <tr> + <td> + </td> + <td class='mw-input'>" . + Xml::radioLabel( + $this->msg( 'import-mapping-subpage' )->text(), + 'mapping', + 'subpage', + // mw-import-mapping-interwiki-subpage, mw-import-mapping-upload-subpage + "mw-import-mapping-$sourceName-subpage", + ( $isSameSourceAsBefore ? ( $this->mapping === 'subpage' ) : '' ) + ) . ' ' . + Xml::input( 'rootpage', 50, + ( $isSameSourceAsBefore ? $this->rootpage : '' ), + array( + // Should be "mw-import-rootpage-...", but we keep this inaccurate + // ID for legacy reasons + // mw-interwiki-rootpage-interwiki, mw-interwiki-rootpage-upload + 'id' => "mw-interwiki-rootpage-$sourceName", + 'type' => 'text' + ) + ) . ' ' . + "</td> + </tr>"; + } + private function showForm() { $action = $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ); $user = $this->getUser(); $out = $this->getOutput(); + $this->addHelpLink( '//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Import', true ); $importSources = $this->getConfig()->get( 'ImportSources' ); if ( $user->isAllowed( 'importupload' ) ) { + $mappingSelection = $this->getMappingFormPart( 'upload' ); $out->addHTML( Xml::fieldset( $this->msg( 'import-upload' )->text() ) . Xml::openElement( @@ -255,22 +339,11 @@ class SpecialImport extends SpecialPage { "</td> <td class='mw-input'>" . Xml::input( 'log-comment', 50, - ( $this->sourceName == 'upload' ? $this->logcomment : '' ), + ( $this->sourceName === 'upload' ? $this->logcomment : '' ), array( 'id' => 'mw-import-comment', 'type' => 'text' ) ) . ' ' . "</td> </tr> - <tr> - <td class='mw-label'>" . - Xml::label( - $this->msg( 'import-interwiki-rootpage' )->text(), - 'mw-interwiki-rootpage-upload' - ) . - "</td> - <td class='mw-input'>" . - Xml::input( 'rootpage', 50, $this->rootpage, - array( 'id' => 'mw-interwiki-rootpage-upload', 'type' => 'text' ) ) . ' ' . - "</td> - </tr> + $mappingSelection <tr> <td></td> <td class='mw-submit'>" . @@ -301,6 +374,7 @@ class SpecialImport extends SpecialPage { "</td> </tr>"; } + $mappingSelection = $this->getMappingFormPart( 'interwiki' ); $out->addHTML( Xml::fieldset( $this->msg( 'importinterwiki' )->text() ) . @@ -415,43 +489,15 @@ class SpecialImport extends SpecialPage { $importDepth <tr> <td class='mw-label'>" . - Xml::label( $this->msg( 'import-interwiki-namespace' )->text(), 'namespace' ) . - "</td> - <td class='mw-input'>" . - Html::namespaceSelector( - array( - 'selected' => $this->namespace, - 'all' => '', - ), array( - 'name' => 'namespace', - 'id' => 'namespace', - 'class' => 'namespaceselector', - ) - ) . - "</td> - </tr> - <tr> - <td class='mw-label'>" . Xml::label( $this->msg( 'import-comment' )->text(), 'mw-interwiki-comment' ) . "</td> <td class='mw-input'>" . Xml::input( 'log-comment', 50, - ( $this->sourceName == 'interwiki' ? $this->logcomment : '' ), + ( $this->sourceName === 'interwiki' ? $this->logcomment : '' ), array( 'id' => 'mw-interwiki-comment', 'type' => 'text' ) ) . ' ' . "</td> </tr> - <tr> - <td class='mw-label'>" . - Xml::label( - $this->msg( 'import-interwiki-rootpage' )->text(), - 'mw-interwiki-rootpage-interwiki' - ) . - "</td> - <td class='mw-input'>" . - Xml::input( 'rootpage', 50, $this->rootpage, - array( 'id' => 'mw-interwiki-rootpage-interwiki', 'type' => 'text' ) ) . ' ' . - "</td> - </tr> + $mappingSelection <tr> <td> </td> diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php index ecb166a4..fbdefea4 100644 --- a/includes/specials/SpecialJavaScriptTest.php +++ b/includes/specials/SpecialJavaScriptTest.php @@ -44,6 +44,11 @@ class SpecialJavaScriptTest extends SpecialPage { if ( $par === null ) { // No framework specified + // If only one framework is configured, redirect to it. Otherwise display a list. + if ( count( self::$frameworks ) === 1 ) { + $out->redirect( $this->getPageTitle( self::$frameworks[0] . '/plain' )->getLocalURL() ); + return; + } $out->setStatusCode( 404 ); $out->setPageTitle( $this->msg( 'javascripttest' ) ); $out->addHTML( @@ -74,10 +79,14 @@ class SpecialJavaScriptTest extends SpecialPage { // no sensitive data. In order to allow TestSwarm to embed it into a test client window, // we need to allow iframing of this page. $out->allowClickjacking(); - $out->setSubtitle( - $this->msg( 'javascripttest-backlink' ) - ->rawParams( Linker::linkKnown( $this->getPageTitle() ) ) - ); + if ( count( self::$frameworks ) !== 1 ) { + // If there's only one framework, don't set the subtitle since it + // is going to redirect back to this page + $out->setSubtitle( + $this->msg( 'javascripttest-backlink' ) + ->rawParams( Linker::linkKnown( $this->getPageTitle() ) ) + ); + } // Custom actions if ( isset( $pars[1] ) ) { @@ -134,13 +143,15 @@ class SpecialJavaScriptTest extends SpecialPage { } /** - * Wrap HTML contents in a summary container. + * Get summary text wrapped in a container * - * @param string $html HTML contents to be wrapped * @return string HTML */ - private function wrapSummaryHtml( $html ) { - return "<div id=\"mw-javascripttest-summary\">$html</div>"; + private function getSummaryHtml() { + $summary = $this->msg( 'javascripttest-qunit-intro' ) + ->params( 'https://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing' ) + ->parseAsBlock(); + return "<div id=\"mw-javascripttest-summary\">$summary</div>"; } /** @@ -153,30 +164,24 @@ class SpecialJavaScriptTest extends SpecialPage { $modules = $out->getResourceLoader()->getTestModuleNames( 'qunit' ); - $summary = $this->msg( 'javascripttest-qunit-intro' ) - ->params( 'https://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing' ) - ->parseAsBlock(); - $baseHtml = <<<HTML <div class="mw-content-ltr"> <div id="qunit"></div> </div> HTML; - $out->addHtml( $this->wrapSummaryHtml( $summary ) . $baseHtml ); + $out->addHtml( $this->getSummaryHtml() . $baseHtml ); // The testrunner configures QUnit and essentially depends on it. However, test suites // are reusable in environments that preload QUnit (or a compatibility interface to // another framework). Therefore we have to load it ourselves. - $out->addHtml( Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - Xml::encodeJsCall( 'mw.loader.using', array( - array( 'jquery.qunit', 'jquery.qunit.completenessTest' ), - new XmlJsCode( - 'function () {' . Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) . '}' - ) - ) ) - ) + $out->addHtml( ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.loader.using', array( + array( 'jquery.qunit', 'jquery.qunit.completenessTest' ), + new XmlJsCode( + 'function () {' . Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) . '}' + ) + ) ) ) ); } @@ -200,13 +205,27 @@ HTML; 'lang' => $this->getLanguage()->getCode(), 'skin' => $this->getSkin()->getSkinName(), 'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false', + 'target' => 'test', ); $embedContext = new ResourceLoaderContext( $rl, new FauxRequest( $query ) ); $query['only'] = 'scripts'; $startupContext = new ResourceLoaderContext( $rl, new FauxRequest( $query ) ); + $query['raw'] = true; + $modules = $rl->getTestModuleNames( 'qunit' ); + // Disable autostart because we load modules asynchronously. By default, QUnit would start + // at domready when there are no tests loaded and also fire 'QUnit.done' which then instructs + // Karma to end the run before the tests even started. + $qunitConfig = 'QUnit.config.autostart = false;' + . 'if (window.__karma__) {' + // karma-qunit's use of autostart=false and QUnit.start conflicts with ours. + // Hack around this by replacing 'karma.loaded' with a no-op and call it ourselves later. + // See <https://github.com/karma-runner/karma-qunit/issues/27>. + . 'window.__karma__.loaded = function () {};' + . '}'; + // The below is essentially a pure-javascript version of OutputPage::getHeadScripts. $startup = $rl->makeModuleResponse( $startupContext, array( 'startup' => $rl->getModule( 'startup' ), @@ -220,43 +239,54 @@ HTML; 'user.options' => $rl->getModule( 'user.options' ), 'user.tokens' => $rl->getModule( 'user.tokens' ), ) ); - $code .= Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ); + // Catch exceptions (such as "dependency missing" or "unknown module") so that we + // always start QUnit. Re-throw so that they are caught and reported as global exceptions + // by QUnit and Karma. + $code .= '(function () {' + . 'var start = window.__karma__ ? window.__karma__.start : QUnit.start;' + . 'try {' + . 'mw.loader.using( ' . Xml::encodeJsVar( $modules ) . ' ).always( start );' + . '} catch ( e ) { start(); throw e; }' + . '}());'; header( 'Content-Type: text/javascript; charset=utf-8' ); header( 'Cache-Control: private, no-cache, must-revalidate' ); header( 'Pragma: no-cache' ); + echo $qunitConfig; echo $startup; - echo "\n"; - // Note: The following has to be wrapped in a script tag because the startup module also - // writes a script tag (the one loading mediawiki.js). Script tags are synchronous, block - // each other, and run in order. But they don't nest. The code appended after the startup - // module runs before the added script tag is parsed and executed. - echo Xml::encodeJsCall( 'document.write', array( Html::inlineScript( $code ) ) ); + // The following has to be deferred via RLQ because the startup module is asynchronous. + echo ResourceLoader::makeLoaderConditionalScript( $code ); } private function plainQUnit() { $out = $this->getOutput(); $out->disable(); - $url = $this->getPageTitle( 'qunit/export' )->getFullURL( array( - 'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false', - ) ); - - $styles = $out->makeResourceLoaderLink( - 'jquery.qunit', ResourceLoaderModule::TYPE_STYLES, false + $styles = $out->makeResourceLoaderLink( 'jquery.qunit', + ResourceLoaderModule::TYPE_STYLES ); - // Use 'raw' since this is a plain HTML page without ResourceLoader - $scripts = $out->makeResourceLoaderLink( - 'jquery.qunit', ResourceLoaderModule::TYPE_SCRIPTS, false, array( 'raw' => 'true' ) + + // Use 'raw' because QUnit loads before ResourceLoader initialises (omit mw.loader.state call) + // Use 'test' to ensure OutputPage doesn't use the "async" attribute because QUnit must + // load before qunit/export. + $scripts = $out->makeResourceLoaderLink( 'jquery.qunit', + ResourceLoaderModule::TYPE_SCRIPTS, + array( 'raw' => true, 'sync' => true ) ); - $head = trim( $styles['html'] . $scripts['html'] ); + $head = implode( "\n", array_merge( $styles['html'], $scripts['html'] ) ); + $summary = $this->getSummaryHtml(); $html = <<<HTML <!DOCTYPE html> <title>QUnit</title> $head +$summary <div id="qunit"></div> HTML; + + $url = $this->getPageTitle( 'qunit/export' )->getFullURL( array( + 'debug' => ResourceLoader::inDebugMode() ? 'true' : 'false', + ) ); $html .= "\n" . Html::linkedScript( $url ); header( 'Content-Type: text/html; charset=utf-8' ); diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php index 75ff8f30..f4748674 100644 --- a/includes/specials/SpecialLinkSearch.php +++ b/includes/specials/SpecialLinkSearch.php @@ -80,7 +80,7 @@ class LinkSearchPage extends QueryPage { return false; } - function execute( $par ) { + public function execute( $par ) { $this->initServices(); $this->setHeaders(); @@ -91,7 +91,7 @@ class LinkSearchPage extends QueryPage { $request = $this->getRequest(); $target = $request->getVal( 'target', $par ); - $namespace = $request->getIntOrNull( 'namespace', null ); + $namespace = $request->getIntOrNull( 'namespace' ); $protocols_list = array(); foreach ( $this->getConfig()->get( 'UrlProtocols' ) as $prot ) { @@ -121,43 +121,41 @@ class LinkSearchPage extends QueryPage { '<nowiki>' . $this->getLanguage()->commaList( $protocols_list ) . '</nowiki>', count( $protocols_list ) ); - $s = Html::openElement( - 'form', - array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => wfScript() ) - ) . "\n" . - Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . "\n" . - Html::openElement( 'fieldset' ) . "\n" . - Html::element( 'legend', array(), $this->msg( 'linksearch' )->text() ) . "\n" . - Xml::inputLabel( - $this->msg( 'linksearch-pat' )->text(), - 'target', - 'target', - 50, - $target, - array( - // URLs are always ltr - 'dir' => 'ltr', - ) - ) . "\n"; - + $fields = array( + 'target' => array( + 'type' => 'text', + 'name' => 'target', + 'id' => 'target', + 'size' => 50, + 'label-message' => 'linksearch-pat', + 'default' => $target, + 'dir' => 'ltr', + ) + ); if ( !$this->getConfig()->get( 'MiserMode' ) ) { - $s .= Html::namespaceSelector( - array( - 'selected' => $namespace, - 'all' => '', - 'label' => $this->msg( 'linksearch-ns' )->text() - ), array( + $fields += array( + 'namespace' => array( + 'type' => 'namespaceselect', 'name' => 'namespace', + 'label-message' => 'linksearch-ns', + 'default' => $namespace, 'id' => 'namespace', - 'class' => 'namespaceselector', - ) + 'all' => '', + 'cssclass' => 'namespaceselector', + ), ); } - - $s .= Xml::submitButton( $this->msg( 'linksearch-ok' )->text() ) . "\n" . - Html::closeElement( 'fieldset' ) . "\n" . - Html::closeElement( 'form' ) . "\n"; - $out->addHTML( $s ); + $hiddenFields = array( + 'title' => $this->getPageTitle()->getPrefixedDBkey(), + ); + $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() ); + $htmlForm->addHiddenFields( $hiddenFields ); + $htmlForm->setSubmitTextMsg( 'linksearch-ok' ); + $htmlForm->setWrapperLegendMsg( 'linksearch' ); + $htmlForm->setAction( wfScript() ); + $htmlForm->setMethod( 'get' ); + $htmlForm->prepareForm()->displayForm( false ); + $this->addHelpLink( 'Help:Linksearch' ); if ( $target != '' ) { $this->setParams( array( @@ -220,7 +218,7 @@ class LinkSearchPage extends QueryPage { return $params; } - function getQueryInfo() { + public function getQueryInfo() { $dbr = wfGetDB( DB_SLAVE ); // strip everything past first wildcard, so that // index-based-only lookup would be done diff --git a/includes/specials/SpecialListDuplicatedFiles.php b/includes/specials/SpecialListDuplicatedFiles.php index 1e3dff6f..317b62fe 100644 --- a/includes/specials/SpecialListDuplicatedFiles.php +++ b/includes/specials/SpecialListDuplicatedFiles.php @@ -34,7 +34,7 @@ class ListDuplicatedFilesPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -53,7 +53,7 @@ class ListDuplicatedFilesPage extends QueryPage { * with however we are doing cached special pages. * @return array */ - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'image' ), 'fields' => array( diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php index d4b45fb3..2d79aaf8 100644 --- a/includes/specials/SpecialListfiles.php +++ b/includes/specials/SpecialListfiles.php @@ -84,14 +84,26 @@ class ImageListPager extends TablePager { function __construct( IContextSource $context, $userName = null, $search = '', $including = false, $showAll = false ) { + $this->setContext( $context ); $this->mIncluding = $including; $this->mShowAll = $showAll; if ( $userName !== null && $userName !== '' ) { $nt = Title::newFromText( $userName, NS_USER ); + $user = User::newFromName( $userName, false ); if ( !is_null( $nt ) ) { $this->mUserName = $nt->getText(); } + if ( !$user || ( $user->isAnon() && !User::isIP( $user->getName() ) ) ) { + $this->getOutput()->wrapWikiMsg( + "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>", + array( + 'listfiles-userdoesnotexist', + wfEscapeWikiText( $userName ), + ) + ); + } + } if ( $search !== '' && !$this->getConfig()->get( 'MiserMode' ) ) { @@ -107,7 +119,7 @@ class ImageListPager extends TablePager { } if ( !$including ) { - if ( $context->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) { + if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) { $this->mDefaultDirection = IndexPager::DIR_DESCENDING; } else { $this->mDefaultDirection = IndexPager::DIR_ASCENDING; diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php index 2df48347..fa94b4ab 100644 --- a/includes/specials/SpecialListredirects.php +++ b/includes/specials/SpecialListredirects.php @@ -33,7 +33,7 @@ class ListredirectsPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -45,7 +45,7 @@ class ListredirectsPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'p1' => 'page', 'redirect', 'p2' => 'page' ), 'fields' => array( 'namespace' => 'p1.page_namespace', diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php index 56c4eb50..31200c84 100644 --- a/includes/specials/SpecialListusers.php +++ b/includes/specials/SpecialListusers.php @@ -263,6 +263,8 @@ class UsersPager extends AlphabeticPager { function getPageHeader() { list( $self ) = explode( '/', $this->getTitle()->getPrefixedDBkey() ); + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); + # Form tag $out = Xml::openElement( 'form', @@ -271,13 +273,14 @@ class UsersPager extends AlphabeticPager { Xml::fieldset( $this->msg( 'listusers' )->text() ) . Html::hidden( 'title', $self ); - # Username field + # Username field (with autocompletion support) $out .= Xml::label( $this->msg( 'listusersfrom' )->text(), 'offset' ) . ' ' . Html::input( 'username', $this->requestedUser, 'text', array( + 'class' => 'mw-autocomplete-user', 'id' => 'offset', 'size' => 20, 'autofocus' => $this->requestedUser === '' @@ -285,13 +288,14 @@ class UsersPager extends AlphabeticPager { ) . ' '; # Group drop-down list - $out .= Xml::label( $this->msg( 'group' )->text(), 'group' ) . ' ' . - Xml::openElement( 'select', array( 'name' => 'group', 'id' => 'group' ) ) . - Xml::option( $this->msg( 'group-all' )->text(), '' ); + $sel = new XmlSelect( 'group', 'group', $this->requestedGroup ); + $sel->addOption( $this->msg( 'group-all' )->text(), '' ); foreach ( $this->getAllGroups() as $group => $groupText ) { - $out .= Xml::option( $groupText, $group, $group == $this->requestedGroup ); + $sel->addOption( $groupText, $group ); } - $out .= Xml::closeElement( 'select' ) . '<br />'; + + $out .= Xml::label( $this->msg( 'group' )->text(), 'group' ) . ' '; + $out .= $sel->getHTML() . '<br />'; $out .= Xml::checkLabel( $this->msg( 'listusers-editsonly' )->text(), 'editsOnly', diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php index 1c1f1250..a276197d 100644 --- a/includes/specials/SpecialLockdb.php +++ b/includes/specials/SpecialLockdb.php @@ -73,9 +73,9 @@ class SpecialLockdb extends FormSpecialPage { return Status::newFatal( 'locknoconfirm' ); } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $fp = fopen( $this->getConfig()->get( 'ReadOnlyFile' ), 'w' ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( false === $fp ) { # This used to show a file not found error, but the likeliest reason for fopen() diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php index 60225ea5..32344a84 100644 --- a/includes/specials/SpecialMIMEsearch.php +++ b/includes/specials/SpecialMIMEsearch.php @@ -34,8 +34,8 @@ class MIMEsearchPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { - return false; + public function isExpensive() { + return true; } function isSyndicated() { @@ -109,7 +109,6 @@ class MIMEsearchPage extends QueryPage { * Return HTML to put just before the results. */ function getPageHeader() { - return Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => wfScript() ) @@ -124,7 +123,7 @@ class MIMEsearchPage extends QueryPage { Xml::closeElement( 'form' ); } - function execute( $par ) { + public function execute( $par ) { $this->mime = $par ? $par : $this->getRequest()->getText( 'mime' ); $this->mime = trim( $this->mime ); list( $this->major, $this->minor ) = File::splitMime( $this->mime ); diff --git a/includes/specials/SpecialMediaStatistics.php b/includes/specials/SpecialMediaStatistics.php index b62de5d2..e5ba8c60 100644 --- a/includes/specials/SpecialMediaStatistics.php +++ b/includes/specials/SpecialMediaStatistics.php @@ -36,7 +36,7 @@ class MediaStatisticsPage extends QueryPage { $this->shownavigation = false; } - function isExpensive() { + public function isExpensive() { return true; } @@ -111,7 +111,11 @@ class MediaStatisticsPage extends QueryPage { protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) { $prevMediaType = null; foreach ( $res as $row ) { - list( $mediaType, $mime, $totalCount, $totalBytes ) = $this->splitFakeTitle( $row->title ); + $mediaStats = $this->splitFakeTitle( $row->title ); + if ( count( $mediaStats ) < 4 ) { + continue; + } + list( $mediaType, $mime, $totalCount, $totalBytes ) = $mediaStats; if ( $prevMediaType !== $mediaType ) { if ( $prevMediaType !== null ) { // We're not at beginning, so we have to @@ -232,7 +236,7 @@ class MediaStatisticsPage extends QueryPage { 'mw-mediastats-table-' . strtolower( $mediaType ), 'sortable', 'wikitable' - )) + ) ) ) ); $this->getOutput()->addHTML( $this->getTableHeaderRow() ); @@ -271,7 +275,7 @@ class MediaStatisticsPage extends QueryPage { array( 'class' => array( 'mw-mediastats-mediatype', 'mw-mediastats-mediatype-' . strtolower( $mediaType ) - )), + ) ), // for grep // mediastatistics-header-unknown, mediastatistics-header-bitmap, // mediastatistics-header-drawing, mediastatistics-header-audio, diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php index 1f0b6d45..7edf961a 100644 --- a/includes/specials/SpecialMergeHistory.php +++ b/includes/specials/SpecialMergeHistory.php @@ -109,6 +109,8 @@ class SpecialMergeHistory extends SpecialPage { } public function execute( $par ) { + $this->useTransactionalTimeLimit(); + $this->checkPermissions(); $this->checkReadOnly(); diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php index c70bbdba..18083f61 100644 --- a/includes/specials/SpecialMostcategories.php +++ b/includes/specials/SpecialMostcategories.php @@ -34,7 +34,7 @@ class MostcategoriesPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -42,7 +42,7 @@ class MostcategoriesPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'categorylinks', 'page' ), 'fields' => array( diff --git a/includes/specials/SpecialMostinterwikis.php b/includes/specials/SpecialMostinterwikis.php index ab3d9c91..b07b8331 100644 --- a/includes/specials/SpecialMostinterwikis.php +++ b/includes/specials/SpecialMostinterwikis.php @@ -34,7 +34,7 @@ class MostinterwikisPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -42,7 +42,7 @@ class MostinterwikisPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'langlinks', diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php index ae0b0708..019df493 100644 --- a/includes/specials/SpecialMostlinked.php +++ b/includes/specials/SpecialMostlinked.php @@ -35,7 +35,7 @@ class MostlinkedPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -43,7 +43,7 @@ class MostlinkedPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'pagelinks', 'page' ), 'fields' => array( diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php index cc718e06..6eeab91b 100644 --- a/includes/specials/SpecialMostlinkedcategories.php +++ b/includes/specials/SpecialMostlinkedcategories.php @@ -38,7 +38,7 @@ class MostlinkedCategoriesPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'category' ), 'fields' => array( 'title' => 'cat_title', diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index ae1fefea..8091f1b0 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -28,7 +28,7 @@ */ class MovePageForm extends UnlistedSpecialPage { /** @var Title */ - protected $oldTitle; + protected $oldTitle = null; /** @var Title */ protected $newTitle; @@ -64,6 +64,8 @@ class MovePageForm extends UnlistedSpecialPage { } public function execute( $par ) { + $this->useTransactionalTimeLimit(); + $this->checkReadOnly(); $this->setHeaders(); @@ -75,9 +77,12 @@ class MovePageForm extends UnlistedSpecialPage { // Yes, the use of getVal() and getText() is wanted, see bug 20365 $oldTitleText = $request->getVal( 'wpOldTitle', $target ); - $this->oldTitle = Title::newFromText( $oldTitleText ); + if ( is_string( $oldTitleText ) ) { + $this->oldTitle = Title::newFromText( $oldTitleText ); + } - if ( is_null( $this->oldTitle ) ) { + if ( $this->oldTitle === null ) { + // Either oldTitle wasn't passed, or newFromText returned null throw new ErrorPageError( 'notargettitle', 'notargettext' ); } if ( !$this->oldTitle->exists() ) { @@ -99,7 +104,9 @@ class MovePageForm extends UnlistedSpecialPage { $permErrors = $this->oldTitle->getUserPermissionsErrors( 'move', $user ); if ( count( $permErrors ) ) { // Auto-block user's IP if the account was "hard" blocked - $user->spreadAnyEditBlock(); + DeferredUpdates::addCallableUpdate( function() use ( $user ) { + $user->spreadAnyEditBlock(); + } ); throw new PermissionsError( 'move', $permErrors ); } @@ -140,6 +147,7 @@ class MovePageForm extends UnlistedSpecialPage { $out = $this->getOutput(); $out->setPageTitle( $this->msg( 'move-page', $this->oldTitle->getPrefixedText() ) ); $out->addModules( 'mediawiki.special.movePage' ); + $out->addModuleStyles( 'mediawiki.special.movePage.styles' ); $this->addHelpLink( 'Help:Moving a page' ); $newTitle = $this->newTitle; @@ -283,7 +291,6 @@ class MovePageForm extends UnlistedSpecialPage { // is enforced in the mediawiki.special.movePage module $immovableNamespaces = array(); - foreach ( array_keys( $this->getLanguage()->getNamespaces() ) as $nsId ) { if ( !MWNamespace::isMovable( $nsId ) ) { $immovableNamespaces[] = $nsId; @@ -292,202 +299,207 @@ class MovePageForm extends UnlistedSpecialPage { $handler = ContentHandler::getForTitle( $this->oldTitle ); - $out->addHTML( - Xml::openElement( - 'form', - array( - 'method' => 'post', - 'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ), - 'id' => 'movepage' - ) - ) . - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, $this->msg( 'move-page-legend' )->text() ) . - Xml::openElement( 'table', array( 'id' => 'mw-movepage-table' ) ) + $out->enableOOUI(); + $fields = array(); + + $fields[] = new OOUI\FieldLayout( + new OOUI\LabelWidget( array( + 'label' => new OOUI\HtmlSnippet( "<strong>$oldTitleLink</strong>" ) + ) ), + array( + 'label' => $this->msg( 'movearticle' )->text(), + 'align' => 'top', + ) ); - $out->addHTML( - "<tr> - <td class='mw-label'>" . - $this->msg( 'movearticle' )->escaped() . - "</td> - <td class='mw-input'> - <strong>{$oldTitleLink}</strong> - </td> - </tr> - <tr> - <td class='mw-label'>" . - 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() ) . - "</td> - </tr> - <tr> - <td class='mw-label'>" . - Xml::label( $this->msg( 'movereason' )->text(), 'wpReason' ) . - "</td> - <td class='mw-input'>" . - Xml::input( 'wpReason', 60, $this->reason, array( - 'type' => 'text', - 'id' => 'wpReason', - 'maxlength' => 200, - ) ) . - "</td> - </tr>" + $fields[] = new OOUI\FieldLayout( + new MediaWiki\Widget\ComplexTitleInputWidget( array( + 'id' => 'wpNewTitle', + 'namespace' => array( + 'id' => 'wpNewTitleNs', + 'name' => 'wpNewTitleNs', + 'value' => $newTitle->getNamespace(), + 'exclude' => $immovableNamespaces, + ), + 'title' => array( + 'id' => 'wpNewTitleMain', + 'name' => 'wpNewTitleMain', + 'value' => $wgContLang->recodeForEdit( $newTitle->getText() ), + // Inappropriate, since we're expecting the user to input a non-existent page's title + 'suggestions' => false, + ), + 'infusable' => true, + ) ), + array( + 'label' => $this->msg( 'newtitle' )->text(), + 'align' => 'top', + ) + ); + + $fields[] = new OOUI\FieldLayout( + new OOUI\TextInputWidget( array( + 'name' => 'wpReason', + 'id' => 'wpReason', + 'maxLength' => 200, + 'infusable' => true, + ) ), + array( + 'label' => $this->msg( 'movereason' )->text(), + 'align' => 'top', + ) ); if ( $considerTalk ) { - $out->addHTML( " - <tr> - <td></td> - <td class='mw-input'>" . - Xml::checkLabel( - $this->msg( 'movetalk' )->text(), - 'wpMovetalk', - 'wpMovetalk', - $this->moveTalk - ) . - "</td> - </tr>" + $fields[] = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( array( + 'name' => 'wpMovetalk', + 'id' => 'wpMovetalk', + 'value' => '1', + 'selected' => $this->moveTalk, + ) ), + array( + 'label' => $this->msg( 'movetalk' )->text(), + 'align' => 'inline', + ) ); } if ( $user->isAllowed( 'suppressredirect' ) ) { if ( $handler->supportsRedirects() ) { $isChecked = $this->leaveRedirect; - $options = array(); + $isDisabled = false; } else { $isChecked = false; - $options = array( - 'disabled' => 'disabled' - ); + $isDisabled = true; } - $out->addHTML( " - <tr> - <td></td> - <td class='mw-input'>" . - Xml::checkLabel( - $this->msg( 'move-leave-redirect' )->text(), - 'wpLeaveRedirect', - 'wpLeaveRedirect', - $isChecked, - $options - ) . - "</td> - </tr>" + $fields[] = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( array( + 'name' => 'wpLeaveRedirect', + 'id' => 'wpLeaveRedirect', + 'value' => '1', + 'selected' => $isChecked, + 'disabled' => $isDisabled, + ) ), + array( + 'label' => $this->msg( 'move-leave-redirect' )->text(), + 'align' => 'inline', + ) ); } if ( $hasRedirects ) { - $out->addHTML( " - <tr> - <td></td> - <td class='mw-input'>" . - Xml::checkLabel( - $this->msg( 'fix-double-redirects' )->text(), - 'wpFixRedirects', - 'wpFixRedirects', - $this->fixRedirects - ) . - "</td> - </tr>" + $fields[] = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( array( + 'name' => 'wpFixRedirects', + 'id' => 'wpFixRedirects', + 'value' => '1', + 'selected' => $this->fixRedirects, + ) ), + array( + 'label' => $this->msg( 'fix-double-redirects' )->text(), + 'align' => 'inline', + ) ); } if ( $canMoveSubpage ) { $maximumMovedPages = $this->getConfig()->get( 'MaximumMovedPages' ); - $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> - </tr>" + $fields[] = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( array( + 'name' => 'wpMovesubpages', + 'id' => 'wpMovesubpages', + 'value' => '1', + # Don't check the box if we only have talk subpages to + # move and we aren't moving the talk page. + 'selected' => $this->moveSubpages && ( $this->oldTitle->hasSubpages() || $this->moveTalk ), + ) ), + array( + 'label' => new OOUI\HtmlSnippet( + $this->msg( + ( $this->oldTitle->hasSubpages() + ? 'move-subpages' + : 'move-talk-subpages' ) + )->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse() + ), + 'align' => 'inline', + ) ); } - $watchChecked = $user->isLoggedIn() && ( $this->watch || $user->getBoolOption( 'watchmoves' ) - || $user->isWatched( $this->oldTitle ) ); # 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>" + $watchChecked = $user->isLoggedIn() && ( $this->watch || $user->getBoolOption( 'watchmoves' ) + || $user->isWatched( $this->oldTitle ) ); + $fields[] = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( array( + 'name' => 'wpWatch', + 'id' => 'watch', # ew + 'value' => '1', + 'selected' => $watchChecked, + ) ), + array( + 'label' => $this->msg( 'move-watch' )->text(), + 'align' => 'inline', + ) ); } if ( $confirm ) { - $out->addHTML( " - <tr> - <td></td> - <td class='mw-input'>" . - Xml::checkLabel( - $this->msg( 'delete_and_move_confirm' )->text(), - 'wpConfirm', - 'wpConfirm' - ) . - "</td> - </tr>" + $watchChecked = $user->isLoggedIn() && ( $this->watch || $user->getBoolOption( 'watchmoves' ) + || $user->isWatched( $this->oldTitle ) ); + $fields[] = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( array( + 'name' => 'wpConfirm', + 'id' => 'wpConfirm', + 'value' => '1', + ) ), + array( + 'label' => $this->msg( 'delete_and_move_confirm' )->text(), + 'align' => 'inline', + ) ); } - $out->addHTML( " - <tr> - <td></td> - <td class='mw-submit'>" . - Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) . - "</td> - </tr>" + $fields[] = new OOUI\FieldLayout( + new OOUI\ButtonInputWidget( array( + 'name' => $submitVar, + 'value' => $movepagebtn, + 'label' => $movepagebtn, + 'flags' => array( 'progressive', 'primary' ), + 'type' => 'submit', + ) ), + array( + 'align' => 'top', + ) + ); + + $fieldset = new OOUI\FieldsetLayout( array( + 'label' => $this->msg( 'move-page-legend' )->text(), + 'id' => 'mw-movepage-table', + 'items' => $fields, + ) ); + + $form = new OOUI\FormLayout( array( + 'method' => 'post', + 'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ), + 'id' => 'movepage', + ) ); + $form->appendContent( + $fieldset, + new OOUI\HtmlSnippet( + Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) . + Html::hidden( 'wpEditToken', $user->getEditToken() ) + ) ); $out->addHTML( - Xml::closeElement( 'table' ) . - Html::hidden( 'wpEditToken', $user->getEditToken() ) . - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ) . - "\n" + new OOUI\PanelLayout( array( + 'classes' => array( 'movepage-wrapper' ), + 'expanded' => false, + 'padded' => true, + 'framed' => true, + 'content' => $form, + ) ) ); $this->showLogFragment( $this->oldTitle ); diff --git a/includes/specials/SpecialMyLanguage.php b/includes/specials/SpecialMyLanguage.php index 6cea1581..d11fbe63 100644 --- a/includes/specials/SpecialMyLanguage.php +++ b/includes/specials/SpecialMyLanguage.php @@ -41,11 +41,11 @@ class SpecialMyLanguage extends RedirectSpecialArticle { * If the special page is a redirect, then get the Title object it redirects to. * False otherwise. * - * @param string $par Subpage string - * @return Title|bool + * @param string|null $subpage + * @return Title */ - public function getRedirect( $par ) { - $title = $this->findTitle( $par ); + public function getRedirect( $subpage ) { + $title = $this->findTitle( $subpage ); // Go to the main page if given invalid title. if ( !$title ) { $title = Title::newMainPage(); @@ -59,18 +59,22 @@ class SpecialMyLanguage extends RedirectSpecialArticle { * it returns Page/fi if it exists, otherwise Page/de if it exists, * otherwise Page. * - * @param string $par + * @param string|null $subpage * @return Title|null */ - public function findTitle( $par ) { + public function findTitle( $subpage ) { // base = title without language code suffix // provided = the title as it was given - $base = $provided = Title::newFromText( $par ); + $base = $provided = null; + if ( $subpage !== null ) { + $provided = Title::newFromText( $subpage ); + $base = $provided; + } - if ( $base && strpos( $par, '/' ) !== false ) { - $pos = strrpos( $par, '/' ); - $basepage = substr( $par, 0, $pos ); - $code = substr( $par, $pos + 1 ); + if ( $provided && strpos( $subpage, '/' ) !== false ) { + $pos = strrpos( $subpage, '/' ); + $basepage = substr( $subpage, 0, $pos ); + $code = substr( $subpage, $pos + 1 ); if ( strlen( $code ) && Language::isKnownLanguageTag( $code ) ) { $base = Title::newFromText( $basepage ); } @@ -95,4 +99,15 @@ class SpecialMyLanguage extends RedirectSpecialArticle { return $base; } } + + /** + * Target can identify a specific user's language preference. + * + * @see T109724 + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } diff --git a/includes/specials/SpecialMyRedirectPages.php b/includes/specials/SpecialMyRedirectPages.php index 9b8d52bb..850b1f63 100644 --- a/includes/specials/SpecialMyRedirectPages.php +++ b/includes/specials/SpecialMyRedirectPages.php @@ -30,16 +30,30 @@ * @ingroup SpecialPage */ class SpecialMypage extends RedirectSpecialArticle { - function __construct() { + public function __construct() { parent::__construct( 'Mypage' ); } - function getRedirect( $subpage ) { - if ( strval( $subpage ) !== '' ) { - return Title::makeTitle( NS_USER, $this->getUser()->getName() . '/' . $subpage ); - } else { + /** + * @param string|null $subpage + * @return Title + */ + public function getRedirect( $subpage ) { + if ( $subpage === null || $subpage === '' ) { return Title::makeTitle( NS_USER, $this->getUser()->getName() ); } + + return Title::makeTitle( NS_USER, $this->getUser()->getName() . '/' . $subpage ); + } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; } } @@ -49,16 +63,30 @@ class SpecialMypage extends RedirectSpecialArticle { * @ingroup SpecialPage */ class SpecialMytalk extends RedirectSpecialArticle { - function __construct() { + public function __construct() { parent::__construct( 'Mytalk' ); } - function getRedirect( $subpage ) { - if ( strval( $subpage ) !== '' ) { - return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() . '/' . $subpage ); - } else { + /** + * @param string|null $subpage + * @return Title + */ + public function getRedirect( $subpage ) { + if ( $subpage === null || $subpage === '' ) { return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() ); } + + return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() . '/' . $subpage ); + } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; } } @@ -68,15 +96,30 @@ class SpecialMytalk extends RedirectSpecialArticle { * @ingroup SpecialPage */ class SpecialMycontributions extends RedirectSpecialPage { - function __construct() { + public function __construct() { parent::__construct( 'Mycontributions' ); $this->mAllowedRedirectParams = array( 'limit', 'namespace', 'tagfilter', - 'offset', 'dir', 'year', 'month', 'feed' ); + 'offset', 'dir', 'year', 'month', 'feed', 'deletedOnly', + 'nsInvert', 'associated', 'newOnly', 'topOnly' ); } - function getRedirect( $subpage ) { + /** + * @param string|null $subpage + * @return Title + */ + public function getRedirect( $subpage ) { return SpecialPage::getTitleFor( 'Contributions', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -85,14 +128,28 @@ class SpecialMycontributions extends RedirectSpecialPage { * @ingroup SpecialPage */ class SpecialMyuploads extends RedirectSpecialPage { - function __construct() { + public function __construct() { parent::__construct( 'Myuploads' ); $this->mAllowedRedirectParams = array( 'limit', 'ilshowall', 'ilsearch' ); } - function getRedirect( $subpage ) { + /** + * @param string|null $subpage + * @return Title + */ + public function getRedirect( $subpage ) { return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -101,14 +158,28 @@ class SpecialMyuploads extends RedirectSpecialPage { * @ingroup SpecialPage */ class SpecialAllMyUploads extends RedirectSpecialPage { - function __construct() { + public function __construct() { parent::__construct( 'AllMyUploads' ); $this->mAllowedRedirectParams = array( 'limit', 'ilsearch' ); } - function getRedirect( $subpage ) { + /** + * @param string|null $subpage + * @return Title + */ + public function getRedirect( $subpage ) { $this->mAddedRedirectParams['ilshowall'] = 1; return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 899c7368..251a8e03 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -233,7 +233,7 @@ class SpecialNewpages extends IncludableSpecialPage { 'name' => 'invert', 'label-message' => 'invert', 'default' => $nsinvert, - 'tooltip' => $this->msg( 'tooltip-invert' )->text(), + 'tooltip' => 'invert', ), 'tagFilter' => array( 'type' => 'tagfilter', @@ -594,7 +594,7 @@ class NewPagesPager extends ReverseChronologicalPager { foreach ( $this->mResult as $row ) { $linkBatch->add( NS_USER, $row->rc_user_text ); $linkBatch->add( NS_USER_TALK, $row->rc_user_text ); - $linkBatch->add( $row->rc_namespace, $row->rc_title ); + $linkBatch->add( $row->page_namespace, $row->page_title ); } $linkBatch->execute(); diff --git a/includes/specials/SpecialPageLanguage.php b/includes/specials/SpecialPageLanguage.php index 79b2444e..6756f274 100644 --- a/includes/specials/SpecialPageLanguage.php +++ b/includes/specials/SpecialPageLanguage.php @@ -87,11 +87,14 @@ class SpecialPageLanguage extends FormSpecialPage { } protected function postText() { - return $this->showLogFragment( $this->par ); + if ( $this->par ) { + return $this->showLogFragment( $this->par ); + } + return ''; } protected function getDisplayFormat() { - return 'vform'; + return 'ooui'; } public function alterForm( HTMLForm $form ) { diff --git a/includes/specials/SpecialPagesWithProp.php b/includes/specials/SpecialPagesWithProp.php index 670a3973..f211ec9b 100644 --- a/includes/specials/SpecialPagesWithProp.php +++ b/includes/specials/SpecialPagesWithProp.php @@ -40,7 +40,7 @@ class SpecialPagesWithProp extends QueryPage { return false; } - function execute( $par ) { + public function execute( $par ) { $this->setHeaders(); $this->outputHeader(); $this->getOutput()->addModuleStyles( 'mediawiki.special.pagesWithProp' ); @@ -100,7 +100,7 @@ class SpecialPagesWithProp extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'page_props', 'page' ), 'fields' => array( @@ -113,9 +113,11 @@ class SpecialPagesWithProp extends QueryPage { 'pp_value', ), 'conds' => array( - 'page_id = pp_page', 'pp_propname' => $this->propName, ), + 'join_conds' => array( + 'page' => array( 'INNER JOIN', 'page_id = pp_page' ) + ), 'options' => array() ); } diff --git a/includes/specials/SpecialPasswordReset.php b/includes/specials/SpecialPasswordReset.php index a2dc2add..8cad6168 100644 --- a/includes/specials/SpecialPasswordReset.php +++ b/includes/specials/SpecialPasswordReset.php @@ -104,7 +104,7 @@ class SpecialPasswordReset extends FormSpecialPage { } protected function getDisplayFormat() { - return 'vform'; + return 'ooui'; } public function alterForm( HTMLForm $form ) { diff --git a/includes/specials/SpecialPermanentLink.php b/includes/specials/SpecialPermanentLink.php index 17115e88..53789c0d 100644 --- a/includes/specials/SpecialPermanentLink.php +++ b/includes/specials/SpecialPermanentLink.php @@ -27,12 +27,16 @@ * @ingroup SpecialPage */ class SpecialPermanentLink extends RedirectSpecialPage { - function __construct() { + public function __construct() { parent::__construct( 'PermanentLink' ); $this->mAllowedRedirectParams = array(); } - function getRedirect( $subpage ) { + /** + * @param string|null $subpage + * @return Title|bool + */ + public function getRedirect( $subpage ) { $subpage = intval( $subpage ); if ( $subpage === 0 ) { # throw an error page when no subpage was given diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index 7371da74..4b75e5f6 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -50,7 +50,14 @@ class SpecialPreferences extends SpecialPage { if ( $this->getRequest()->getCheck( 'success' ) ) { $out->wrapWikiMsg( - "<div class=\"successbox\">\n$1\n</div>", + Html::rawElement( + 'div', + array( + 'class' => 'mw-preferences-messagebox successbox', + 'id' => 'mw-preferences-success' + ), + Html::element( 'p', array(), '$1' ) + ), 'savedprefs' ); } diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php index dd9198cb..85ce78ff 100644 --- a/includes/specials/SpecialProtectedtitles.php +++ b/includes/specials/SpecialProtectedtitles.php @@ -67,16 +67,8 @@ class SpecialProtectedtitles extends SpecialPage { * @return string */ function formatRow( $row ) { - - static $infinity = null; - - if ( is_null( $infinity ) ) { - $infinity = wfGetDB( DB_SLAVE )->getInfinity(); - } - $title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title ); if ( !$title ) { - return Html::rawElement( 'li', array(), @@ -100,9 +92,9 @@ class SpecialProtectedtitles extends SpecialPage { $lang = $this->getLanguage(); $expiry = strlen( $row->pt_expiry ) ? $lang->formatExpiry( $row->pt_expiry, TS_MW ) : - $infinity; + 'infinity'; - if ( $expiry != $infinity ) { + if ( $expiry !== 'infinity' ) { $user = $this->getUser(); $description_items[] = $this->msg( 'protect-expiring-local', diff --git a/includes/specials/SpecialRandomInCategory.php b/includes/specials/SpecialRandomInCategory.php index b5c9e19a..7cf6b0a1 100644 --- a/includes/specials/SpecialRandomInCategory.php +++ b/includes/specials/SpecialRandomInCategory.php @@ -70,15 +70,15 @@ class SpecialRandomInCategory extends FormSpecialPage { protected function getFormFields() { $this->addHelpLink( 'Help:RandomInCategory' ); - $form = array( + return array( 'category' => array( - 'type' => 'text', + 'type' => 'title', + 'namespace' => NS_CATEGORY, + 'relative' => true, 'label-message' => 'randomincategory-category', 'required' => true, ) ); - - return $form; } public function requiresWrite() { @@ -89,6 +89,14 @@ class SpecialRandomInCategory extends FormSpecialPage { return false; } + protected function getDisplayFormat() { + return 'ooui'; + } + + protected function alterForm( HTMLForm $form ) { + $form->setSubmitTextMsg( 'randomincategory-submit' ); + } + protected function setParameter( $par ) { // if subpage present, fake form submission $this->onSubmit( array( 'category' => $par ) ); diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php index 73a88b9e..9f7ef669 100644 --- a/includes/specials/SpecialRandompage.php +++ b/includes/specials/SpecialRandompage.php @@ -135,20 +135,26 @@ class RandomPage extends SpecialPage { protected function getQueryInfo( $randstr ) { $redirect = $this->isRedirect() ? 1 : 0; + $tables = array( 'page' ); + $conds = array_merge( array( + 'page_namespace' => $this->namespaces, + 'page_is_redirect' => $redirect, + 'page_random >= ' . $randstr + ), $this->extra ); + $joinConds = array(); + + // Allow extensions to modify the query + Hooks::run( 'RandomPageQuery', array( &$tables, &$conds, &$joinConds ) ); return array( - 'tables' => array( 'page' ), + 'tables' => $tables, 'fields' => array( 'page_title', 'page_namespace' ), - 'conds' => array_merge( array( - 'page_namespace' => $this->namespaces, - 'page_is_redirect' => $redirect, - 'page_random >= ' . $randstr - ), $this->extra ), + 'conds' => $conds, 'options' => array( 'ORDER BY' => 'page_random', 'LIMIT' => 1, ), - 'join_conds' => array() + 'join_conds' => $joinConds ); } diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index 64b0ecae..0f201d52 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -57,6 +57,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage { return; } + $this->addHelpLink( + '//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Recent_changes', + true + ); parent::execute( $subpage ); } @@ -233,14 +237,21 @@ class SpecialRecentChanges extends ChangesListSpecialPage { return false; } - // rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough - // knowledge to use an index merge if it wants (it may use some other index though). + // array_merge() is used intentionally here so that hooks can, should + // they so desire, override the ORDER BY / LIMIT condition(s); prior to + // MediaWiki 1.26 this used to use the plus operator instead, which meant + // that extensions weren't able to change these conditions + $query_options = array_merge( array( + 'ORDER BY' => 'rc_timestamp DESC', + 'LIMIT' => $opts['limit'] ), $query_options ); $rows = $dbr->select( $tables, $fields, + // rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough + // knowledge to use an index merge if it wants (it may use some other index though). $conds + array( 'rc_new' => array( 0, 1 ) ), __METHOD__, - array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $opts['limit'] ) + $query_options, + $query_options, $join_conds ); @@ -263,6 +274,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage { ); } + protected function getDB() { + return wfGetDB( DB_SLAVE, 'recentchanges' ); + } + public function outputFeedLinks() { $this->addFeedLinks( $this->getFeedQuery() ); } @@ -272,7 +287,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { * * @return array */ - private function getFeedQuery() { + protected function getFeedQuery() { $query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) { // API handles empty parameters in a different way return $value !== ''; diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php index 3ad9f0f4..3c403feb 100644 --- a/includes/specials/SpecialRecentchangeslinked.php +++ b/includes/specials/SpecialRecentchangeslinked.php @@ -244,6 +244,7 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges { Xml::check( 'showlinkedto', $opts['showlinkedto'], array( 'id' => 'showlinkedto' ) ) . ' ' . Xml::label( $this->msg( 'recentchangeslinked-to' )->text(), 'showlinkedto' ) ); + $this->addHelpLink( 'Help:Related changes' ); return $extraOpts; } diff --git a/includes/specials/SpecialResetTokens.php b/includes/specials/SpecialResetTokens.php index ba2b9a5b..cba5a449 100644 --- a/includes/specials/SpecialResetTokens.php +++ b/includes/specials/SpecialResetTokens.php @@ -25,6 +25,7 @@ * Let users reset tokens like the watchlist token. * * @ingroup SpecialPage + * @deprecated 1.26 */ class SpecialResetTokens extends FormSpecialPage { private $tokensList; @@ -123,6 +124,10 @@ class SpecialResetTokens extends FormSpecialPage { } } + protected function getDisplayFormat() { + return 'ooui'; + } + public function onSubmit( array $formData ) { if ( $formData['tokens'] ) { $user = $this->getUser(); diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php index 3b5ef9d4..d1072b4e 100644 --- a/includes/specials/SpecialRevisiondelete.php +++ b/includes/specials/SpecialRevisiondelete.php @@ -110,6 +110,8 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { } public function execute( $par ) { + $this->useTransactionalTimeLimit(); + $this->checkPermissions(); $this->checkReadOnly(); @@ -158,6 +160,13 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $this->ids ); + # We need a target page! + if ( $this->targetObj === null ) { + $output->addWikiMsg( 'undelete-header' ); + + return; + } + $this->typeLabels = self::$UILabels[$this->typeName]; $list = $this->getList(); $list->reset(); @@ -168,12 +177,6 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $this->mIsAllowed = $this->mIsAllowed && !( $canViewSuppressedOnly && $pageIsSuppressed ); $this->otherReason = $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(); @@ -449,9 +452,8 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { Xml::closeElement( 'form' ) . "\n"; // Show link to edit the dropdown reasons if ( $this->getUser()->isAllowed( 'editinterface' ) ) { - $title = Title::makeTitle( NS_MEDIAWIKI, 'Revdelete-reason-dropdown' ); - $link = Linker::link( - $title, + $link = Linker::linkKnown( + $this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->getTitle(), $this->msg( 'revdelete-edit-reasonlist' )->escaped(), array(), array( 'action' => 'edit' ) @@ -585,7 +587,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { throw new PermissionsError( 'suppressrevision' ); } # If the save went through, go to success message... - $status = $this->save( $bitParams, $comment, $this->targetObj ); + $status = $this->save( $bitParams, $comment ); if ( $status->isGood() ) { $this->success(); @@ -605,7 +607,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { // Messages: revdelete-success, logdelete-success $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) ); $this->getOutput()->wrapWikiMsg( - "<span class=\"success\">\n$1\n</span>", + "<div class=\"successbox\">\n$1\n</div>", $this->typeLabels['success'] ); $this->wasSaved = true; @@ -620,7 +622,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { protected function failure( $status ) { // Messages: revdelete-failure, logdelete-failure $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) ); - $this->getOutput()->addWikiText( $status->getWikiText( $this->typeLabels['failure'] ) ); + $this->getOutput()->addWikiText( '<div class="errorbox">' . + $status->getWikiText( $this->typeLabels['failure'] ) . + '</div>' + ); $this->showForm(); } @@ -648,14 +653,13 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { /** * Do the write operations. Simple wrapper for RevDel*List::setVisibility(). - * @param int $bitfield + * @param array $bitPars ExtractBitParams() bitfield array * @param string $reason - * @param Title $title * @return Status */ - protected function save( $bitfield, $reason, $title ) { + protected function save( array $bitPars, $reason ) { return $this->getList()->setVisibility( - array( 'value' => $bitfield, 'comment' => $reason ) + array( 'value' => $bitPars, 'comment' => $reason ) ); } diff --git a/includes/specials/SpecialRunJobs.php b/includes/specials/SpecialRunJobs.php index 8cf93670..286a7456 100644 --- a/includes/specials/SpecialRunJobs.php +++ b/includes/specials/SpecialRunJobs.php @@ -38,14 +38,14 @@ class SpecialRunJobs extends UnlistedSpecialPage { $this->getOutput()->disable(); if ( wfReadOnly() ) { - header( "HTTP/1.0 423 Locked" ); + // HTTP 423 Locked + HttpStatus::header( 423 ); print 'Wiki is in read-only mode'; return; } elseif ( !$this->getRequest()->wasPosted() ) { - header( "HTTP/1.0 400 Bad Request" ); + HttpStatus::header( 400 ); print 'Request must be POSTed'; - return; } @@ -55,9 +55,8 @@ class SpecialRunJobs extends UnlistedSpecialPage { $params = array_intersect_key( $this->getRequest()->getValues(), $required + $optional ); $missing = array_diff_key( $required, $params ); if ( count( $missing ) ) { - header( "HTTP/1.0 400 Bad Request" ); + HttpStatus::header( 400 ); print 'Missing parameters: ' . implode( ', ', array_keys( $missing ) ); - return; } @@ -69,9 +68,8 @@ class SpecialRunJobs extends UnlistedSpecialPage { $verified = is_string( $providedSignature ) && hash_equals( $correctSignature, $providedSignature ); if ( !$verified || $params['sigexpiry'] < time() ) { - header( "HTTP/1.0 400 Bad Request" ); + HttpStatus::header( 400 ); print 'Invalid or stale signature provided'; - return; } @@ -83,7 +81,8 @@ class SpecialRunJobs extends UnlistedSpecialPage { // but it needs to know when it is safe to disconnect. Until this // reaches ignore_user_abort(), it is not safe as the jobs won't run. ignore_user_abort( true ); // jobs may take a bit of time - header( "HTTP/1.0 202 Accepted" ); + // HTTP 202 Accepted + HttpStatus::header( 202 ); ob_flush(); flush(); // Once the client receives this response, it can disconnect diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 608d62e6..8c546edd 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -47,7 +47,10 @@ class SpecialSearch extends SpecialPage { /** @var array For links */ protected $extraParams = array(); - /** @var string No idea, apparently used by some other classes */ + /** + * @var string The prefix url parameter. Set on the searcher and the + * is expected to treat it as prefix filter on titles. + */ protected $mPrefix; /** @@ -65,6 +68,11 @@ class SpecialSearch extends SpecialPage { */ protected $fulltext; + /** + * @var bool + */ + protected $runSuggestion = true; + const NAMESPACES_CURRENT = 'sense'; public function __construct() { @@ -166,6 +174,7 @@ class SpecialSearch extends SpecialPage { } $this->fulltext = $request->getVal( 'fulltext' ); + $this->runSuggestion = (bool)$request->getVal( 'runsuggestion', true ); $this->profile = $profile; } @@ -207,11 +216,11 @@ class SpecialSearch extends SpecialPage { global $wgContLang; $search = $this->getSearchEngine(); + $search->setFeatureData( 'rewrite', $this->runSuggestion ); $search->setLimitOffset( $this->limit, $this->offset ); $search->setNamespaces( $this->namespaces ); $search->prefix = $this->mPrefix; $term = $search->transformSearchTerm( $term ); - $didYouMeanHtml = ''; Hooks::run( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) ); @@ -262,37 +271,13 @@ class SpecialSearch extends SpecialPage { } // did you mean... suggestions - if ( $showSuggestion && $textMatches && !$textStatus && $textMatches->hasSuggestion() ) { - # mirror Go/Search behavior of original request .. - $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() ); - - if ( $this->fulltext != null ) { - $didYouMeanParams['fulltext'] = $this->fulltext; - } - - $stParams = array_merge( - $didYouMeanParams, - $this->powerSearchOptions() - ); - - $suggestionSnippet = $textMatches->getSuggestionSnippet(); - - if ( $suggestionSnippet == '' ) { - $suggestionSnippet = null; + $didYouMeanHtml = ''; + if ( $showSuggestion && $textMatches && !$textStatus ) { + if ( $textMatches->hasRewrittenQuery() ) { + $didYouMeanHtml = $this->getDidYouMeanRewrittenHtml( $term, $textMatches ); + } elseif ( $textMatches->hasSuggestion() ) { + $didYouMeanHtml = $this->getDidYouMeanHtml( $textMatches ); } - - $suggestLink = Linker::linkKnown( - $this->getPageTitle(), - $suggestionSnippet, - array(), - $stParams - ); - - # 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 ( !Hooks::run( 'SpecialSearchResultsPrepend', array( $this, $out, $term ) ) ) { @@ -381,14 +366,14 @@ class SpecialSearch extends SpecialPage { $out->wrapWikiMsg( "==$1==\n", 'textmatches' ); } - // show interwiki results if any - if ( $textMatches->hasInterwikiResults() ) { - $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) ); - } // show results if ( $numTextMatches > 0 ) { $out->addHTML( $this->showMatches( $textMatches ) ); } + // show interwiki results if any + if ( $textMatches->hasInterwikiResults() ) { + $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) ); + } $textMatches->free(); } @@ -402,11 +387,104 @@ class SpecialSearch extends SpecialPage { $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); } } - $out->addHtml( "</div>" ); + $out->addHTML( '<div class="visualClear"></div>' ); if ( $prevnext ) { $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); } + + $out->addHtml( "</div>" ); + + Hooks::run( 'SpecialSearchResultsAppend', array( $this, $out ) ); + + } + + /** + * Decide if the suggested query should be run, and it's results returned + * instead of the provided $textMatches + * + * @param SearchResultSet $textMatches The results of a users query + * @return bool + */ + protected function shouldRunSuggestedQuery( SearchResultSet $textMatches ) { + if ( !$this->runSuggestion || + !$textMatches->hasSuggestion() || + $textMatches->numRows() > 0 || + $textMatches->searchContainedSyntax() + ) { + return false; + } + + return $this->getConfig()->get( 'SearchRunSuggestedQuery' ); + } + + /** + * Generates HTML shown to the user when we have a suggestion about a query + * that might give more results than their current query. + */ + protected function getDidYouMeanHtml( SearchResultSet $textMatches ) { + # mirror Go/Search behavior of original request .. + $params = array( 'search' => $textMatches->getSuggestionQuery() ); + if ( $this->fulltext != null ) { + $params['fulltext'] = $this->fulltext; + } + $stParams = array_merge( $params, $this->powerSearchOptions() ); + + $suggest = Linker::linkKnown( + $this->getPageTitle(), + $textMatches->getSuggestionSnippet() ?: null, + array(), + $stParams + ); + + # html of did you mean... search suggestion link + return Html::rawElement( + 'div', + array( 'class' => 'searchdidyoumean' ), + $this->msg( 'search-suggest' )->rawParams( $suggest )->parse() + ); + } + + /** + * Generates HTML shown to user when their query has been internally rewritten, + * and the results of the rewritten query are being returned. + * + * @param string $term The users search input + * @param SearchResultSet $textMatches The response to the users initial search request + * @return string HTML linking the user to their original $term query, and the one + * suggested by $textMatches. + */ + protected function getDidYouMeanRewrittenHtml( $term, SearchResultSet $textMatches ) { + // Showing results for '$rewritten' + // Search instead for '$orig' + + $params = array( 'search' => $textMatches->getQueryAfterRewrite() ); + if ( $this->fulltext != null ) { + $params['fulltext'] = $this->fulltext; + } + $stParams = array_merge( $params, $this->powerSearchOptions() ); + + $rewritten = Linker::linkKnown( + $this->getPageTitle(), + $textMatches->getQueryAfterRewriteSnippet() ?: null, + array(), + $stParams + ); + + $stParams['search'] = $term; + $stParams['runsuggestion'] = 0; + $original = Linker::linkKnown( + $this->getPageTitle(), + htmlspecialchars( $term ), + array(), + $stParams + ); + + return Html::rawElement( + 'div', + array( 'class' => 'searchdidyoumean' ), + $this->msg( 'search-rewritten' )->rawParams( $rewritten, $original )->escaped() + ); } /** diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php index 7ec69e06..ba11862e 100644 --- a/includes/specials/SpecialShortpages.php +++ b/includes/specials/SpecialShortpages.php @@ -37,7 +37,7 @@ class ShortPagesPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'page' ), 'fields' => array( diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php index eaa90072..cf3804e5 100644 --- a/includes/specials/SpecialSpecialpages.php +++ b/includes/specials/SpecialSpecialpages.php @@ -96,22 +96,14 @@ class SpecialSpecialpages extends UnlistedSpecialPage { $includesCachedPages = false; foreach ( $groups as $group => $sortedPages ) { - $total = count( $sortedPages ); - $middle = ceil( $total / 2 ); - $count = 0; $out->wrapWikiMsg( "<h2 class=\"mw-specialpagesgroup\" id=\"mw-specialpagesgroup-$group\">$1</h2>\n", "specialpages-group-$group" ); $out->addHTML( - Html::openElement( - 'table', - array( 'style' => 'width:100%;', 'class' => 'mw-specialpages-table' ) - ) . "\n" . - Html::openElement( 'tr' ) . "\n" . - Html::openElement( 'td', array( 'style' => 'width:30%;vertical-align:top' ) ) . "\n" . - Html::openElement( 'ul' ) . "\n" + Html::openElement( 'div', array( 'class' => 'mw-specialpages-list' ) ) + . '<ul>' ); foreach ( $sortedPages as $desc => $specialpage ) { list( $title, $restricted, $cached ) = $specialpage; @@ -132,21 +124,10 @@ class SpecialSpecialpages extends UnlistedSpecialPage { array( 'class' => implode( ' ', $pageClasses ) ), $link ) . "\n" ); - - # Split up the larger groups - $count++; - if ( $total > 3 && $count == $middle ) { - $out->addHTML( - Html::closeElement( 'ul' ) . Html::closeElement( 'td' ) . - Html::element( 'td', array( 'style' => 'width:10%' ), '' ) . - Html::openElement( 'td', array( 'style' => 'width:30%' ) ) . Html::openElement( 'ul' ) . "\n" - ); - } } $out->addHTML( - Html::closeElement( 'ul' ) . Html::closeElement( 'td' ) . - Html::element( 'td', array( 'style' => 'width:30%' ), '' ) . - Html::closeElement( 'tr' ) . Html::closeElement( 'table' ) . "\n" + Html::closeElement( 'ul' ) . + Html::closeElement( 'div' ) ); } diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php index c35de241..8de6f8b8 100644 --- a/includes/specials/SpecialStatistics.php +++ b/includes/specials/SpecialStatistics.php @@ -107,10 +107,11 @@ class SpecialStatistics extends SpecialPage { ) { if ( $descMsg ) { $msg = $this->msg( $descMsg, $descMsgParam ); - if ( $msg->exists() ) { - $descriptionText = $this->msg( 'parentheses' )->rawParams( $msg->parse() )->escaped(); - $text .= "<br />" . Xml::element( 'small', array( 'class' => 'mw-statistic-desc' ), - " $descriptionText" ); + if ( !$msg->isDisabled() ) { + $descriptionHtml = $this->msg( 'parentheses' )->rawParams( $msg->parse() ) + ->escaped(); + $text .= "<br />" . Html::rawElement( 'small', array( 'class' => 'mw-statistic-desc' ), + " $descriptionHtml" ); } } @@ -126,26 +127,36 @@ class SpecialStatistics extends SpecialPage { * @return string */ private function getPageStats() { - return Xml::openElement( 'tr' ) . - Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-pages' )->parse() ) . + $pageStatsHtml = Xml::openElement( 'tr' ) . + Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-pages' ) + ->parse() ) . Xml::closeElement( 'tr' ) . $this->formatRow( Linker::linkKnown( SpecialPage::getTitleFor( 'Allpages' ), $this->msg( 'statistics-articles' )->parse() ), $this->getLanguage()->formatNum( $this->good ), - array( 'class' => 'mw-statistics-articles' ) ) . + array( 'class' => 'mw-statistics-articles' ), + 'statistics-articles-desc' ) . $this->formatRow( $this->msg( 'statistics-pages' )->parse(), $this->getLanguage()->formatNum( $this->total ), array( 'class' => 'mw-statistics-pages' ), - 'statistics-pages-desc' ) . - $this->formatRow( Linker::linkKnown( SpecialPage::getTitleFor( 'MediaStatistics' ), - $this->msg( 'statistics-files' )->parse() ), - $this->getLanguage()->formatNum( $this->images ), - array( 'class' => 'mw-statistics-files' ) ); + 'statistics-pages-desc' ); + + // Show the image row only, when there are files or upload is possible + if ( $this->images !== 0 || $this->getConfig()->get( 'EnableUploads' ) ) { + $pageStatsHtml .= $this->formatRow( + Linker::linkKnown( SpecialPage::getTitleFor( 'MediaStatistics' ), + $this->msg( 'statistics-files' )->parse() ), + $this->getLanguage()->formatNum( $this->images ), + array( 'class' => 'mw-statistics-files' ) ); + } + + return $pageStatsHtml; } private function getEditStats() { return Xml::openElement( 'tr' ) . - Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-edits' )->parse() ) . + Xml::tags( 'th', array( 'colspan' => '2' ), + $this->msg( 'statistics-header-edits' )->parse() ) . Xml::closeElement( 'tr' ) . $this->formatRow( $this->msg( 'statistics-edits' )->parse(), $this->getLanguage()->formatNum( $this->edits ), @@ -160,7 +171,8 @@ class SpecialStatistics extends SpecialPage { private function getUserStats() { return Xml::openElement( 'tr' ) . - Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-header-users' )->parse() ) . + Xml::tags( 'th', array( 'colspan' => '2' ), + $this->msg( 'statistics-header-users' )->parse() ) . Xml::closeElement( 'tr' ) . $this->formatRow( $this->msg( 'statistics-users' )->parse(), $this->getLanguage()->formatNum( $this->users ), @@ -223,7 +235,8 @@ class SpecialStatistics extends SpecialPage { } $text .= $this->formatRow( $grouppage . ' ' . $grouplink, $this->getLanguage()->formatNum( $countUsers ), - array( 'class' => 'statistics-group-' . Sanitizer::escapeClass( $group ) . $classZero ) ); + array( 'class' => 'statistics-group-' . Sanitizer::escapeClass( $group ) . + $classZero ) ); } return $text; diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php index 0b8147e1..70eee9f0 100644 --- a/includes/specials/SpecialTags.php +++ b/includes/specials/SpecialTags.php @@ -27,14 +27,21 @@ * @ingroup SpecialPage */ class SpecialTags extends SpecialPage { + /** - * @var array List of defined tags + * @var array List of explicitly defined tags */ - public $definedTags; + protected $explicitlyDefinedTags; + /** - * @var array List of active tags + * @var array List of extension defined tags */ - public $activeTags; + protected $extensionDefinedTags; + + /** + * @var array List of extension activated tags + */ + protected $extensionActivatedTags; function __construct() { parent::__construct( 'Tags' ); @@ -69,9 +76,11 @@ class SpecialTags extends SpecialPage { $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' ); $user = $this->getUser(); + $userCanManage = $user->isAllowed( 'managechangetags' ); + $userCanEditInterface = $user->isAllowed( 'editinterface' ); // Show form to create a tag - if ( $user->isAllowed( 'managechangetags' ) ) { + if ( $userCanManage ) { $fields = array( 'Tag' => array( 'type' => 'text', @@ -108,40 +117,50 @@ class SpecialTags extends SpecialPage { } } - // 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' ); + // Used to get hitcounts for #doTagRow() + $tagStats = ChangeTags::tagUsageStatistics(); - // Write the headers - $tagUsageStatistics = ChangeTags::tagUsageStatistics(); + // Used in #doTagRow() + $this->explicitlyDefinedTags = array_fill_keys( + ChangeTags::listExplicitlyDefinedTags(), true ); + $this->extensionDefinedTags = array_fill_keys( + ChangeTags::listExtensionDefinedTags(), true ); + + // List all defined tags, even if they were never applied + $definedTags = array_keys( array_merge( + $this->explicitlyDefinedTags, $this->extensionDefinedTags ) ); // Show header only if there exists atleast one tag - if ( !$tagUsageStatistics ) { + if ( !$tagStats && !$definedTags ) { return; } + + // Write the headers $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() ) . - ( $showActions ? + ( $userCanManage ? Xml::tags( 'th', array( 'class' => 'unsortable' ), $this->msg( 'tags-actions-header' )->parse() ) : '' ) ); // Used in #doTagRow() - $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 ( $tagUsageStatistics as $tag => $hitcount ) { - $html .= $this->doTagRow( $tag, $hitcount, $showActions ); + // Insert tags that have been applied at least once + foreach ( $tagStats as $tag => $hitcount ) { + $html .= $this->doTagRow( $tag, $hitcount, $userCanManage, $userCanEditInterface ); + } + // Insert tags defined somewhere but never applied + foreach ( $definedTags as $tag ) { + if ( !isset( $tagStats[$tag] ) ) { + $html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanEditInterface ); + } } $out->addHTML( Xml::tags( @@ -151,16 +170,15 @@ class SpecialTags extends SpecialPage { ) ); } - function doTagRow( $tag, $hitcount, $showActions ) { - $user = $this->getUser(); + function doTagRow( $tag, $hitcount, $showActions, $showEditLinks ) { $newRow = ''; $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) ); $disp = ChangeTags::tagDescription( $tag ); - if ( $user->isAllowed( 'editinterface' ) ) { + if ( $showEditLinks ) { $disp .= ' '; $editLink = Linker::link( - Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), + $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(), $this->msg( 'tags-edit' )->escaped() ); $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped(); @@ -169,10 +187,10 @@ class SpecialTags extends SpecialPage { $msg = $this->msg( "tag-$tag-description" ); $desc = !$msg->exists() ? '' : $msg->parse(); - if ( $user->isAllowed( 'editinterface' ) ) { + if ( $showEditLinks ) { $desc .= ' '; $editDescLink = Linker::link( - Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), + $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(), $this->msg( 'tags-edit' )->escaped() ); $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped(); @@ -198,21 +216,24 @@ class SpecialTags extends SpecialPage { $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() ); $hitcountLabel = $this->msg( 'tags-hitcount' )->numParams( $hitcount )->escaped(); - $hitcountLink = Linker::link( - SpecialPage::getTitleFor( 'Recentchanges' ), - $hitcountLabel, - array(), - array( 'tagfilter' => $tag ) - ); + if ( $this->getConfig()->get( 'UseTagFilter' ) ) { + $hitcountLabel = Linker::link( + SpecialPage::getTitleFor( 'Recentchanges' ), + $hitcountLabel, + array(), + array( 'tagfilter' => $tag ) + ); + } // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters - $newRow .= Xml::tags( 'td', array( 'data-sort-value' => $hitcount ), $hitcountLink ); + $newRow .= Xml::tags( 'td', array( 'data-sort-value' => $hitcount ), $hitcountLabel ); // actions - $actionLinks = array(); - if ( $showActions ) { + if ( $showActions ) { // we've already checked that the user had the requisite userright + $actionLinks = array(); + // delete - if ( ChangeTags::canDeleteTag( $tag, $user )->isOK() ) { + if ( ChangeTags::canDeleteTag( $tag )->isOK() ) { $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'delete' ), $this->msg( 'tags-delete' )->escaped(), array(), @@ -220,7 +241,7 @@ class SpecialTags extends SpecialPage { } // activate - if ( ChangeTags::canActivateTag( $tag, $user )->isOK() ) { + if ( ChangeTags::canActivateTag( $tag )->isOK() ) { $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'activate' ), $this->msg( 'tags-activate' )->escaped(), array(), @@ -228,7 +249,7 @@ class SpecialTags extends SpecialPage { } // deactivate - if ( ChangeTags::canDeactivateTag( $tag, $user )->isOK() ) { + if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) { $actionLinks[] = Linker::linkKnown( $this->getPageTitle( 'deactivate' ), $this->msg( 'tags-deactivate' )->escaped(), array(), @@ -318,7 +339,7 @@ class SpecialTags extends SpecialPage { $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock(); $tagUsage = ChangeTags::tagUsageStatistics(); - if ( $tagUsage[$tag] > 0 ) { + if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) { $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag, $tagUsage[$tag] )->parseAsBlock(); } diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index f2362a18..a66a3d10 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -149,7 +149,8 @@ class PageArchive { $fields, $conds, $join_conds, - $options + $options, + '' ); return $dbr->select( $tables, @@ -756,8 +757,8 @@ class SpecialUndelete extends SpecialPage { * @param User $user * @return bool */ - private function isAllowed( $permission, User $user = null ) { - $user = $user ? : $this->getUser(); + protected function isAllowed( $permission, User $user = null ) { + $user = $user ?: $this->getUser(); if ( $this->mTargetObj !== null ) { return $this->mTargetObj->userCan( $permission, $user ); } else { @@ -770,6 +771,8 @@ class SpecialUndelete extends SpecialPage { } function execute( $par ) { + $this->useTransactionalTimeLimit(); + $user = $this->getUser(); $this->setHeaders(); @@ -998,7 +1001,7 @@ class SpecialUndelete extends SpecialPage { return; } - if ( $this->mPreview || !$isText ) { + if ( ( $this->mPreview || !$isText ) && $content ) { // NOTE: non-text content has no source view, so always use rendered preview // Hide [edit]s @@ -1206,7 +1209,7 @@ class SpecialUndelete extends SpecialPage { $repo->streamFile( $path ); } - private function showHistory() { + protected function showHistory() { $out = $this->getOutput(); if ( $this->mAllowed ) { $out->addModules( 'mediawiki.special.undelete' ); @@ -1377,7 +1380,7 @@ class SpecialUndelete extends SpecialPage { return true; } - private function formatRevisionRow( $row, $earliestLiveTime, $remaining ) { + protected function formatRevisionRow( $row, $earliestLiveTime, $remaining ) { $rev = Revision::newFromArchiveRow( $row, array( 'title' => $this->mTargetObj diff --git a/includes/specials/SpecialUnlockdb.php b/includes/specials/SpecialUnlockdb.php index a8b97d78..dc03a4a7 100644 --- a/includes/specials/SpecialUnlockdb.php +++ b/includes/specials/SpecialUnlockdb.php @@ -65,9 +65,9 @@ class SpecialUnlockdb extends FormSpecialPage { } $readOnlyFile = $this->getConfig()->get( 'ReadOnlyFile' ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $res = unlink( $readOnlyFile ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $res ) { return Status::newGood(); diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php index 713823bb..0d3216cd 100644 --- a/includes/specials/SpecialUnusedcategories.php +++ b/includes/specials/SpecialUnusedcategories.php @@ -29,7 +29,7 @@ class UnusedCategoriesPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -37,7 +37,7 @@ class UnusedCategoriesPage extends QueryPage { return $this->msg( 'unusedcategoriestext' )->parseAsBlock(); } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'page', 'categorylinks' ), 'fields' => array( diff --git a/includes/specials/SpecialUnusedtemplates.php b/includes/specials/SpecialUnusedtemplates.php index 0c2b8707..33444f63 100644 --- a/includes/specials/SpecialUnusedtemplates.php +++ b/includes/specials/SpecialUnusedtemplates.php @@ -34,7 +34,7 @@ class UnusedtemplatesPage extends QueryPage { parent::__construct( $name ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -46,7 +46,7 @@ class UnusedtemplatesPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'page', 'templatelinks' ), 'fields' => array( diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php index bb07c197..b3ca006c 100644 --- a/includes/specials/SpecialUnwatchedpages.php +++ b/includes/specials/SpecialUnwatchedpages.php @@ -35,7 +35,7 @@ class UnwatchedpagesPage extends QueryPage { parent::__construct( $name, 'unwatchedpages' ); } - function isExpensive() { + public function isExpensive() { return true; } @@ -43,7 +43,7 @@ class UnwatchedpagesPage extends QueryPage { return false; } - function getQueryInfo() { + public function getQueryInfo() { return array( 'tables' => array( 'page', 'watchlist' ), 'fields' => array( diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index 2e0699af..16f4d161 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -152,6 +152,8 @@ class SpecialUpload extends SpecialPage { * @throws UserBlockedError */ public function execute( $par ) { + $this->useTransactionalTimeLimit(); + $this->setHeaders(); $this->outputHeader(); @@ -357,7 +359,7 @@ class SpecialUpload extends SpecialPage { $sessionKey = $this->mUpload->stashSession(); $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" - . '<ul class="warning">'; + . '<div class="warningbox"><ul>'; foreach ( $warnings as $warning => $args ) { if ( $warning == 'badfilename' ) { $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText(); @@ -385,7 +387,7 @@ class SpecialUpload extends SpecialPage { } $warningHtml .= $msg; } - $warningHtml .= "</ul>\n"; + $warningHtml .= "</ul></div>\n"; $warningHtml .= $this->msg( 'uploadwarning-text' )->parseAsBlock(); $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true ); @@ -795,6 +797,10 @@ class UploadForm extends HTMLForm { protected $mMaxUploadSize = array(); public function __construct( array $options = array(), IContextSource $context = null ) { + if ( $context instanceof IContextSource ) { + $this->setContext( $context ); + } + $this->mWatch = !empty( $options['watch'] ); $this->mForReUpload = !empty( $options['forreupload'] ); $this->mSessionKey = isset( $options['sessionkey'] ) ? $options['sessionkey'] : ''; @@ -821,8 +827,8 @@ class UploadForm extends HTMLForm { # Add a link to edit MediaWik:Licenses if ( $this->getUser()->isAllowed( 'editinterface' ) ) { - $licensesLink = Linker::link( - Title::makeTitle( NS_MEDIAWIKI, 'Licenses' ), + $licensesLink = Linker::linkKnown( + $this->msg( 'licenses' )->inContentLanguage()->getTitle(), $this->msg( 'licenses-edit' )->escaped(), array(), array( 'action' => 'edit' ) diff --git a/includes/specials/SpecialUploadStash.php b/includes/specials/SpecialUploadStash.php index 12e103e7..dd905900 100644 --- a/includes/specials/SpecialUploadStash.php +++ b/includes/specials/SpecialUploadStash.php @@ -58,6 +58,8 @@ class SpecialUploadStash extends UnlistedSpecialPage { * @return bool Success */ public function execute( $subPage ) { + $this->useTransactionalTimeLimit(); + $this->stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() ); $this->checkPermissions(); @@ -226,7 +228,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { */ private function outputRemoteScaledThumb( $file, $params, $flags ) { // This option probably looks something like - // 'http://upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use + // '//upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use // trailing slash. $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' ); diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php index 10edbcfb..21f1194f 100644 --- a/includes/specials/SpecialUserlogin.php +++ b/includes/specials/SpecialUserlogin.php @@ -20,6 +20,7 @@ * @file * @ingroup SpecialPage */ +use MediaWiki\Logger\LoggerFactory; /** * Implements Special:UserLogin @@ -43,6 +44,24 @@ class LoginForm extends SpecialPage { const WRONG_TOKEN = 13; const USER_MIGRATED = 14; + public static $statusCodes = array( + self::SUCCESS => 'success', + self::NO_NAME => 'no_name', + self::ILLEGAL => 'illegal', + self::WRONG_PLUGIN_PASS => 'wrong_plugin_pass', + self::NOT_EXISTS => 'not_exists', + self::WRONG_PASS => 'wrong_pass', + self::EMPTY_PASS => 'empty_pass', + self::RESET_PASS => 'reset_pass', + self::ABORTED => 'aborted', + self::CREATE_BLOCKED => 'create_blocked', + self::THROTTLED => 'throttled', + self::USER_BLOCKED => 'user_blocked', + self::NEED_TOKEN => 'need_token', + self::WRONG_TOKEN => 'wrong_token', + self::USER_MIGRATED => 'user_migrated', + ); + /** * Valid error and warning messages * @@ -194,13 +213,13 @@ class LoginForm extends SpecialPage { && in_array( $entryError->getKey(), self::getValidErrorMessages() ) ) { $this->mEntryErrorType = 'error'; - $this->mEntryError = $entryError->rawParams( $loginreqlink )->escaped(); + $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse(); } elseif ( $entryWarning->exists() && in_array( $entryWarning->getKey(), self::getValidErrorMessages() ) ) { $this->mEntryErrorType = 'warning'; - $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->escaped(); + $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse(); } if ( $wgEnableEmail ) { @@ -338,6 +357,10 @@ class LoginForm extends SpecialPage { } $status = $this->addNewAccountInternal(); + LoggerFactory::getInstance( 'authmanager' )->info( 'Account creation attempt with mailed password', array( + 'event' => 'accountcreation', + 'status' => $status, + ) ); if ( !$status->isGood() ) { $error = $status->getMessage(); $this->mainLoginForm( $error->toString() ); @@ -375,6 +398,11 @@ class LoginForm extends SpecialPage { # Create the account and abort if there's a problem doing so $status = $this->addNewAccountInternal(); + LoggerFactory::getInstance( 'authmanager' )->info( 'Account creation attempt', array( + 'event' => 'accountcreation', + 'status' => $status, + ) ); + if ( !$status->isGood() ) { $error = $status->getMessage(); $this->mainLoginForm( $error->toString() ); @@ -453,8 +481,7 @@ class LoginForm extends SpecialPage { * @return Status */ public function addNewAccountInternal() { - global $wgAuth, $wgMemc, $wgAccountCreationThrottle, - $wgMinimalPasswordLength, $wgEmailConfirmToEdit; + global $wgAuth, $wgMemc, $wgAccountCreationThrottle, $wgEmailConfirmToEdit; // If the user passes an invalid domain, something is fishy if ( !$wgAuth->validDomain( $this->mDomain ) ) { @@ -530,9 +557,15 @@ class LoginForm extends SpecialPage { # Now create a dummy user ($u) and check if it is valid $u = User::newFromName( $this->mUsername, 'creatable' ); - if ( !is_object( $u ) ) { + if ( !$u ) { return Status::newFatal( 'noname' ); - } elseif ( 0 != $u->idForName() ) { + } + + # Make sure the user does not exist already + $lock = $wgMemc->getScopedLock( wfGlobalCacheKey( 'account', md5( $this->mUsername ) ) ); + if ( !$lock ) { + return Status::newFatal( 'usernameinprogress' ); + } elseif ( $u->idForName( User::READ_LOCKING ) ) { return Status::newFatal( 'userexists' ); } @@ -546,7 +579,7 @@ class LoginForm extends SpecialPage { } # check for password validity, return a fatal Status if invalid - $validity = $u->checkPasswordValidity( $this->mPassword ); + $validity = $u->checkPasswordValidity( $this->mPassword, 'create' ); if ( !$validity->isGood() ) { $validity->ok = false; // make sure this Status is fatal return $validity; @@ -641,7 +674,12 @@ class LoginForm extends SpecialPage { $u->setRealName( $this->mRealName ); $u->setToken(); + Hooks::run( 'LocalUserCreated', array( $u, $autocreate ) ); + $oldUser = $u; $wgAuth->initUser( $u, $autocreate ); + if ( $oldUser !== $u ) { + wfWarn( get_class( $wgAuth ) . '::initUser() replaced the user object' ); + } $u->saveSettings(); @@ -710,7 +748,11 @@ class LoginForm extends SpecialPage { } $u = User::newFromName( $this->mUsername ); + if ( $u === false ) { + return self::ILLEGAL; + } + $msg = null; // Give extensions a way to indicate the username has been updated, // rather than telling the user the account doesn't exist. if ( !Hooks::run( 'LoginUserMigrated', array( $u, &$msg ) ) ) { @@ -718,7 +760,7 @@ class LoginForm extends SpecialPage { return self::USER_MIGRATED; } - if ( !( $u instanceof User ) || !User::isUsableName( $u->getName() ) ) { + if ( !User::isUsableName( $u->getName() ) ) { return self::ILLEGAL; } @@ -736,7 +778,6 @@ class LoginForm extends SpecialPage { // Give general extensions, such as a captcha, a chance to abort logins $abort = self::ABORTED; - $msg = null; if ( !Hooks::run( 'AbortLogin', array( $u, $this->mPassword, &$abort, &$msg ) ) ) { $this->mAbortLoginErrorMsg = $msg; @@ -784,7 +825,12 @@ class LoginForm extends SpecialPage { $retval = self::RESET_PASS; $this->mAbortLoginErrorMsg = 'resetpass-expired'; } else { + Hooks::run( 'UserLoggedIn', array( $u ) ); + $oldUser = $u; $wgAuth->updateUser( $u ); + if ( $oldUser !== $u ) { + wfWarn( get_class( $wgAuth ) . '::updateUser() replaced the user object' ); + } $wgUser = $u; // This should set it for OutputPage and the Skin // which is needed or the personal links will be @@ -909,7 +955,8 @@ class LoginForm extends SpecialPage { global $wgMemc, $wgLang, $wgSecureLogin, $wgPasswordAttemptThrottle, $wgInvalidPasswordReset; - switch ( $this->authenticateUserData() ) { + $authRes = $this->authenticateUserData(); + switch ( $authRes ) { case self::SUCCESS: # We've verified now, update the real record $user = $this->getUser(); @@ -946,7 +993,10 @@ class LoginForm extends SpecialPage { } elseif ( $wgInvalidPasswordReset && !$user->isValidPassword( $this->mPassword ) ) { - $status = $user->checkPasswordValidity( $this->mPassword ); + $status = $user->checkPasswordValidity( + $this->mPassword, + 'login' + ); $this->resetLoginForm( $status->getMessage( 'resetpass-validity-soft' ) ); @@ -1029,6 +1079,12 @@ class LoginForm extends SpecialPage { default: throw new MWException( 'Unhandled case value' ); } + + LoggerFactory::getInstance( 'authmanager' )->info( 'Login attempt', array( + 'event' => 'login', + 'successful' => $authRes === self::SUCCESS, + 'status' => LoginForm::$statusCodes[$authRes], + ) ); } /** @@ -1280,8 +1336,9 @@ class LoginForm extends SpecialPage { function mainLoginForm( $msg, $msgtype = 'error' ) { global $wgEnableEmail, $wgEnableUserEmail; global $wgHiddenPrefs, $wgLoginLanguageSelector; - global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration; + global $wgAuth, $wgEmailConfirmToEdit; global $wgSecureLogin, $wgPasswordResetRoutes; + global $wgExtendedLoginCookieExpiration, $wgCookieExpiration; $titleObj = $this->getPageTitle(); $user = $this->getUser(); @@ -1320,9 +1377,6 @@ class LoginForm extends SpecialPage { 'mediawiki.ui.input', 'mediawiki.special.userlogin.common.styles' ) ); - $out->addModules( array( - 'mediawiki.special.userlogin.common.js' - ) ); if ( $this->mType == 'signup' ) { // XXX hack pending RL or JS parse() support for complex content messages @@ -1384,6 +1438,7 @@ class LoginForm extends SpecialPage { : is_array( $wgPasswordResetRoutes ) && in_array( true, array_values( $wgPasswordResetRoutes ) ); $template->set( 'header', '' ); + $template->set( 'formheader', '' ); $template->set( 'skin', $this->getSkin() ); $template->set( 'name', $this->mUsername ); $template->set( 'password', $this->mPassword ); @@ -1404,7 +1459,7 @@ class LoginForm extends SpecialPage { $template->set( 'emailothers', $wgEnableUserEmail ); $template->set( 'canreset', $wgAuth->allowPasswordChange() ); $template->set( 'resetlink', $resetLink ); - $template->set( 'canremember', ( $wgCookieExpiration > 0 ) ); + $template->set( 'canremember', $wgExtendedLoginCookieExpiration === null ? ( $wgCookieExpiration > 0 ) : ( $wgExtendedLoginCookieExpiration > 0 ) ); $template->set( 'usereason', $user->isLoggedIn() ); $template->set( 'remember', $this->mRemember ); $template->set( 'cansecurelogin', ( $wgSecureLogin === true ) ); @@ -1526,7 +1581,6 @@ class LoginForm extends SpecialPage { */ public static function getCreateaccountToken() { global $wgRequest; - return $wgRequest->getSessionData( 'wsCreateaccountToken' ); } @@ -1601,22 +1655,21 @@ class LoginForm extends SpecialPage { */ function makeLanguageSelector() { $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage(); - if ( !$msg->isBlank() ) { - $langs = explode( "\n", $msg->text() ); - $links = array(); - foreach ( $langs as $lang ) { - $lang = trim( $lang, '* ' ); - $parts = explode( '|', $lang ); - if ( count( $parts ) >= 2 ) { - $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) ); - } - } - - return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams( - $this->getLanguage()->pipeList( $links ) )->escaped() : ''; - } else { + if ( $msg->isBlank() ) { return ''; } + $langs = explode( "\n", $msg->text() ); + $links = array(); + foreach ( $langs as $lang ) { + $lang = trim( $lang, '* ' ); + $parts = explode( '|', $lang ); + if ( count( $parts ) >= 2 ) { + $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) ); + } + } + + return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams( + $this->getLanguage()->pipeList( $links ) )->escaped() : ''; } /** diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index 758e3c05..e91c0bde 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 ( $this->mTarget !== null && User::getCanonicalName( $this->mTarget ) === $user->getName() ) { $this->isself = true; } @@ -145,6 +145,7 @@ class UserrightsPage extends SpecialPage { if ( $request->wasPosted() && $request->getCheck( 'saveusergroups' ) && + $this->mTarget !== null && $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget ) ) { // save settings @@ -249,7 +250,7 @@ class UserrightsPage extends SpecialPage { if ( $remove ) { foreach ( $remove as $index => $group ) { if ( !$user->removeGroup( $group ) ) { - unset($remove[$index]); + unset( $remove[$index] ); } } $newGroups = array_diff( $newGroups, $remove ); @@ -257,7 +258,7 @@ class UserrightsPage extends SpecialPage { if ( $add ) { foreach ( $add as $index => $group ) { if ( !$user->addGroup( $group ) ) { - unset($add[$index]); + unset( $add[$index] ); } } $newGroups = array_merge( $newGroups, $add ); @@ -268,6 +269,7 @@ class UserrightsPage extends SpecialPage { $user->invalidateCache(); // update groups in external authentication database + Hooks::run( 'UserGroupsChanged', array( $user, $add, $remove, $this->getUser() ) ); $wgAuth->updateExternalDBGroups( $user, $add, $remove ); wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) . "\n" ); diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php index 9a1c5e5d..38baf5b9 100644 --- a/includes/specials/SpecialVersion.php +++ b/includes/specials/SpecialVersion.php @@ -92,7 +92,14 @@ class SpecialVersion extends SpecialPage { if ( $file ) { $wikiText = file_get_contents( $file ); if ( substr( $file, -4 ) === '.txt' ) { - $wikiText = Html::element( 'pre', array(), $wikiText ); + $wikiText = Html::element( + 'pre', + array( + 'lang' => 'en', + 'dir' => 'ltr', + ), + $wikiText + ); } } } @@ -109,7 +116,14 @@ class SpecialVersion extends SpecialPage { $file = $this->getExtLicenseFileName( dirname( $extNode['path'] ) ); if ( $file ) { $wikiText = file_get_contents( $file ); - $wikiText = "<pre>$wikiText</pre>"; + $wikiText = Html::element( + 'pre', + array( + 'lang' => 'en', + 'dir' => 'ltr', + ), + $wikiText + ); } } @@ -215,6 +229,10 @@ class SpecialVersion extends SpecialPage { } $software[$dbr->getSoftwareLink()] = $dbr->getServerInfo(); + if ( IcuCollation::getICUVersion() ) { + $software['[http://site.icu-project.org/ ICU]'] = IcuCollation::getICUVersion(); + } + // Allow a hook to add/remove items. Hooks::run( 'SoftwareInfo', array( &$software ) ); @@ -522,6 +540,9 @@ class SpecialVersion extends SpecialPage { $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::element( 'th', array(), $this->msg( 'version-libraries-license' )->text() ) + . Html::element( 'th', array(), $this->msg( 'version-libraries-description' )->text() ) + . Html::element( 'th', array(), $this->msg( 'version-libraries-authors' )->text() ) . Html::closeElement( 'tr' ); foreach ( $lock->getInstalledDependencies() as $name => $info ) { @@ -530,13 +551,32 @@ class SpecialVersion extends SpecialPage { // in their proper section continue; } + $authors = array_map( function( $arr ) { + // If a homepage is set, link to it + if ( isset( $arr['homepage'] ) ) { + return "[{$arr['homepage']} {$arr['name']}]"; + } + return $arr['name']; + }, $info['authors'] ); + $authors = $this->listAuthors( $authors, false, "$IP/vendor/$name" ); + + // We can safely assume that the libraries' names and descriptions + // are written in English and aren't going to be translated, + // so set appropriate lang and dir attributes $out .= Html::openElement( 'tr' ) . Html::rawElement( 'td', array(), - Linker::makeExternalLink( "https://packagist.org/packages/$name", $name ) + Linker::makeExternalLink( + "https://packagist.org/packages/$name", $name, + true, '', + array( 'class' => 'mw-version-library-name' ) + ) ) - . Html::element( 'td', array(), $info['version'] ) + . Html::element( 'td', array( 'dir' => 'auto' ), $info['version'] ) + . Html::element( 'td', array( 'dir' => 'auto' ), $this->listToText( $info['licenses'] ) ) + . Html::element( 'td', array( 'lang' => 'en', 'dir' => 'ltr' ), $info['description'] ) + . Html::rawElement( 'td', array(), $authors ) . Html::closeElement( 'tr' ); } $out .= Html::closeElement( 'table' ); @@ -557,7 +597,10 @@ class SpecialVersion extends SpecialPage { if ( count( $tags ) ) { $out = Html::rawElement( 'h2', - array( 'class' => 'mw-headline plainlinks' ), + array( + 'class' => 'mw-headline plainlinks', + 'id' => 'mw-version-parser-extensiontags', + ), Linker::makeExternalLink( '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions', $this->msg( 'version-parser-extensiontags' )->parse(), @@ -597,7 +640,10 @@ class SpecialVersion extends SpecialPage { if ( count( $fhooks ) ) { $out = Html::rawElement( 'h2', - array( 'class' => 'mw-headline plainlinks' ), + array( + 'class' => 'mw-headline plainlinks', + 'id' => 'mw-version-parser-function-hooks', + ), Linker::makeExternalLink( '//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions', $this->msg( 'version-parser-function-hooks' )->parse(), @@ -842,7 +888,7 @@ class SpecialVersion extends SpecialPage { // Finally! Create the table $html = Html::openElement( 'tr', array( 'class' => 'mw-version-ext', - 'id' => "mw-version-ext-{$extension['name']}" + 'id' => Sanitizer::escapeId( 'mw-version-ext-' . $extension['name'] ) ) ); @@ -959,7 +1005,8 @@ class SpecialVersion extends SpecialPage { * 'and others' will be added to the end of the credits. * * @param string|array $authors - * @param string $extName Name of the extension for link creation + * @param string|bool $extName Name of the extension for link creation, + * false if no links should be created * @param string $extDir Path to the extension root directory * * @return string HTML fragment @@ -972,7 +1019,7 @@ class SpecialVersion extends SpecialPage { if ( $item == '...' ) { $hasOthers = true; - if ( $this->getExtAuthorsFileName( $extDir ) ) { + if ( $extName && $this->getExtAuthorsFileName( $extDir ) ) { $text = Linker::link( $this->getPageTitle( "Credits/$extName" ), $this->msg( 'version-poweredby-others' )->escaped() @@ -991,7 +1038,7 @@ class SpecialVersion extends SpecialPage { } } - if ( !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) { + if ( $extName && !$hasOthers && $this->getExtAuthorsFileName( $extDir ) ) { $list[] = $text = Linker::link( $this->getPageTitle( "Credits/$extName" ), $this->msg( 'version-poweredby-others' )->escaped() @@ -1091,7 +1138,10 @@ class SpecialVersion extends SpecialPage { if ( is_array( $list ) && count( $list ) == 1 ) { $list = $list[0]; } - if ( is_object( $list ) ) { + if ( $list instanceof Closure ) { + // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$" + return 'Closure'; + } elseif ( is_object( $list ) ) { $class = wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped(); return $class; @@ -1146,9 +1196,9 @@ class SpecialVersion extends SpecialPage { } // SimpleXml whines about the xmlns... - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $xml = simplexml_load_file( $entries ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $xml ) { foreach ( $xml->entry as $entry ) { diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php index 8a1a6c6c..a718aa81 100644 --- a/includes/specials/SpecialWantedfiles.php +++ b/includes/specials/SpecialWantedfiles.php @@ -52,7 +52,7 @@ class WantedFilesPage extends WantedQueryPage { $noForeign = ''; if ( !$this->likelyToHaveFalsePositives() ) { // Additional messages for grep: - // wantedfiletext-cat-noforeign, wantedfiletext-nocat + // wantedfiletext-cat-noforeign, wantedfiletext-nocat-noforeign $noForeign = '-noforeign'; } diff --git a/includes/specials/SpecialWantedtemplates.php b/includes/specials/SpecialWantedtemplates.php index a4b9dd84..9e26f0fa 100644 --- a/includes/specials/SpecialWantedtemplates.php +++ b/includes/specials/SpecialWantedtemplates.php @@ -44,10 +44,7 @@ class WantedTemplatesPage extends WantedQueryPage { 'title' => 'tl_title', 'value' => 'COUNT(*)' ), - 'conds' => array( - 'page_title IS NULL', - 'tl_namespace' => NS_TEMPLATE - ), + 'conds' => array( 'page_title IS NULL' ), 'options' => array( 'GROUP BY' => array( 'tl_namespace', 'tl_title' ) ), 'join_conds' => array( 'page' => array( 'LEFT JOIN', array( 'page_namespace = tl_namespace', diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index df9d3639..20f57760 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -43,6 +43,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { $output = $this->getOutput(); $request = $this->getRequest(); + $this->addHelpLink( 'Help:Watching pages' ); $mode = SpecialEditWatchlist::getMode( $request, $subpage ); if ( $mode !== false ) { diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php index 0b3175a6..39980d29 100644 --- a/includes/specials/SpecialWhatlinkshere.php +++ b/includes/specials/SpecialWhatlinkshere.php @@ -46,6 +46,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $this->setHeaders(); $this->outputHeader(); + $this->addHelpLink( 'Help:What links here' ); $opts = new FormOptions(); @@ -154,7 +155,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { $conds['pagelinks'][] = 'rd_from is NOT NULL'; } - $queryFunc = function ( $dbr, $table, $fromCol ) use ( + $queryFunc = function ( IDatabase $dbr, $table, $fromCol ) use ( $conds, $target, $limit, $useLinkNamespaceDBFields ) { // Read an extra row as an at-end check @@ -169,11 +170,12 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { } // Inner LIMIT is 2X in case of stale backlinks with wrong namespaces $subQuery = $dbr->selectSqlText( - array( $table, 'page', 'redirect' ), + array( $table, 'redirect', 'page' ), array( $fromCol, 'rd_from' ), $conds[$table], __CLASS__ . '::showIndirectLinks', - array( 'ORDER BY' => $fromCol, 'LIMIT' => 2 * $queryLimit ), + // Force JOIN order per T106682 to avoid large filesorts + array( 'ORDER BY' => $fromCol, 'LIMIT' => 2 * $queryLimit, 'STRAIGHT_JOIN' ), array( 'page' => array( 'INNER JOIN', "$fromCol = page_id" ), 'redirect' => array( 'LEFT JOIN', $on ) @@ -266,6 +268,14 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { } $prevId = $from; + // use LinkBatch to make sure, that all required data (associated with Titles) + // is loaded in one query + $lb = new LinkBatch(); + foreach ( $rows as $row ) { + $lb->add( $row->page_namespace, $row->page_title ); + } + $lb->execute(); + if ( $level == 0 ) { if ( !$this->including() ) { $out->addHTML( $this->whatlinkshereForm() ); @@ -313,7 +323,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { static $msgcache = null; if ( $msgcache === null ) { static $msgs = array( 'isredirect', 'istemplate', 'semicolon-separator', - 'whatlinkshere-links', 'isimage' ); + 'whatlinkshere-links', 'isimage', 'editlink' ); $msgcache = array(); foreach ( $msgs as $msg ) { $msgcache[$msg] = $this->msg( $msg )->escaped(); @@ -354,7 +364,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { } # Space for utilities links, with a what-links-here link provided - $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] ); + $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'], $msgcache['editlink'] ); $wlh = Xml::wrapClass( $this->msg( 'parentheses' )->rawParams( $wlhLink )->escaped(), 'mw-whatlinkshere-tools' @@ -369,18 +379,39 @@ class SpecialWhatLinksHere extends IncludableSpecialPage { return Xml::closeElement( 'ul' ); } - protected function wlhLink( Title $target, $text ) { + protected function wlhLink( Title $target, $text, $editText ) { static $title = null; if ( $title === null ) { $title = $this->getPageTitle(); } - return Linker::linkKnown( - $title, - $text, - array(), - array( 'target' => $target->getPrefixedText() ) + // always show a "<- Links" link + $links = array( + 'links' => Linker::linkKnown( + $title, + $text, + array(), + array( 'target' => $target->getPrefixedText() ) + ), ); + + // if the page is editable, add an edit link + if ( + // check user permissions + $this->getUser()->isAllowed( 'edit' ) && + // check, if the content model is editable through action=edit + ContentHandler::getForTitle( $target )->supportsDirectEditing() + ) { + $links['edit'] = Linker::linkKnown( + $target, + $editText, + array(), + array( 'action' => 'edit' ) + ); + } + + // build the links html + return $this->getLanguage()->pipeList( $links ); } function makeSelfLink( $text, $query ) { |