From c1f9b1f7b1b77776192048005dcc66dcf3df2bfb Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 27 Dec 2014 15:41:37 +0100 Subject: Update to MediaWiki 1.24.1 --- includes/specials/SpecialActiveusers.php | 237 +++++-- includes/specials/SpecialAllMessages.php | 479 ++++++++++++++ includes/specials/SpecialAllPages.php | 384 +++++++++++ includes/specials/SpecialAllmessages.php | 444 ------------- includes/specials/SpecialAllpages.php | 573 ----------------- includes/specials/SpecialBlock.php | 141 +++-- includes/specials/SpecialBlockList.php | 65 +- includes/specials/SpecialBooksources.php | 30 +- includes/specials/SpecialBrokenRedirects.php | 4 +- includes/specials/SpecialCachedPage.php | 24 +- includes/specials/SpecialCategories.php | 72 ++- includes/specials/SpecialChangeEmail.php | 255 +++----- includes/specials/SpecialChangePassword.php | 385 +++++------ includes/specials/SpecialComparePages.php | 4 +- includes/specials/SpecialConfirmemail.php | 44 +- includes/specials/SpecialContributions.php | 254 +++++--- includes/specials/SpecialCreateAccount.php | 56 ++ includes/specials/SpecialDeletedContributions.php | 42 +- includes/specials/SpecialDiff.php | 61 ++ includes/specials/SpecialEditWatchlist.php | 222 ++++--- includes/specials/SpecialEmailuser.php | 60 +- includes/specials/SpecialExpandTemplates.php | 286 +++++++++ includes/specials/SpecialExport.php | 87 +-- includes/specials/SpecialFewestrevisions.php | 2 +- includes/specials/SpecialFileDuplicateSearch.php | 12 +- includes/specials/SpecialFilepath.php | 8 +- includes/specials/SpecialImport.php | 164 +++-- includes/specials/SpecialJavaScriptTest.php | 33 +- includes/specials/SpecialLinkSearch.php | 114 +++- includes/specials/SpecialListDuplicatedFiles.php | 113 ++++ includes/specials/SpecialListfiles.php | 223 ++++--- includes/specials/SpecialListgrouprights.php | 200 ++++-- includes/specials/SpecialListredirects.php | 11 +- includes/specials/SpecialListusers.php | 65 +- includes/specials/SpecialLockdb.php | 10 +- includes/specials/SpecialLog.php | 33 +- includes/specials/SpecialLonelypages.php | 52 +- includes/specials/SpecialMIMEsearch.php | 58 +- includes/specials/SpecialMediaStatistics.php | 325 ++++++++++ includes/specials/SpecialMergeHistory.php | 105 ++- includes/specials/SpecialMostinterwikis.php | 4 +- includes/specials/SpecialMostlinked.php | 6 +- includes/specials/SpecialMostlinkedcategories.php | 1 + includes/specials/SpecialMostlinkedtemplates.php | 11 +- includes/specials/SpecialMostrevisions.php | 1 + includes/specials/SpecialMovepage.php | 98 ++- includes/specials/SpecialMyLanguage.php | 93 +++ includes/specials/SpecialMyRedirectPages.php | 114 ++++ includes/specials/SpecialNewimages.php | 37 +- includes/specials/SpecialNewpages.php | 68 +- includes/specials/SpecialPageLanguage.php | 195 ++++++ includes/specials/SpecialPagesWithProp.php | 45 +- includes/specials/SpecialPasswordReset.php | 43 +- includes/specials/SpecialPermanentLink.php | 45 ++ includes/specials/SpecialPreferences.php | 16 +- includes/specials/SpecialPrefixindex.php | 46 +- includes/specials/SpecialProtectedpages.php | 406 ++++++++---- includes/specials/SpecialProtectedtitles.php | 17 +- includes/specials/SpecialRandomInCategory.php | 77 ++- includes/specials/SpecialRandompage.php | 9 +- includes/specials/SpecialRecentchanges.php | 562 ++++++---------- includes/specials/SpecialRecentchangeslinked.php | 61 +- includes/specials/SpecialRedirect.php | 72 ++- includes/specials/SpecialResetTokens.php | 12 +- includes/specials/SpecialRevisiondelete.php | 286 ++++++--- includes/specials/SpecialRunJobs.php | 112 ++++ includes/specials/SpecialSearch.php | 661 ++++++++++--------- includes/specials/SpecialShortpages.php | 21 +- includes/specials/SpecialSpecialpages.php | 27 +- includes/specials/SpecialStatistics.php | 168 ++--- includes/specials/SpecialTags.php | 10 +- includes/specials/SpecialTrackingCategories.php | 148 +++++ includes/specials/SpecialUnblock.php | 38 +- includes/specials/SpecialUncategorizedimages.php | 13 +- includes/specials/SpecialUncategorizedpages.php | 26 +- includes/specials/SpecialUndelete.php | 393 +++++++----- includes/specials/SpecialUnlockdb.php | 11 +- includes/specials/SpecialUnusedcategories.php | 27 +- includes/specials/SpecialUnusedimages.php | 25 +- includes/specials/SpecialUnusedtemplates.php | 18 +- includes/specials/SpecialUnwatchedpages.php | 28 +- includes/specials/SpecialUpload.php | 254 +++++--- includes/specials/SpecialUploadStash.php | 121 ++-- includes/specials/SpecialUserlogin.php | 455 +++++++++---- includes/specials/SpecialUserlogout.php | 1 - includes/specials/SpecialUserrights.php | 165 +++-- includes/specials/SpecialVersion.php | 738 +++++++++++++++++----- includes/specials/SpecialWantedcategories.php | 68 +- includes/specials/SpecialWantedfiles.php | 77 ++- includes/specials/SpecialWantedpages.php | 7 +- includes/specials/SpecialWantedtemplates.php | 22 +- includes/specials/SpecialWatchlist.php | 652 ++++++++++--------- includes/specials/SpecialWhatlinkshere.php | 216 ++++--- includes/specials/SpecialWithoutinterwiki.php | 34 +- 94 files changed, 8020 insertions(+), 4578 deletions(-) create mode 100644 includes/specials/SpecialAllMessages.php create mode 100644 includes/specials/SpecialAllPages.php delete mode 100644 includes/specials/SpecialAllmessages.php delete mode 100644 includes/specials/SpecialAllpages.php create mode 100644 includes/specials/SpecialCreateAccount.php create mode 100644 includes/specials/SpecialDiff.php create mode 100644 includes/specials/SpecialExpandTemplates.php create mode 100644 includes/specials/SpecialListDuplicatedFiles.php create mode 100644 includes/specials/SpecialMediaStatistics.php create mode 100644 includes/specials/SpecialMyLanguage.php create mode 100644 includes/specials/SpecialMyRedirectPages.php create mode 100644 includes/specials/SpecialPageLanguage.php create mode 100644 includes/specials/SpecialPermanentLink.php create mode 100644 includes/specials/SpecialRunJobs.php create mode 100644 includes/specials/SpecialTrackingCategories.php (limited to 'includes/specials') diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php index 705dab55..ce436525 100644 --- a/includes/specials/SpecialActiveusers.php +++ b/includes/specials/SpecialActiveusers.php @@ -31,33 +31,35 @@ * @ingroup SpecialPage */ class ActiveUsersPager extends UsersPager { - /** * @var FormOptions */ protected $opts; /** - * @var Array + * @var array */ protected $hideGroups = array(); /** - * @var Array + * @var array */ protected $hideRights = array(); /** - * @param $context IContextSource - * @param $group null Unused + * @var array + */ + private $blockStatusByUid; + + /** + * @param IContextSource $context + * @param null $group Unused * @param string $par Parameter passed to the page */ function __construct( IContextSource $context = null, $group = null, $par = null ) { - global $wgActiveUserDays; - parent::__construct( $context ); - $this->RCMaxAge = $wgActiveUserDays; + $this->RCMaxAge = $this->getConfig()->get( 'ActiveUserDays' ); $un = $this->getRequest()->getText( 'username', $par ); $this->requestedUser = ''; if ( $un != '' ) { @@ -87,39 +89,36 @@ class ActiveUsersPager extends UsersPager { } function getIndexField() { - return 'rc_user_text'; + return 'qcc_title'; } function getQueryInfo() { $dbr = $this->getDatabase(); - $conds = array( 'rc_user > 0' ); // Users - no anons - $conds[] = 'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ); - $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( - $dbr->timestamp( wfTimestamp( TS_UNIX ) - $this->RCMaxAge * 24 * 3600 ) ); - + $activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400; + $timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds ); + $conds = array( + 'qcc_type' => 'activeusers', + 'qcc_namespace' => NS_USER, + 'user_name = qcc_title', + 'rc_user_text = qcc_title', + 'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata. + 'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ), + 'rc_timestamp >= ' . $dbr->addQuotes( $timestamp ), + ); if ( $this->requestedUser != '' ) { - $conds[] = 'rc_user_text >= ' . $dbr->addQuotes( $this->requestedUser ); + $conds[] = 'qcc_title >= ' . $dbr->addQuotes( $this->requestedUser ); } - if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { $conds[] = 'NOT EXISTS (' . $dbr->selectSQLText( - 'ipblocks', '1', array( 'rc_user=ipb_user', 'ipb_deleted' => 1 ) + 'ipblocks', '1', array( 'ipb_user=user_id', 'ipb_deleted' => 1 ) ) . ')'; } return array( - 'tables' => array( 'recentchanges' ), - 'fields' => array( - 'user_name' => 'rc_user_text', // for Pager inheritance - 'rc_user_text', // for Pager - 'user_id' => 'MAX(rc_user)', // Postgres - 'recentedits' => 'COUNT(*)' - ), - 'options' => array( - 'GROUP BY' => array( 'rc_user_text' ), - 'USE INDEX' => array( 'recentchanges' => 'rc_user_text' ) - ), + 'tables' => array( 'querycachetwo', 'user', 'recentchanges' ), + 'fields' => array( 'user_name', 'user_id', 'recentedits' => 'COUNT(*)', 'qcc_title' ), + 'options' => array( 'GROUP BY' => array( 'qcc_title' ) ), 'conds' => $conds ); } @@ -196,25 +195,34 @@ class ActiveUsersPager extends UsersPager { } function getPageHeader() { - global $wgScript; - $self = $this->getTitle(); $limit = $this->mLimit ? Html::hidden( 'limit', $this->mLimit ) : ''; - $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); # Form tag + # Form tag + $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => wfScript() ) ); $out .= Xml::fieldset( $this->msg( 'activeusers' )->text() ) . "\n"; $out .= Html::hidden( 'title', $self->getPrefixedDBkey() ) . $limit . "\n"; + # Username field $out .= Xml::inputLabel( $this->msg( 'activeusers-from' )->text(), - 'username', 'offset', 20, $this->requestedUser, array( 'tabindex' => 1 ) ) . '
';# Username field + 'username', 'offset', 20, $this->requestedUser, array( 'tabindex' => 1 ) ) . '
'; $out .= Xml::checkLabel( $this->msg( 'activeusers-hidebots' )->text(), 'hidebots', 'hidebots', $this->opts->getValue( 'hidebots' ), array( 'tabindex' => 2 ) ); - $out .= Xml::checkLabel( $this->msg( 'activeusers-hidesysops' )->text(), - 'hidesysops', 'hidesysops', $this->opts->getValue( 'hidesysops' ), array( 'tabindex' => 3 ) ) . '
'; - - $out .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text(), array( 'tabindex' => 4 ) ) . "\n";# Submit button and form bottom + $out .= Xml::checkLabel( + $this->msg( 'activeusers-hidesysops' )->text(), + 'hidesysops', + 'hidesysops', + $this->opts->getValue( 'hidesysops' ), + array( 'tabindex' => 3 ) + ) . '
'; + + # Submit button and form bottom + $out .= Xml::submitButton( + $this->msg( 'allpagessubmit' )->text(), + array( 'tabindex' => 4 ) + ) . "\n"; $out .= Xml::closeElement( 'fieldset' ); $out .= Xml::closeElement( 'form' ); @@ -237,17 +245,23 @@ class SpecialActiveUsers extends SpecialPage { /** * Show the special page * - * @param $par Mixed: parameter passed to the page or null + * @param string $par Parameter passed to the page or null */ public function execute( $par ) { - global $wgActiveUserDays; + $days = $this->getConfig()->get( 'ActiveUserDays' ); $this->setHeaders(); $this->outputHeader(); $out = $this->getOutput(); $out->wrapWikiMsg( "
\n$1\n
", - array( 'activeusers-intro', $this->getLanguage()->formatNum( $wgActiveUserDays ) ) ); + array( 'activeusers-intro', $this->getLanguage()->formatNum( $days ) ) ); + + // Occasionally merge in new updates + $seconds = min( self::mergeActiveUsers( 600, $days ), $days * 86400 ); + // Mention the level of staleness + $out->addWikiMsg( 'cachedspecial-viewing-cached-ttl', + $this->getLanguage()->formatDuration( $seconds ) ); $up = new ActiveUsersPager( $this->getContext(), null, $par ); @@ -269,4 +283,149 @@ class SpecialActiveUsers extends SpecialPage { protected function getGroupName() { return 'users'; } + + /** + * @param int $period Seconds (do updates no more often than this) + * @param int $days How many days user must be idle before he is considered inactive + * @return int How many seconds old the cache is + */ + public static function mergeActiveUsers( $period, $days ) { + $dbr = wfGetDB( DB_SLAVE ); + $cTime = $dbr->selectField( 'querycache_info', + 'qci_timestamp', + array( 'qci_type' => 'activeusers' ) + ); + + if ( !wfReadOnly() ) { + if ( !$cTime || ( time() - wfTimestamp( TS_UNIX, $cTime ) ) > $period ) { + $dbw = wfGetDB( DB_MASTER ); + if ( $dbw->estimateRowCount( 'recentchanges' ) <= 10000 ) { + $window = $days * 86400; // small wiki + } else { + $window = $period * 2; + } + $cTime = self::doQueryCacheUpdate( $dbw, $days, $window ) ?: $cTime; + } + } + + return ( time() - + ( $cTime ? wfTimestamp( TS_UNIX, $cTime ) : $days * 86400 ) ); + } + + /** + * @param DatabaseBase $dbw Passed in from updateSpecialPages.php + * @return void + */ + public static function cacheUpdate( DatabaseBase $dbw ) { + global $wgActiveUserDays; + + self::doQueryCacheUpdate( $dbw, $wgActiveUserDays, $wgActiveUserDays * 86400 ); + } + + /** + * Update the query cache as needed + * + * @param DatabaseBase $dbw + * @param int $days How many days user must be idle before he is considered inactive + * @param int $window Maximum time range of new data to scan (in seconds) + * @return int|bool UNIX timestamp the cache is now up-to-date as of (false on error) + */ + protected static function doQueryCacheUpdate( DatabaseBase $dbw, $days, $window ) { + $lockKey = wfWikiID() . '-activeusers'; + if ( !$dbw->lock( $lockKey, __METHOD__, 1 ) ) { + return false; // exclusive update (avoids duplicate entries) + } + + $now = time(); + $cTime = $dbw->selectField( 'querycache_info', + 'qci_timestamp', + array( 'qci_type' => 'activeusers' ) + ); + $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1; + + // Pick the date range to fetch from. This is normally from the last + // update to till the present time, but has a limited window for sanity. + // If the window is limited, multiple runs are need to fully populate it. + $sTimestamp = max( $cTimeUnix, $now - $days * 86400 ); + $eTimestamp = min( $sTimestamp + $window, $now ); + + // Get all the users active since the last update + $res = $dbw->select( + array( 'recentchanges' ), + array( 'rc_user_text', 'lastedittime' => 'MAX(rc_timestamp)' ), + array( + 'rc_user > 0', // actual accounts + 'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata + 'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ), + 'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ), + 'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) ) + ), + __METHOD__, + array( + 'GROUP BY' => array( 'rc_user_text' ), + 'ORDER BY' => 'NULL' // avoid filesort + ) + ); + $names = array(); + foreach ( $res as $row ) { + $names[$row->rc_user_text] = $row->lastedittime; + } + + // Rotate out users that have not edited in too long (according to old data set) + $dbw->delete( 'querycachetwo', + array( + 'qcc_type' => 'activeusers', + 'qcc_value < ' . $dbw->addQuotes( $now - $days * 86400 ) // TS_UNIX + ), + __METHOD__ + ); + + // Find which of the recently active users are already accounted for + if ( count( $names ) ) { + $res = $dbw->select( 'querycachetwo', + array( 'user_name' => 'qcc_title' ), + array( + 'qcc_type' => 'activeusers', + 'qcc_namespace' => NS_USER, + 'qcc_title' => array_keys( $names ) ), + __METHOD__ + ); + foreach ( $res as $row ) { + unset( $names[$row->user_name] ); + } + } + + // Insert the users that need to be added to the list (which their last edit time + if ( count( $names ) ) { + $newRows = array(); + foreach ( $names as $name => $lastEditTime ) { + $newRows[] = array( + 'qcc_type' => 'activeusers', + 'qcc_namespace' => NS_USER, + 'qcc_title' => $name, + 'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ), + 'qcc_namespacetwo' => 0, // unused + 'qcc_titletwo' => '' // unused + ); + } + foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) { + $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ ); + if ( !$dbw->trxLevel() ) { + wfWaitForSlaves(); + } + } + } + + // Touch the data freshness timestamp + $dbw->replace( 'querycache_info', + array( 'qci_type' ), + array( 'qci_type' => 'activeusers', + 'qci_timestamp' => $dbw->timestamp( $eTimestamp ) ), // not always $now + __METHOD__ + ); + + $dbw->unlock( $lockKey, __METHOD__ ); + + return $eTimestamp; + } } diff --git a/includes/specials/SpecialAllMessages.php b/includes/specials/SpecialAllMessages.php new file mode 100644 index 00000000..96be4d03 --- /dev/null +++ b/includes/specials/SpecialAllMessages.php @@ -0,0 +1,479 @@ +getRequest(); + $out = $this->getOutput(); + + $this->setHeaders(); + + if ( !$this->getConfig()->get( 'UseDatabaseMessages' ) ) { + $out->addWikiMsg( 'allmessagesnotsupportedDB' ); + + return; + } + + $this->outputHeader( 'allmessagestext' ); + $out->addModuleStyles( 'mediawiki.special' ); + + $this->table = new AllmessagesTablePager( + $this, + array(), + wfGetLangObj( $request->getVal( 'lang', $par ) ) + ); + + $this->langcode = $this->table->lang->getCode(); + + $out->addHTML( $this->table->buildForm() ); + $out->addParserOutputContent( $this->table->getFullOutput() ); + } + + protected function getGroupName() { + return 'wiki'; + } +} + +/** + * Use TablePager for prettified output. We have to pretend that we're + * getting data from a table when in fact not all of it comes from the database. + */ +class AllMessagesTablePager extends TablePager { + protected $filter, $prefix, $langcode, $displayPrefix; + + public $mLimitsShown; + + /** + * @var Language + */ + public $lang; + + /** + * @var null|bool + */ + public $custom; + + function __construct( $page, $conds, $langObj = null ) { + parent::__construct( $page->getContext() ); + $this->mIndexField = 'am_title'; + $this->mPage = $page; + $this->mConds = $conds; + // FIXME: Why does this need to be set to DIR_DESCENDING to produce ascending ordering? + $this->mDefaultDirection = IndexPager::DIR_DESCENDING; + $this->mLimitsShown = array( 20, 50, 100, 250, 500, 5000 ); + + global $wgContLang; + + $this->talk = $this->msg( 'talkpagelinktext' )->escaped(); + + $this->lang = ( $langObj ? $langObj : $wgContLang ); + $this->langcode = $this->lang->getCode(); + $this->foreign = $this->langcode !== $wgContLang->getCode(); + + $request = $this->getRequest(); + + $this->filter = $request->getVal( 'filter', 'all' ); + if ( $this->filter === 'all' ) { + $this->custom = null; // So won't match in either case + } else { + $this->custom = ( $this->filter === 'unmodified' ); + } + + $prefix = $this->getLanguage()->ucfirst( $request->getVal( 'prefix', '' ) ); + $prefix = $prefix !== '' ? + Title::makeTitleSafe( NS_MEDIAWIKI, $request->getVal( 'prefix', null ) ) : + null; + + if ( $prefix !== null ) { + $this->displayPrefix = $prefix->getDBkey(); + $this->prefix = '/^' . preg_quote( $this->displayPrefix ) . '/i'; + } else { + $this->displayPrefix = false; + $this->prefix = false; + } + + // The suffix that may be needed for message names if we're in a + // different language (eg [[MediaWiki:Foo/fr]]: $suffix = '/fr' + if ( $this->foreign ) { + $this->suffix = '/' . $this->langcode; + } else { + $this->suffix = ''; + } + } + + function buildForm() { + $attrs = array( 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' ); + $msg = wfMessage( 'allmessages-language' ); + $langSelect = Xml::languageSelector( $this->langcode, false, null, $attrs, $msg ); + + $out = Xml::openElement( 'form', array( + 'method' => 'get', + 'action' => $this->getConfig()->get( 'Script' ), + 'id' => 'mw-allmessages-form' + ) ) . + Xml::fieldset( $this->msg( 'allmessages-filter-legend' )->text() ) . + Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . + Xml::openElement( 'table', array( 'class' => 'mw-allmessages-table' ) ) . "\n" . + ' + ' . + Xml::label( $this->msg( 'allmessages-prefix' )->text(), 'mw-allmessages-form-prefix' ) . + "\n + " . + Xml::input( + 'prefix', + 20, + str_replace( '_', ' ', $this->displayPrefix ), + array( 'id' => 'mw-allmessages-form-prefix' ) + ) . + "\n + + \n + " . + $this->msg( 'allmessages-filter' )->escaped() . + "\n + " . + Xml::radioLabel( $this->msg( 'allmessages-filter-unmodified' )->text(), + 'filter', + 'unmodified', + 'mw-allmessages-form-filter-unmodified', + ( $this->filter === 'unmodified' ) + ) . + Xml::radioLabel( $this->msg( 'allmessages-filter-all' )->text(), + 'filter', + 'all', + 'mw-allmessages-form-filter-all', + ( $this->filter === 'all' ) + ) . + Xml::radioLabel( $this->msg( 'allmessages-filter-modified' )->text(), + 'filter', + 'modified', + 'mw-allmessages-form-filter-modified', + ( $this->filter === 'modified' ) + ) . + "\n + + \n + " . $langSelect[0] . "\n + " . $langSelect[1] . "\n + " . + + ' + ' . + Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) . + ' + ' . + $this->getLimitSelect() . + ' + + + ' . + Xml::submitButton( $this->msg( 'allmessages-filter-submit' )->text() ) . + "\n + " . + + Xml::closeElement( 'table' ) . + $this->getHiddenFields( array( 'title', 'prefix', 'filter', 'lang', 'limit' ) ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ); + + return $out; + } + + function getAllMessages( $descending ) { + wfProfileIn( __METHOD__ ); + $messageNames = Language::getLocalisationCache()->getSubitemList( 'en', 'messages' ); + if ( $descending ) { + rsort( $messageNames ); + } else { + asort( $messageNames ); + } + + // Normalise message names so they look like page titles + $messageNames = array_map( array( $this->lang, 'ucfirst' ), $messageNames ); + + wfProfileOut( __METHOD__ ); + + return $messageNames; + } + + /** + * Determine which of the MediaWiki and MediaWiki_talk namespace pages exist. + * Returns array( 'pages' => ..., 'talks' => ... ), where the subarrays have + * an entry for each existing page, with the key being the message name and + * value arbitrary. + * + * @param array $messageNames + * @param string $langcode What language code + * @param bool $foreign Whether the $langcode is not the content language + * @return array A 'pages' and 'talks' array with the keys of existing pages + */ + public static function getCustomisedStatuses( $messageNames, $langcode = 'en', $foreign = false ) { + // FIXME: This function should be moved to Language:: or something. + wfProfileIn( __METHOD__ . '-db' ); + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'page', + array( 'page_namespace', 'page_title' ), + array( 'page_namespace' => array( NS_MEDIAWIKI, NS_MEDIAWIKI_TALK ) ), + __METHOD__, + array( 'USE INDEX' => 'name_title' ) + ); + $xNames = array_flip( $messageNames ); + + $pageFlags = $talkFlags = array(); + + foreach ( $res as $s ) { + $exists = false; + + if ( $foreign ) { + $titleParts = explode( '/', $s->page_title ); + if ( count( $titleParts ) === 2 && + $langcode === $titleParts[1] && + isset( $xNames[$titleParts[0]] ) + ) { + $exists = $titleParts[0]; + } + } elseif ( isset( $xNames[$s->page_title] ) ) { + $exists = $s->page_title; + } + + $title = Title::newFromRow( $s ); + if ( $exists && $title->inNamespace( NS_MEDIAWIKI ) ) { + $pageFlags[$exists] = true; + } elseif ( $exists && $title->inNamespace( NS_MEDIAWIKI_TALK ) ) { + $talkFlags[$exists] = true; + } + } + + wfProfileOut( __METHOD__ . '-db' ); + + return array( 'pages' => $pageFlags, 'talks' => $talkFlags ); + } + + /** + * This function normally does a database query to get the results; we need + * to make a pretend result using a FakeResultWrapper. + * @param string $offset + * @param int $limit + * @param bool $descending + * @return FakeResultWrapper + */ + function reallyDoQuery( $offset, $limit, $descending ) { + $result = new FakeResultWrapper( array() ); + + $messageNames = $this->getAllMessages( $descending ); + $statuses = self::getCustomisedStatuses( $messageNames, $this->langcode, $this->foreign ); + + $count = 0; + foreach ( $messageNames as $key ) { + $customised = isset( $statuses['pages'][$key] ); + if ( $customised !== $this->custom && + ( $descending && ( $key < $offset || !$offset ) || !$descending && $key > $offset ) && + ( ( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false ) + ) { + $actual = wfMessage( $key )->inLanguage( $this->langcode )->plain(); + $default = wfMessage( $key )->inLanguage( $this->langcode )->useDatabase( false )->plain(); + $result->result[] = array( + 'am_title' => $key, + 'am_actual' => $actual, + 'am_default' => $default, + 'am_customised' => $customised, + 'am_talk_exists' => isset( $statuses['talks'][$key] ) + ); + $count++; + } + + if ( $count === $limit ) { + break; + } + } + + return $result; + } + + function getStartBody() { + $tableClass = $this->getTableClass(); + return Xml::openElement( 'table', array( + 'class' => "mw-datatable $tableClass", + 'id' => 'mw-allmessagestable' + ) ) . + "\n" . + " + " . + $this->msg( 'allmessagesname' )->escaped() . " + + " . + $this->msg( 'allmessagesdefault' )->escaped() . + " + \n + + " . + $this->msg( 'allmessagescurrent' )->escaped() . + " + \n"; + } + + function formatValue( $field, $value ) { + switch ( $field ) { + case 'am_title' : + $title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix ); + $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix ); + $translation = Linker::makeExternalLink( + 'https://translatewiki.net/w/i.php?' . wfArrayToCgi( array( + 'title' => 'Special:SearchTranslations', + 'group' => 'mediawiki', + 'grouppath' => 'mediawiki', + 'query' => 'language:' . $this->getLanguage()->getCode() . '^25 ' . + 'messageid:"MediaWiki:' . $value . '"^10 "' . + $this->msg( $value )->inLanguage( 'en' )->plain() . '"' + ) ), + $this->msg( 'allmessages-filter-translate' )->text() + ); + + if ( $this->mCurrentRow->am_customised ) { + $title = Linker::linkKnown( $title, $this->getLanguage()->lcfirst( $value ) ); + } else { + $title = Linker::link( + $title, + $this->getLanguage()->lcfirst( $value ), + array(), + array(), + array( 'broken' ) + ); + } + if ( $this->mCurrentRow->am_talk_exists ) { + $talk = Linker::linkKnown( $talk, $this->talk ); + } else { + $talk = Linker::link( + $talk, + $this->talk, + array(), + array(), + array( 'broken' ) + ); + } + + return $title . ' ' + . $this->msg( 'parentheses' )->rawParams( $talk )->escaped() + . ' ' + . $this->msg( 'parentheses' )->rawParams( $translation )->escaped(); + + case 'am_default' : + case 'am_actual' : + return Sanitizer::escapeHtmlAllowEntities( $value, ENT_QUOTES ); + } + + return ''; + } + + function formatRow( $row ) { + // Do all the normal stuff + $s = parent::formatRow( $row ); + + // But if there's a customised message, add that too. + if ( $row->am_customised ) { + $s .= Xml::openElement( 'tr', $this->getRowAttrs( $row, true ) ); + $formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) ); + + if ( $formatted === '' ) { + $formatted = ' '; + } + + $s .= Xml::tags( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) + . "\n"; + } + + return $s; + } + + function getRowAttrs( $row, $isSecond = false ) { + $arr = array(); + + if ( $row->am_customised ) { + $arr['class'] = 'allmessages-customised'; + } + + if ( !$isSecond ) { + $arr['id'] = Sanitizer::escapeId( 'msg_' . $this->getLanguage()->lcfirst( $row->am_title ) ); + } + + return $arr; + } + + function getCellAttrs( $field, $value ) { + if ( $this->mCurrentRow->am_customised && $field === 'am_title' ) { + return array( 'rowspan' => '2', 'class' => $field ); + } elseif ( $field === 'am_title' ) { + return array( 'class' => $field ); + } else { + return array( 'lang' => $this->langcode, 'dir' => $this->lang->getDir(), 'class' => $field ); + } + } + + // This is not actually used, as getStartBody is overridden above + function getFieldNames() { + return array( + 'am_title' => $this->msg( 'allmessagesname' )->text(), + 'am_default' => $this->msg( 'allmessagesdefault' )->text() + ); + } + + function getTitle() { + return SpecialPage::getTitleFor( 'Allmessages', false ); + } + + function isFieldSortable( $x ) { + return false; + } + + function getDefaultSort() { + return ''; + } + + function getQueryInfo() { + return ''; + } +} diff --git a/includes/specials/SpecialAllPages.php b/includes/specials/SpecialAllPages.php new file mode 100644 index 00000000..08b8761a --- /dev/null +++ b/includes/specials/SpecialAllPages.php @@ -0,0 +1,384 @@ +getRequest(); + $out = $this->getOutput(); + + $this->setHeaders(); + $this->outputHeader(); + $out->allowClickjacking(); + + # GET values + $from = $request->getVal( 'from', null ); + $to = $request->getVal( 'to', null ); + $namespace = $request->getInt( 'namespace' ); + $hideredirects = $request->getBool( 'hideredirects', false ); + + $namespaces = $this->getContext()->getLanguage()->getNamespaces(); + + $out->setPageTitle( + ( $namespace > 0 && array_key_exists( $namespace, $namespaces ) ) ? + $this->msg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : + $this->msg( 'allarticles' ) + ); + $out->addModuleStyles( 'mediawiki.special' ); + + if ( $par !== null ) { + $this->showChunk( $namespace, $par, $to, $hideredirects ); + } elseif ( $from !== null && $to === null ) { + $this->showChunk( $namespace, $from, $to, $hideredirects ); + } else { + $this->showToplevel( $namespace, $from, $to, $hideredirects ); + } + } + + /** + * HTML for the top form + * + * @param int $namespace A namespace constant (default NS_MAIN). + * @param string $from DbKey we are starting listing at. + * @param string $to DbKey we are ending listing at. + * @param bool $hideredirects Dont show redirects (default false) + * @return string + */ + function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) { + $t = $this->getPageTitle(); + + $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); + $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getConfig()->get( 'Script' ) ) ); + $out .= Html::hidden( 'title', $t->getPrefixedText() ); + $out .= Xml::openElement( 'fieldset' ); + $out .= Xml::element( 'legend', null, $this->msg( 'allpages' )->text() ); + $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); + $out .= " + " . + Xml::label( $this->msg( 'allpagesfrom' )->text(), 'nsfrom' ) . + " + " . + Xml::input( 'from', 30, str_replace( '_', ' ', $from ), array( 'id' => 'nsfrom' ) ) . + " + + + " . + Xml::label( $this->msg( 'allpagesto' )->text(), 'nsto' ) . + " + " . + Xml::input( 'to', 30, str_replace( '_', ' ', $to ), array( 'id' => 'nsto' ) ) . + " + + + " . + Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) . + " + " . + Html::namespaceSelector( + array( 'selected' => $namespace ), + array( 'name' => 'namespace', 'id' => 'namespace' ) + ) . ' ' . + Xml::checkLabel( + $this->msg( 'allpages-hide-redirects' )->text(), + 'hideredirects', + 'hideredirects', + $hideredirects + ) . ' ' . + Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . + " +"; + $out .= Xml::closeElement( 'table' ); + $out .= Xml::closeElement( 'fieldset' ); + $out .= Xml::closeElement( 'form' ); + $out .= Xml::closeElement( 'div' ); + + return $out; + } + + /** + * @param int $namespace (default NS_MAIN) + * @param string $from List all pages from this name + * @param string $to List all pages to this name + * @param bool $hideredirects Dont show redirects (default false) + */ + function showToplevel( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) { + $from = Title::makeTitleSafe( $namespace, $from ); + $to = Title::makeTitleSafe( $namespace, $to ); + $from = ( $from && $from->isLocal() ) ? $from->getDBkey() : null; + $to = ( $to && $to->isLocal() ) ? $to->getDBkey() : null; + + $this->showChunk( $namespace, $from, $to, $hideredirects ); + } + + /** + * @param int $namespace Namespace (Default NS_MAIN) + * @param string $from List all pages from this name (default false) + * @param string $to List all pages to this name (default false) + * @param bool $hideredirects Dont show redirects (default false) + */ + function showChunk( $namespace = NS_MAIN, $from = false, $to = false, $hideredirects = false ) { + $output = $this->getOutput(); + + $fromList = $this->getNamespaceKeyAndText( $namespace, $from ); + $toList = $this->getNamespaceKeyAndText( $namespace, $to ); + $namespaces = $this->getContext()->getLanguage()->getNamespaces(); + $n = 0; + + if ( !$fromList || !$toList ) { + $out = $this->msg( 'allpagesbadtitle' )->parseAsBlock(); + } elseif ( !array_key_exists( $namespace, $namespaces ) ) { + // Show errormessage and reset to NS_MAIN + $out = $this->msg( 'allpages-bad-ns', $namespace )->parse(); + $namespace = NS_MAIN; + } else { + list( $namespace, $fromKey, $from ) = $fromList; + list( , $toKey, $to ) = $toList; + + $dbr = wfGetDB( DB_SLAVE ); + $conds = array( + 'page_namespace' => $namespace, + 'page_title >= ' . $dbr->addQuotes( $fromKey ) + ); + + if ( $hideredirects ) { + $conds['page_is_redirect'] = 0; + } + + if ( $toKey !== "" ) { + $conds[] = 'page_title <= ' . $dbr->addQuotes( $toKey ); + } + + $res = $dbr->select( 'page', + array( 'page_namespace', 'page_title', 'page_is_redirect', 'page_id' ), + $conds, + __METHOD__, + array( + 'ORDER BY' => 'page_title', + 'LIMIT' => $this->maxPerPage + 1, + 'USE INDEX' => 'name_title', + ) + ); + + if ( $res->numRows() > 0 ) { + $out = Xml::openElement( 'ul', array( 'class' => 'mw-allpages-chunk' ) ); + while ( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { + $t = Title::newFromRow( $s ); + if ( $t ) { + $out .= 'page_is_redirect ? ' class="allpagesredirect"' : '' ) . + '>' . + Linker::link( $t ) . + "\n"; + } else { + $out .= '
  • [[' . htmlspecialchars( $s->page_title ) . "]]
  • \n"; + } + $n++; + } + $out .= Xml::closeElement( 'ul' ); + } else { + $out = ''; + } + } + + if ( $this->including() ) { + $output->addHTML( $out ); + 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( + Linker::link( $self, $this->msg( 'allpages' )->escaped() ) + ); + $bottomLinks = array(); + + # Do we put a previous link ? + if ( $prevTitle && $pt = $prevTitle->getText() ) { + $query = array( 'from' => $prevTitle->getText() ); + + if ( $namespace ) { + $query['namespace'] = $namespace; + } + + if ( $hideredirects ) { + $query['hideredirects'] = $hideredirects; + } + + $prevLink = Linker::linkKnown( + $self, + $this->msg( 'prevpage', $pt )->escaped(), + array(), + $query + ); + $topLinks[] = $prevLink; + $bottomLinks[] = $prevLink; + } + + if ( $n == $this->maxPerPage && $s = $res->fetchObject() ) { + # $s is the first link of the next chunk + $t = Title::makeTitle( $namespace, $s->page_title ); + $query = array( 'from' => $t->getText() ); + + if ( $namespace ) { + $query['namespace'] = $namespace; + } + + if ( $hideredirects ) { + $query['hideredirects'] = $hideredirects; + } + + $nextLink = Linker::linkKnown( + $self, + $this->msg( 'nextpage', $t->getText() )->escaped(), + array(), + $query + ); + $topLinks[] = $nextLink; + $bottomLinks[] = $nextLink; + } + + $nsForm = $this->namespaceForm( $namespace, $from, $to, $hideredirects ); + $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ) . + ' + ' . + $nsForm . + ' + ' . + $this->getLanguage()->pipeList( $topLinks ) . + ''; + + $output->addHTML( $out2 . $out ); + + if ( count( $bottomLinks ) ) { + $output->addHTML( + Html::element( 'hr' ) . + Html::rawElement( 'div', array( 'class' => 'mw-allpages-nav' ), + $this->getLanguage()->pipeList( $bottomLinks ) + ) + ); + } + } + + /** + * @param int $ns The namespace of the article + * @param string $text The name of the article + * @return array( int namespace, string dbkey, string pagename ) or null on error + */ + protected function getNamespaceKeyAndText( $ns, $text ) { + if ( $text == '' ) { + # shortcut for common case + return array( $ns, '', '' ); + } + + $t = Title::makeTitleSafe( $ns, $text ); + if ( $t && $t->isLocal() ) { + return array( $t->getNamespace(), $t->getDBkey(), $t->getText() ); + } elseif ( $t ) { + return null; + } + + # try again, in case the problem was an empty pagename + $text = preg_replace( '/(#|$)/', 'X$1', $text ); + $t = Title::makeTitleSafe( $ns, $text ); + if ( $t && $t->isLocal() ) { + return array( $t->getNamespace(), '', '' ); + } else { + return null; + } + } + + protected function getGroupName() { + return 'pages'; + } +} diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php deleted file mode 100644 index 35d6a0c0..00000000 --- a/includes/specials/SpecialAllmessages.php +++ /dev/null @@ -1,444 +0,0 @@ -getRequest(); - $out = $this->getOutput(); - - $this->setHeaders(); - - global $wgUseDatabaseMessages; - if ( !$wgUseDatabaseMessages ) { - $out->addWikiMsg( 'allmessagesnotsupportedDB' ); - - return; - } else { - $this->outputHeader( 'allmessagestext' ); - } - - $out->addModuleStyles( 'mediawiki.special' ); - - $this->table = new AllmessagesTablePager( - $this, - array(), - wfGetLangObj( $request->getVal( 'lang', $par ) ) - ); - - $this->langcode = $this->table->lang->getCode(); - - $out->addHTML( $this->table->buildForm() . - $this->table->getNavigationBar() . - $this->table->getBody() . - $this->table->getNavigationBar() ); - } - - protected function getGroupName() { - return 'wiki'; - } -} - -/** - * Use TablePager for prettified output. We have to pretend that we're - * getting data from a table when in fact not all of it comes from the database. - */ -class AllmessagesTablePager extends TablePager { - protected $filter, $prefix, $langcode, $displayPrefix; - - public $mLimitsShown; - - /** - * @var Language - */ - public $lang; - - /** - * @var null|bool - */ - public $custom; - - function __construct( $page, $conds, $langObj = null ) { - parent::__construct( $page->getContext() ); - $this->mIndexField = 'am_title'; - $this->mPage = $page; - $this->mConds = $conds; - $this->mDefaultDirection = true; // always sort ascending - $this->mLimitsShown = array( 20, 50, 100, 250, 500, 5000 ); - - global $wgContLang; - - $this->talk = $this->msg( 'talkpagelinktext' )->escaped(); - - $this->lang = ( $langObj ? $langObj : $wgContLang ); - $this->langcode = $this->lang->getCode(); - $this->foreign = $this->langcode != $wgContLang->getCode(); - - $request = $this->getRequest(); - - $this->filter = $request->getVal( 'filter', 'all' ); - if ( $this->filter === 'all' ) { - $this->custom = null; // So won't match in either case - } else { - $this->custom = ( $this->filter == 'unmodified' ); - } - - $prefix = $this->getLanguage()->ucfirst( $request->getVal( 'prefix', '' ) ); - $prefix = $prefix != '' ? Title::makeTitleSafe( NS_MEDIAWIKI, $request->getVal( 'prefix', null ) ) : null; - if ( $prefix !== null ) { - $this->displayPrefix = $prefix->getDBkey(); - $this->prefix = '/^' . preg_quote( $this->displayPrefix ) . '/i'; - } else { - $this->displayPrefix = false; - $this->prefix = false; - } - - // The suffix that may be needed for message names if we're in a - // different language (eg [[MediaWiki:Foo/fr]]: $suffix = '/fr' - if ( $this->foreign ) { - $this->suffix = '/' . $this->langcode; - } else { - $this->suffix = ''; - } - } - - function buildForm() { - global $wgScript; - - $attrs = array( 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' ); - $msg = wfMessage( 'allmessages-language' ); - $langSelect = Xml::languageSelector( $this->langcode, false, null, $attrs, $msg ); - - $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-allmessages-form' ) ) . - Xml::fieldset( $this->msg( 'allmessages-filter-legend' )->text() ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - Xml::openElement( 'table', array( 'class' => 'mw-allmessages-table' ) ) . "\n" . - ' - ' . - Xml::label( $this->msg( 'allmessages-prefix' )->text(), 'mw-allmessages-form-prefix' ) . - "\n - " . - Xml::input( 'prefix', 20, str_replace( '_', ' ', $this->displayPrefix ), array( 'id' => 'mw-allmessages-form-prefix' ) ) . - "\n - - \n - " . - $this->msg( 'allmessages-filter' )->escaped() . - "\n - " . - Xml::radioLabel( $this->msg( 'allmessages-filter-unmodified' )->text(), - 'filter', - 'unmodified', - 'mw-allmessages-form-filter-unmodified', - ( $this->filter == 'unmodified' ) - ) . - Xml::radioLabel( $this->msg( 'allmessages-filter-all' )->text(), - 'filter', - 'all', - 'mw-allmessages-form-filter-all', - ( $this->filter == 'all' ) - ) . - Xml::radioLabel( $this->msg( 'allmessages-filter-modified' )->text(), - 'filter', - 'modified', - 'mw-allmessages-form-filter-modified', - ( $this->filter == 'modified' ) - ) . - "\n - - \n - " . $langSelect[0] . "\n - " . $langSelect[1] . "\n - " . - - ' - ' . - Xml::label( $this->msg( 'table_pager_limit_label' )->text(), 'mw-table_pager_limit_label' ) . - ' - ' . - $this->getLimitSelect() . - ' - - - ' . - Xml::submitButton( $this->msg( 'allmessages-filter-submit' )->text() ) . - "\n - " . - - Xml::closeElement( 'table' ) . - $this->getHiddenFields( array( 'title', 'prefix', 'filter', 'lang', 'limit' ) ) . - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ); - - return $out; - } - - function getAllMessages( $descending ) { - wfProfileIn( __METHOD__ ); - $messageNames = Language::getLocalisationCache()->getSubitemList( 'en', 'messages' ); - if ( $descending ) { - rsort( $messageNames ); - } else { - asort( $messageNames ); - } - - // Normalise message names so they look like page titles - $messageNames = array_map( array( $this->lang, 'ucfirst' ), $messageNames ); - - wfProfileOut( __METHOD__ ); - - return $messageNames; - } - - /** - * Determine which of the MediaWiki and MediaWiki_talk namespace pages exist. - * Returns array( 'pages' => ..., 'talks' => ... ), where the subarrays have - * an entry for each existing page, with the key being the message name and - * value arbitrary. - * - * @param array $messageNames - * @param string $langcode What language code - * @param bool $foreign Whether the $langcode is not the content language - * @return array: a 'pages' and 'talks' array with the keys of existing pages - */ - public static function getCustomisedStatuses( $messageNames, $langcode = 'en', $foreign = false ) { - // FIXME: This function should be moved to Language:: or something. - wfProfileIn( __METHOD__ . '-db' ); - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'page', - array( 'page_namespace', 'page_title' ), - array( 'page_namespace' => array( NS_MEDIAWIKI, NS_MEDIAWIKI_TALK ) ), - __METHOD__, - array( 'USE INDEX' => 'name_title' ) - ); - $xNames = array_flip( $messageNames ); - - $pageFlags = $talkFlags = array(); - - foreach ( $res as $s ) { - $exists = false; - if ( $foreign ) { - $title = explode( '/', $s->page_title ); - if ( count( $title ) === 2 && $langcode == $title[1] - && isset( $xNames[$title[0]] ) - ) { - $exists = $title[0]; - } - } elseif ( isset( $xNames[$s->page_title] ) ) { - $exists = $s->page_title; - } - if ( $exists && $s->page_namespace == NS_MEDIAWIKI ) { - $pageFlags[$exists] = true; - } elseif ( $exists && $s->page_namespace == NS_MEDIAWIKI_TALK ) { - $talkFlags[$exists] = true; - } - } - - wfProfileOut( __METHOD__ . '-db' ); - - return array( 'pages' => $pageFlags, 'talks' => $talkFlags ); - } - - /** - * This function normally does a database query to get the results; we need - * to make a pretend result using a FakeResultWrapper. - * @param string $offset - * @param int $limit - * @param bool $descending - * @return FakeResultWrapper - */ - function reallyDoQuery( $offset, $limit, $descending ) { - $result = new FakeResultWrapper( array() ); - - $messageNames = $this->getAllMessages( $descending ); - $statuses = self::getCustomisedStatuses( $messageNames, $this->langcode, $this->foreign ); - - $count = 0; - foreach ( $messageNames as $key ) { - $customised = isset( $statuses['pages'][$key] ); - if ( $customised !== $this->custom && - ( $descending && ( $key < $offset || !$offset ) || !$descending && $key > $offset ) && - ( ( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false ) - ) { - $actual = wfMessage( $key )->inLanguage( $this->langcode )->plain(); - $default = wfMessage( $key )->inLanguage( $this->langcode )->useDatabase( false )->plain(); - $result->result[] = array( - 'am_title' => $key, - 'am_actual' => $actual, - 'am_default' => $default, - 'am_customised' => $customised, - 'am_talk_exists' => isset( $statuses['talks'][$key] ) - ); - $count++; - } - - if ( $count == $limit ) { - break; - } - } - - return $result; - } - - function getStartBody() { - return Xml::openElement( 'table', array( 'class' => 'mw-datatable TablePager', 'id' => 'mw-allmessagestable' ) ) . "\n" . - " - " . - $this->msg( 'allmessagesname' )->escaped() . " - - " . - $this->msg( 'allmessagesdefault' )->escaped() . - " - \n - - " . - $this->msg( 'allmessagescurrent' )->escaped() . - " - \n"; - } - - function formatValue( $field, $value ) { - switch ( $field ) { - case 'am_title' : - $title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix ); - $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix ); - - if ( $this->mCurrentRow->am_customised ) { - $title = Linker::linkKnown( $title, $this->getLanguage()->lcfirst( $value ) ); - } else { - $title = Linker::link( - $title, - $this->getLanguage()->lcfirst( $value ), - array(), - array(), - array( 'broken' ) - ); - } - if ( $this->mCurrentRow->am_talk_exists ) { - $talk = Linker::linkKnown( $talk, $this->talk ); - } else { - $talk = Linker::link( - $talk, - $this->talk, - array(), - array(), - array( 'broken' ) - ); - } - - return $title . ' ' . $this->msg( 'parentheses' )->rawParams( $talk )->escaped(); - - case 'am_default' : - case 'am_actual' : - return Sanitizer::escapeHtmlAllowEntities( $value, ENT_QUOTES ); - } - return ''; - } - - function formatRow( $row ) { - // Do all the normal stuff - $s = parent::formatRow( $row ); - - // But if there's a customised message, add that too. - if ( $row->am_customised ) { - $s .= Xml::openElement( 'tr', $this->getRowAttrs( $row, true ) ); - $formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) ); - if ( $formatted == '' ) { - $formatted = ' '; - } - $s .= Xml::tags( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) - . "\n"; - } - - return $s; - } - - function getRowAttrs( $row, $isSecond = false ) { - $arr = array(); - if ( $row->am_customised ) { - $arr['class'] = 'allmessages-customised'; - } - if ( !$isSecond ) { - $arr['id'] = Sanitizer::escapeId( 'msg_' . $this->getLanguage()->lcfirst( $row->am_title ) ); - } - - return $arr; - } - - function getCellAttrs( $field, $value ) { - if ( $this->mCurrentRow->am_customised && $field == 'am_title' ) { - return array( 'rowspan' => '2', 'class' => $field ); - } elseif ( $field == 'am_title' ) { - return array( 'class' => $field ); - } else { - return array( 'lang' => $this->langcode, 'dir' => $this->lang->getDir(), 'class' => $field ); - } - } - - // This is not actually used, as getStartBody is overridden above - function getFieldNames() { - return array( - 'am_title' => $this->msg( 'allmessagesname' )->text(), - 'am_default' => $this->msg( 'allmessagesdefault' )->text() - ); - } - - function getTitle() { - return SpecialPage::getTitleFor( 'Allmessages', false ); - } - - function isFieldSortable( $x ) { - return false; - } - - function getDefaultSort() { - return ''; - } - - function getQueryInfo() { - return ''; - } -} diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php deleted file mode 100644 index a0820493..00000000 --- a/includes/specials/SpecialAllpages.php +++ /dev/null @@ -1,573 +0,0 @@ -getRequest(); - $out = $this->getOutput(); - - $this->setHeaders(); - $this->outputHeader(); - $out->allowClickjacking(); - - # GET values - $from = $request->getVal( 'from', null ); - $to = $request->getVal( 'to', null ); - $namespace = $request->getInt( 'namespace' ); - $hideredirects = $request->getBool( 'hideredirects', false ); - - $namespaces = $this->getContext()->getLanguage()->getNamespaces(); - - $out->setPageTitle( - ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) ) ? - $this->msg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : - $this->msg( 'allarticles' ) - ); - $out->addModuleStyles( 'mediawiki.special' ); - - if ( $par !== null ) { - $this->showChunk( $namespace, $par, $to, $hideredirects ); - } elseif ( $from !== null && $to === null ) { - $this->showChunk( $namespace, $from, $to, $hideredirects ); - } else { - $this->showToplevel( $namespace, $from, $to, $hideredirects ); - } - } - - /** - * HTML for the top form - * - * @param $namespace Integer: a namespace constant (default NS_MAIN). - * @param string $from dbKey we are starting listing at. - * @param string $to dbKey we are ending listing at. - * @param bool $hideredirects dont show redirects (default FALSE) - * @return string - */ - function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) { - global $wgScript; - $t = $this->getTitle(); - - $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); - $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); - $out .= Html::hidden( 'title', $t->getPrefixedText() ); - $out .= Xml::openElement( 'fieldset' ); - $out .= Xml::element( 'legend', null, $this->msg( 'allpages' )->text() ); - $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); - $out .= " - " . - Xml::label( $this->msg( 'allpagesfrom' )->text(), 'nsfrom' ) . - " - " . - Xml::input( 'from', 30, str_replace( '_', ' ', $from ), array( 'id' => 'nsfrom' ) ) . - " - - - " . - Xml::label( $this->msg( 'allpagesto' )->text(), 'nsto' ) . - " - " . - Xml::input( 'to', 30, str_replace( '_', ' ', $to ), array( 'id' => 'nsto' ) ) . - " - - - " . - Xml::label( $this->msg( 'namespace' )->text(), 'namespace' ) . - " - " . - Html::namespaceSelector( - array( 'selected' => $namespace ), - array( 'name' => 'namespace', 'id' => 'namespace' ) - ) . ' ' . - Xml::checkLabel( - $this->msg( 'allpages-hide-redirects' )->text(), - 'hideredirects', - 'hideredirects', - $hideredirects - ) . ' ' . - Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . - " -"; - $out .= Xml::closeElement( 'table' ); - $out .= Xml::closeElement( 'fieldset' ); - $out .= Xml::closeElement( 'form' ); - $out .= Xml::closeElement( 'div' ); - - return $out; - } - - /** - * @param $namespace Integer (default NS_MAIN) - * @param string $from list all pages from this name - * @param string $to list all pages to this name - * @param bool $hideredirects dont show redirects (default FALSE) - */ - function showToplevel( $namespace = NS_MAIN, $from = '', $to = '', $hideredirects = false ) { - $output = $this->getOutput(); - - # TODO: Either make this *much* faster or cache the title index points - # in the querycache table. - - $dbr = wfGetDB( DB_SLAVE ); - $out = ""; - $where = array( 'page_namespace' => $namespace ); - - if ( $hideredirects ) { - $where['page_is_redirect'] = 0; - } - - $from = Title::makeTitleSafe( $namespace, $from ); - $to = Title::makeTitleSafe( $namespace, $to ); - $from = ( $from && $from->isLocal() ) ? $from->getDBkey() : null; - $to = ( $to && $to->isLocal() ) ? $to->getDBkey() : null; - - if ( isset( $from ) ) { - $where[] = 'page_title >= ' . $dbr->addQuotes( $from ); - } - - if ( isset( $to ) ) { - $where[] = 'page_title <= ' . $dbr->addQuotes( $to ); - } - - global $wgMemc; - $key = wfMemcKey( 'allpages', 'ns', $namespace, sha1( $from ), sha1( $to ) ); - $lines = $wgMemc->get( $key ); - - $count = $dbr->estimateRowCount( 'page', '*', $where, __METHOD__ ); - $maxPerSubpage = intval( $count / $this->maxLineCount ); - $maxPerSubpage = max( $maxPerSubpage, $this->maxPerPage ); - - if ( !is_array( $lines ) ) { - $options = array( 'LIMIT' => 1 ); - $options['ORDER BY'] = 'page_title ASC'; - $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options ); - $lastTitle = $firstTitle; - # This array is going to hold the page_titles in order. - $lines = array( $firstTitle ); - # If we are going to show n rows, we need n+1 queries to find the relevant titles. - $done = false; - while ( !$done ) { - // Fetch the last title of this chunk and the first of the next - $chunk = ( $lastTitle === false ) - ? array() - : array( 'page_title >= ' . $dbr->addQuotes( $lastTitle ) ); - $res = $dbr->select( 'page', /* FROM */ - 'page_title', /* WHAT */ - array_merge( $where, $chunk ), - __METHOD__, - array( 'LIMIT' => 2, 'OFFSET' => $maxPerSubpage - 1, 'ORDER BY' => 'page_title ASC' ) - ); - - $s = $dbr->fetchObject( $res ); - if ( $s ) { - array_push( $lines, $s->page_title ); - } else { - // Final chunk, but ended prematurely. Go back and find the end. - $endTitle = $dbr->selectField( 'page', 'MAX(page_title)', - array_merge( $where, $chunk ), - __METHOD__ ); - array_push( $lines, $endTitle ); - $done = true; - } - - $s = $res->fetchObject(); - if ( $s ) { - array_push( $lines, $s->page_title ); - $lastTitle = $s->page_title; - } else { - // This was a final chunk and ended exactly at the limit. - // Rare but convenient! - $done = true; - } - $res->free(); - } - $wgMemc->add( $key, $lines, 3600 ); - } - - // If there are only two or less sections, don't even display them. - // Instead, display the first section directly. - if ( count( $lines ) <= 2 ) { - if ( !empty( $lines ) ) { - $this->showChunk( $namespace, $from, $to, $hideredirects ); - } else { - $output->addHTML( $this->namespaceForm( $namespace, $from, $to, $hideredirects ) ); - } - - return; - } - - # At this point, $lines should contain an even number of elements. - $out .= Xml::openElement( 'table', array( 'class' => 'allpageslist' ) ); - while ( count( $lines ) > 0 ) { - $inpoint = array_shift( $lines ); - $outpoint = array_shift( $lines ); - $out .= $this->showline( $inpoint, $outpoint, $namespace, $hideredirects ); - } - $out .= Xml::closeElement( 'table' ); - $nsForm = $this->namespaceForm( $namespace, $from, $to, $hideredirects ); - - # Is there more? - if ( $this->including() ) { - $out2 = ''; - } else { - if ( isset( $from ) || isset( $to ) ) { - $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ) . - ' - ' . - $nsForm . - ' - ' . - Linker::link( $this->getTitle(), $this->msg( 'allpages' )->escaped(), - array(), array(), 'known' ) . - " - " . - Xml::closeElement( 'table' ); - } else { - $out2 = $nsForm; - } - } - $output->addHTML( $out2 . $out ); - } - - /** - * Show a line of "ABC to DEF" ranges of articles - * - * @param string $inpoint lower limit of pagenames - * @param string $outpoint upper limit of pagenames - * @param $namespace Integer (Default NS_MAIN) - * @param bool $hideRedirects don't show redirects. Default: false - * @return string - */ - function showline( $inpoint, $outpoint, $namespace = NS_MAIN, $hideRedirects = false ) { - // Use content language since page titles are considered to use content language - global $wgContLang; - - $inpointf = str_replace( '_', ' ', $inpoint ); - $outpointf = str_replace( '_', ' ', $outpoint ); - - // Don't let the length runaway - $inpointf = $wgContLang->truncate( $inpointf, $this->maxPageLength ); - $outpointf = $wgContLang->truncate( $outpointf, $this->maxPageLength ); - - $queryParams = array( - 'from' => $inpoint, - 'to' => $outpoint, - ); - - if ( $namespace ) { - $queryParams['namespace'] = $namespace; - } - if ( $hideRedirects ) { - $queryParams['hideredirects'] = 1; - } - - $url = $this->getTitle()->getLocalURL( $queryParams ); - $inlink = Html::element( 'a', array( 'href' => $url ), $inpointf ); - $outlink = Html::element( 'a', array( 'href' => $url ), $outpointf ); - - $out = $this->msg( 'alphaindexline' )->rawParams( - "$inlink", - "$outlink" - )->escaped(); - - return '' . $out . ''; - } - - /** - * @param int $namespace Namespace (Default NS_MAIN) - * @param string $from list all pages from this name (default FALSE) - * @param string $to list all pages to this name (default FALSE) - * @param bool $hideredirects dont show redirects (default FALSE) - */ - function showChunk( $namespace = NS_MAIN, $from = false, $to = false, $hideredirects = false ) { - $output = $this->getOutput(); - - $fromList = $this->getNamespaceKeyAndText( $namespace, $from ); - $toList = $this->getNamespaceKeyAndText( $namespace, $to ); - $namespaces = $this->getContext()->getLanguage()->getNamespaces(); - $n = 0; - - if ( !$fromList || !$toList ) { - $out = $this->msg( 'allpagesbadtitle' )->parseAsBlock(); - } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) { - // Show errormessage and reset to NS_MAIN - $out = $this->msg( 'allpages-bad-ns', $namespace )->parse(); - $namespace = NS_MAIN; - } else { - list( $namespace, $fromKey, $from ) = $fromList; - list( , $toKey, $to ) = $toList; - - $dbr = wfGetDB( DB_SLAVE ); - $conds = array( - 'page_namespace' => $namespace, - 'page_title >= ' . $dbr->addQuotes( $fromKey ) - ); - - if ( $hideredirects ) { - $conds['page_is_redirect'] = 0; - } - - if ( $toKey !== "" ) { - $conds[] = 'page_title <= ' . $dbr->addQuotes( $toKey ); - } - - $res = $dbr->select( 'page', - array( 'page_namespace', 'page_title', 'page_is_redirect', 'page_id' ), - $conds, - __METHOD__, - array( - 'ORDER BY' => 'page_title', - 'LIMIT' => $this->maxPerPage + 1, - 'USE INDEX' => 'name_title', - ) - ); - - if ( $res->numRows() > 0 ) { - $out = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-chunk' ) ); - while ( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { - $t = Title::newFromRow( $s ); - if ( $t ) { - $link = ( $s->page_is_redirect ? '
    ' : '' ) . - Linker::link( $t ) . - ( $s->page_is_redirect ? '
    ' : '' ); - } else { - $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; - } - - if ( $n % 3 == 0 ) { - $out .= ''; - } - - $out .= "$link"; - $n++; - if ( $n % 3 == 0 ) { - $out .= "\n"; - } - } - - if ( ( $n % 3 ) != 0 ) { - $out .= "\n"; - } - $out .= Xml::closeElement( 'table' ); - } else { - $out = ''; - } - } - - if ( $this->including() ) { - $out2 = ''; - } else { - 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->getTitle(); - - $nsForm = $this->namespaceForm( $namespace, $from, $to, $hideredirects ); - $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ) . - ' - ' . - $nsForm . - ' - ' . - Linker::link( $self, $this->msg( 'allpages' )->escaped() ); - - # Do we put a previous link ? - if ( isset( $prevTitle ) && $pt = $prevTitle->getText() ) { - $query = array( 'from' => $prevTitle->getText() ); - - if ( $namespace ) { - $query['namespace'] = $namespace; - } - - if ( $hideredirects ) { - $query['hideredirects'] = $hideredirects; - } - - $prevLink = Linker::linkKnown( - $self, - $this->msg( 'prevpage', $pt )->escaped(), - array(), - $query - ); - $out2 = $this->getLanguage()->pipeList( array( $out2, $prevLink ) ); - } - - if ( $n == $this->maxPerPage && $s = $res->fetchObject() ) { - # $s is the first link of the next chunk - $t = Title::makeTitle( $namespace, $s->page_title ); - $query = array( 'from' => $t->getText() ); - - if ( $namespace ) { - $query['namespace'] = $namespace; - } - - if ( $hideredirects ) { - $query['hideredirects'] = $hideredirects; - } - - $nextLink = Linker::linkKnown( - $self, - $this->msg( 'nextpage', $t->getText() )->escaped(), - array(), - $query - ); - $out2 = $this->getLanguage()->pipeList( array( $out2, $nextLink ) ); - } - $out2 .= ""; - } - - $output->addHTML( $out2 . $out ); - - $links = array(); - if ( isset( $prevLink ) ) { - $links[] = $prevLink; - } - - if ( isset( $nextLink ) ) { - $links[] = $nextLink; - } - - if ( count( $links ) ) { - $output->addHTML( - Html::element( 'hr' ) . - Html::rawElement( 'div', array( 'class' => 'mw-allpages-nav' ), - $this->getLanguage()->pipeList( $links ) - ) - ); - } - } - - /** - * @param $ns Integer: the namespace of the article - * @param string $text the name of the article - * @return array( int namespace, string dbkey, string pagename ) or NULL on error - */ - protected function getNamespaceKeyAndText( $ns, $text ) { - if ( $text == '' ) { - # shortcut for common case - return array( $ns, '', '' ); - } - - $t = Title::makeTitleSafe( $ns, $text ); - if ( $t && $t->isLocal() ) { - return array( $t->getNamespace(), $t->getDBkey(), $t->getText() ); - } elseif ( $t ) { - return null; - } - - # try again, in case the problem was an empty pagename - $text = preg_replace( '/(#|$)/', 'X$1', $text ); - $t = Title::makeTitleSafe( $ns, $text ); - if ( $t && $t->isLocal() ) { - return array( $t->getNamespace(), '', '' ); - } else { - return null; - } - } - - protected function getGroupName() { - return 'pages'; - } -} diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index 3b73a374..3297c17a 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -28,27 +28,23 @@ * @ingroup SpecialPage */ class SpecialBlock extends FormSpecialPage { - /** The maximum number of edits a user can have and still be hidden - * TODO: config setting? */ - const HIDEUSER_CONTRIBLIMIT = 1000; - - /** @var User user to be blocked, as passed either by parameter (url?wpTarget=Foo) + /** @var User User to be blocked, as passed either by parameter (url?wpTarget=Foo) * or as subpage (Special:Block/Foo) */ protected $target; - /// @var Block::TYPE_ constant + /** @var int Block::TYPE_ constant */ protected $type; - /// @var User|String the previous block target + /** @var User|string The previous block target */ protected $previousTarget; - /// @var Bool whether the previous submission of the form asked for HideUser + /** @var bool Whether the previous submission of the form asked for HideUser */ protected $requestedHideUser; - /// @var Bool + /** @var bool */ protected $alreadyBlocked; - /// @var Array + /** @var array */ protected $preErrors = array(); public function __construct() { @@ -74,7 +70,7 @@ class SpecialBlock extends FormSpecialPage { /** * Handle some magic here * - * @param $par String + * @param string $par */ protected function setParameter( $par ) { # Extract variables from the request. Try not to get into a situation where we @@ -88,14 +84,15 @@ class SpecialBlock extends FormSpecialPage { $this->getSkin()->setRelevantUser( $this->target ); } - list( $this->previousTarget, /*...*/ ) = Block::parseTarget( $request->getVal( 'wpPreviousTarget' ) ); + list( $this->previousTarget, /*...*/ ) = + Block::parseTarget( $request->getVal( 'wpPreviousTarget' ) ); $this->requestedHideUser = $request->getBool( 'wpHideUser' ); } /** * Customizes the HTMLForm a bit * - * @param $form HTMLForm + * @param HTMLForm $form */ protected function alterForm( HTMLForm $form ) { $form->setWrapperLegendMsg( 'blockip-legend' ); @@ -120,7 +117,7 @@ class SpecialBlock extends FormSpecialPage { /** * Get the HTMLForm descriptor array for the block form - * @return Array + * @return array */ protected function getFormFields() { global $wgBlockAllowsUTEdit; @@ -132,8 +129,7 @@ class SpecialBlock extends FormSpecialPage { $a = array( 'Target' => array( 'type' => 'text', - 'label-message' => 'ipadressorusername', - 'tabindex' => '1', + 'label-message' => 'ipaddressorusername', 'id' => 'mw-bi-target', 'size' => '45', 'autofocus' => true, @@ -144,7 +140,6 @@ class SpecialBlock extends FormSpecialPage { 'type' => !count( $suggestedDurations ) ? 'text' : 'selectorother', 'label-message' => 'ipbexpiry', 'required' => true, - 'tabindex' => '2', 'options' => $suggestedDurations, 'other' => $this->msg( 'ipbother' )->text(), 'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(), @@ -221,6 +216,9 @@ class SpecialBlock extends FormSpecialPage { $this->maybeAlterFormDefaults( $a ); + // Allow extensions to add more fields + wfRunHooks( 'SpecialBlockModifyFormFields', array( $this, &$a ) ); + return $a; } @@ -228,7 +226,7 @@ class SpecialBlock extends FormSpecialPage { * If the user has already been blocked with similar settings, load that block * and change the defaults for the form fields to match the existing settings. * @param array $fields HTMLForm descriptor array - * @return Bool whether fields were altered (that is, whether the target is + * @return bool Whether fields were altered (that is, whether the target is * already blocked) */ protected function maybeAlterFormDefaults( &$fields ) { @@ -293,20 +291,20 @@ class SpecialBlock extends FormSpecialPage { if ( $this->requestedHideUser ) { $fields['Confirm']['type'] = 'check'; unset( $fields['Confirm']['default'] ); - $this->preErrors[] = 'ipb-confirmhideuser'; + $this->preErrors[] = array( 'ipb-confirmhideuser', 'ipb-confirmaction' ); } # Or if the user is trying to block themselves if ( (string)$this->target === $this->getUser()->getName() ) { $fields['Confirm']['type'] = 'check'; unset( $fields['Confirm']['default'] ); - $this->preErrors[] = 'ipb-blockingself'; + $this->preErrors[] = array( 'ipb-blockingself', 'ipb-confirmaction' ); } } /** * Add header elements like block log entries, etc. - * @return String + * @return string */ protected function preText() { $this->getOutput()->addModules( 'mediawiki.special.block' ); @@ -362,7 +360,10 @@ class SpecialBlock extends FormSpecialPage { # Link to unblock the specified user, or to a blank unblock form if ( $this->target instanceof User ) { - $message = $this->msg( 'ipb-unblock-addr', wfEscapeWikiText( $this->target->getName() ) )->parse(); + $message = $this->msg( + 'ipb-unblock-addr', + wfEscapeWikiText( $this->target->getName() ) + )->parse(); $list = SpecialPage::getTitleFor( 'Unblock', $this->target->getName() ); } else { $message = $this->msg( 'ipb-unblock' )->parse(); @@ -437,7 +438,7 @@ class SpecialBlock extends FormSpecialPage { /** * Get a user page target for things like logs. * This handles account and IP range targets. - * @param $target User|string + * @param User|string $target * @return Title|null */ protected static function getTargetUserTitle( $target ) { @@ -452,10 +453,10 @@ class SpecialBlock extends FormSpecialPage { /** * Determine the target of the block, and the type of target - * TODO: should be in Block.php? - * @param string $par subpage parameter passed to setup, or data value from + * @todo Should be in Block.php? + * @param string $par Subpage parameter passed to setup, or data value from * the HTMLForm - * @param $request WebRequest optionally try and get data from a request too + * @param WebRequest $request Optionally try and get data from a request too * @return array( User|string|null, Block::TYPE_ constant|null ) */ public static function getTargetAndType( $par, WebRequest $request = null ) { @@ -504,9 +505,9 @@ class SpecialBlock extends FormSpecialPage { /** * HTMLForm field validation-callback for Target field. * @since 1.18 - * @param $value String - * @param $alldata Array - * @param $form HTMLForm + * @param string $value + * @param array $alldata + * @param HTMLForm $form * @return Message */ public static function validateTargetField( $value, $alldata, $form ) { @@ -584,9 +585,9 @@ class SpecialBlock extends FormSpecialPage { /** * Submit callback for an HTMLForm object, will simply pass - * @param $data array - * @param $form HTMLForm - * @return Bool|String + * @param array $data + * @param HTMLForm $form + * @return bool|string */ public static function processUIForm( array $data, HTMLForm $form ) { return self::processForm( $data, $form->getContext() ); @@ -594,12 +595,12 @@ class SpecialBlock extends FormSpecialPage { /** * Given the form data, actually implement a block - * @param $data Array - * @param $context IContextSource - * @return Bool|String + * @param array $data + * @param IContextSource $context + * @return bool|string */ public static function processForm( array $data, IContextSource $context ) { - global $wgBlockAllowsUTEdit; + global $wgBlockAllowsUTEdit, $wgHideUserContribLimit, $wgContLang; $performer = $context->getUser(); @@ -627,7 +628,7 @@ class SpecialBlock extends FormSpecialPage { if ( $target === $performer->getName() && ( $data['PreviousTarget'] !== $target || !$data['Confirm'] ) ) { - return array( 'ipb-blockingself' ); + return array( 'ipb-blockingself', 'ipb-confirmaction' ); } } elseif ( $type == Block::TYPE_RANGE ) { $userId = 0; @@ -670,12 +671,15 @@ class SpecialBlock extends FormSpecialPage { } elseif ( !in_array( $data['Expiry'], array( 'infinite', 'infinity', 'indefinite' ) ) ) { # Bad expiry. return array( 'ipb_expiry_temp' ); - } elseif ( $user->getEditCount() > self::HIDEUSER_CONTRIBLIMIT ) { + } elseif ( $wgHideUserContribLimit !== false + && $user->getEditCount() > $wgHideUserContribLimit + ) { # Typically, the user should have a handful of edits. # Disallow hiding users with many edits for performance. - return array( 'ipb_hide_invalid' ); + return array( array( 'ipb_hide_invalid', + Message::numParam( $wgHideUserContribLimit ) ) ); } elseif ( !$data['Confirm'] ) { - return array( 'ipb-confirmhideuser' ); + return array( 'ipb-confirmhideuser', 'ipb-confirmaction' ); } } @@ -683,7 +687,8 @@ class SpecialBlock extends FormSpecialPage { $block = new Block(); $block->setTarget( $target ); $block->setBlocker( $performer ); - $block->mReason = $data['Reason'][0]; + # Truncate reason for whole multibyte characters + $block->mReason = $wgContLang->truncate( $data['Reason'][0], 255 ); $block->mExpiry = self::parseExpiryInput( $data['Expiry'] ); $block->prevents( 'createaccount', $data['CreateAccount'] ); $block->prevents( 'editownusertalk', ( !$wgBlockAllowsUTEdit || $data['DisableUTEdit'] ) ); @@ -692,8 +697,9 @@ class SpecialBlock extends FormSpecialPage { $block->isAutoblocking( $data['AutoBlock'] ); $block->mHideName = $data['HideUser']; - if ( !wfRunHooks( 'BlockIp', array( &$block, &$performer ) ) ) { - return array( 'hookaborted' ); + $reason = array( 'hookaborted' ); + if ( !wfRunHooks( 'BlockIp', array( &$block, &$performer, &$reason ) ) ) { + return $reason; } # Try to insert block. Is there a conflicting block? @@ -726,8 +732,17 @@ class SpecialBlock extends FormSpecialPage { return array( 'cant-see-hidden-user' ); } - $currentBlock->delete(); - $status = $block->insert(); + $currentBlock->isHardblock( $block->isHardblock() ); + $currentBlock->prevents( 'createaccount', $block->prevents( 'createaccount' ) ); + $currentBlock->mExpiry = $block->mExpiry; + $currentBlock->isAutoblocking( $block->isAutoblocking() ); + $currentBlock->mHideName = $block->mHideName; + $currentBlock->prevents( 'sendemail', $block->prevents( 'sendemail' ) ); + $currentBlock->prevents( 'editownusertalk', $block->prevents( 'editownusertalk' ) ); + $currentBlock->mReason = $block->mReason; + + $status = $currentBlock->update(); + $logaction = 'reblock'; # Unset _deleted fields if requested @@ -753,7 +768,11 @@ class SpecialBlock extends FormSpecialPage { # Can't watch a rangeblock if ( $type != Block::TYPE_RANGE && $data['Watch'] ) { - WatchAction::doWatch( Title::makeTitle( NS_USER, $target ), $performer, WatchedItem::IGNORE_USER_RIGHTS ); + WatchAction::doWatch( + Title::makeTitle( NS_USER, $target ), + $performer, + WatchedItem::IGNORE_USER_RIGHTS + ); } # Block constructor sanitizes certain block options on insert @@ -787,9 +806,9 @@ class SpecialBlock extends FormSpecialPage { * Get an array of suggested block durations from MediaWiki:Ipboptions * @todo FIXME: This uses a rather odd syntax for the options, should it be converted * to the standard "**|" format? - * @param $lang Language|null the language to get the durations in, or null to use + * @param Language|null $lang The language to get the durations in, or null to use * the wiki's content language - * @return Array + * @return array */ public static function getSuggestedDurations( $lang = null ) { $a = array(); @@ -816,8 +835,8 @@ 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 + * @param string $expiry Whatever was typed into the form + * @return string Timestamp or "infinity" string for the DB implementation */ public static function parseExpiryInput( $expiry ) { static $infinity; @@ -842,8 +861,8 @@ class SpecialBlock extends FormSpecialPage { /** * Can we do an email block? - * @param $user User: the sysop wanting to make a block - * @return Boolean + * @param User $user The sysop wanting to make a block + * @return bool */ public static function canBlockEmail( $user ) { global $wgEnableUserEmail, $wgSysopEmailBans; @@ -855,9 +874,9 @@ class SpecialBlock extends FormSpecialPage { * bug 15810: blocked admins should not be able to block/unblock * others, and probably shouldn't be able to unblock themselves * either. - * @param $user User|Int|String - * @param $performer User user doing the request - * @return Bool|String true or error message key + * @param User|int|string $user + * @param User $performer User doing the request + * @return bool|string True or error message key */ public static function checkUnblockSelf( $user, User $performer ) { if ( is_int( $user ) ) { @@ -889,8 +908,8 @@ class SpecialBlock extends FormSpecialPage { /** * Return a comma-delimited list of "flags" to be passed to the log * reader for this block, to provide more information in the logs - * @param array $data from HTMLForm data - * @param $type Block::TYPE_ constant (USER, RANGE, or IP) + * @param array $data From HTMLForm data + * @param int $type Block::TYPE_ constant (USER, RANGE, or IP) * @return string */ protected static function blockLogFlags( array $data, $type ) { @@ -935,8 +954,8 @@ class SpecialBlock extends FormSpecialPage { /** * Process the form on POST submission. - * @param $data Array - * @return Bool|Array true for success, false for didn't-try, array of errors on failure + * @param array $data + * @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 @@ -957,7 +976,3 @@ class SpecialBlock extends FormSpecialPage { return 'users'; } } - -# BC @since 1.18 -class IPBlockForm extends SpecialBlock { -} diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php index f1992c0f..456f4ecb 100644 --- a/includes/specials/SpecialBlockList.php +++ b/includes/specials/SpecialBlockList.php @@ -27,8 +27,9 @@ * @ingroup SpecialPage */ class SpecialBlockList extends SpecialPage { + protected $target; - protected $target, $options; + protected $options; function __construct() { parent::__construct( 'BlockList' ); @@ -37,7 +38,7 @@ class SpecialBlockList extends SpecialPage { /** * Main execution point * - * @param string $par title fragment + * @param string $par Title fragment */ public function execute( $par ) { $this->setHeaders(); @@ -67,7 +68,7 @@ class SpecialBlockList extends SpecialPage { $fields = array( 'Target' => array( 'type' => 'text', - 'label-message' => 'ipadressorusername', + 'label-message' => 'ipaddressorusername', 'tabindex' => '1', 'size' => '45', 'default' => $this->target, @@ -83,7 +84,7 @@ class SpecialBlockList extends SpecialPage { 'flatlist' => true, ), 'Limit' => array( - 'class' => 'HTMLBlockedUsersItemSelect', + 'type' => 'limitselect', 'label-message' => 'table_pager_limit_label', 'options' => array( $lang->formatNum( 20 ) => 20, @@ -97,7 +98,7 @@ class SpecialBlockList extends SpecialPage { ), ); $context = new DerivativeContext( $this->getContext() ); - $context->setTitle( $this->getTitle() ); // Remove subpage + $context->setTitle( $this->getPageTitle() ); // Remove subpage $form = new HTMLForm( $fields, $context ); $form->setMethod( 'get' ); $form->setWrapperLegendMsg( 'ipblocklist-legend' ); @@ -180,11 +181,7 @@ class SpecialBlockList extends SpecialPage { $pager = new BlockListPager( $this, $conds ); if ( $pager->getNumRows() ) { - $out->addHTML( - $pager->getNavigationBar() . - $pager->getBody() . - $pager->getNavigationBar() - ); + $out->addParserOutputContent( $pager->getFullOutput() ); } elseif ( $this->target ) { $out->addWikiMsg( 'ipblocklist-no-results' ); } else { @@ -203,7 +200,11 @@ class SpecialBlockList extends SpecialPage { foreach ( $otherBlockLink as $link ) { $list .= Html::rawElement( 'li', array(), $link ) . "\n"; } - $out->addHTML( Html::rawElement( 'ul', array( 'class' => 'mw-ipblocklist-otherblocks' ), $list ) . "\n" ); + $out->addHTML( Html::rawElement( + 'ul', + array( 'class' => 'mw-ipblocklist-otherblocks' ), + $list + ) . "\n" ); } } @@ -217,20 +218,20 @@ class BlockListPager extends TablePager { protected $page; /** - * @param $page SpecialPage - * @param $conds Array + * @param SpecialPage $page + * @param array $conds */ function __construct( $page, $conds ) { $this->page = $page; $this->conds = $conds; - $this->mDefaultDirection = true; + $this->mDefaultDirection = IndexPager::DIR_DESCENDING; parent::__construct( $page->getContext() ); } function getFieldNames() { static $headers = null; - if ( $headers == array() ) { + if ( $headers === null ) { $headers = array( 'ipb_timestamp' => 'blocklist-timestamp', 'ipb_target' => 'blocklist-target', @@ -404,7 +405,7 @@ class BlockListPager extends TablePager { } public function getTableClass() { - return 'TablePager mw-blocklist'; + return parent::getTableClass() . ' mw-blocklist'; } function getIndexField() { @@ -452,35 +453,3 @@ class BlockListPager extends TablePager { wfProfileOut( __METHOD__ ); } } - -/** - * Items per page dropdown. Essentially a crap workaround for bug 32603. - * - * @todo Do not release 1.19 with this. - */ -class HTMLBlockedUsersItemSelect extends HTMLSelectField { - /** - * Basically don't do any validation. If it's a number that's fine. Also, - * add it to the list if it's not there already - * - * @param $value - * @param $alldata - * @return bool - */ - function validate( $value, $alldata ) { - if ( $value == '' ) { - return true; - } - - // Let folks pick an explicit limit not from our list, as long as it's a real numbr. - if ( !in_array( $value, $this->mParams['options'] ) && $value == intval( $value ) && $value > 0 ) { - // This adds the explicitly requested limit value to the drop-down, - // then makes sure it's sorted correctly so when we output the list - // later, the custom option doesn't just show up last. - $this->mParams['options'][$this->mParent->getLanguage()->formatNum( $value )] = intval( $value ); - asort( $this->mParams['options'] ); - } - - return true; - } -} diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php index 5ad961c3..72f4e466 100644 --- a/includes/specials/SpecialBooksources.php +++ b/includes/specials/SpecialBooksources.php @@ -30,7 +30,6 @@ * @ingroup SpecialPage */ class SpecialBookSources extends SpecialPage { - /** * ISBN passed to the page, if any */ @@ -55,7 +54,10 @@ class SpecialBookSources extends SpecialPage { $this->getOutput()->addHTML( $this->makeForm() ); if ( strlen( $this->isbn ) > 0 ) { if ( !self::isValidISBN( $this->isbn ) ) { - $this->getOutput()->wrapWikiMsg( "
    \n$1\n
    ", 'booksources-invalid-isbn' ); + $this->getOutput()->wrapWikiMsg( + "
    \n$1\n
    ", + 'booksources-invalid-isbn' + ); } $this->showList(); } @@ -115,16 +117,26 @@ class SpecialBookSources extends SpecialPage { * @return string */ private function makeForm() { - global $wgScript; - $form = Html::openElement( 'fieldset' ) . "\n"; - $form .= Html::element( 'legend', array(), $this->msg( 'booksources-search-legend' )->text() ) . "\n"; - $form .= Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . "\n"; - $form .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; - $form .= '

    ' . Xml::inputLabel( $this->msg( 'booksources-isbn' )->text(), 'isbn', 'isbn', 20, $this->isbn, array( 'autofocus' => true ) ); + $form .= Html::element( + 'legend', + array(), + $this->msg( 'booksources-search-legend' )->text() + ) . "\n"; + $form .= Html::openElement( 'form', array( 'method' => 'get', 'action' => wfScript() ) ) . "\n"; + $form .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) . "\n"; + $form .= '

    ' . Xml::inputLabel( + $this->msg( 'booksources-isbn' )->text(), + 'isbn', + 'isbn', + 20, + $this->isbn, + array( 'autofocus' => true ) + ); $form .= ' ' . Xml::submitButton( $this->msg( 'booksources-go' )->text() ) . "

    \n"; $form .= Html::closeElement( 'form' ) . "\n"; $form .= Html::closeElement( 'fieldset' ) . "\n"; + return $form; } @@ -188,6 +200,6 @@ class SpecialBookSources extends SpecialPage { } protected function getGroupName() { - return 'other'; + return 'wiki'; } } diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php index b2ddc220..1bbdbeab 100644 --- a/includes/specials/SpecialBrokenRedirects.php +++ b/includes/specials/SpecialBrokenRedirects.php @@ -28,7 +28,6 @@ * @ingroup SpecialPage */ class BrokenRedirectsPage extends QueryPage { - function __construct( $name = 'BrokenRedirects' ) { parent::__construct( $name ); } @@ -148,7 +147,8 @@ class BrokenRedirectsPage extends QueryPage { ); } - $out .= $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( $links ) )->escaped(); + $out .= $this->msg( 'parentheses' )->rawParams( $this->getLanguage() + ->pipeList( $links ) )->escaped(); $out .= " {$arr} {$to}"; return $out; diff --git a/includes/specials/SpecialCachedPage.php b/includes/specials/SpecialCachedPage.php index 39305f01..cb9b07cd 100644 --- a/includes/specials/SpecialCachedPage.php +++ b/includes/specials/SpecialCachedPage.php @@ -38,7 +38,6 @@ * @since 1.20 */ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { - /** * CacheHelper object to which we forward the non-SpecialPage specific caching work. * Initialized in startCache. @@ -52,7 +51,7 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { * If the cache is enabled or not. * * @since 1.20 - * @var boolean + * @var bool */ protected $cacheEnabled = true; @@ -61,7 +60,7 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { * * @since 1.20 * - * @param $subPage string|null + * @param string|null $subPage */ protected function afterExecute( $subPage ) { $this->saveCache(); @@ -73,7 +72,7 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { * Sets if the cache should be enabled or not. * * @since 1.20 - * @param boolean $cacheEnabled + * @param bool $cacheEnabled */ public function setCacheEnabled( $cacheEnabled ) { $this->cacheHelper->setCacheEnabled( $cacheEnabled ); @@ -85,8 +84,8 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { * * @since 1.20 * - * @param integer|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. - * @param boolean|null $cacheEnabled Sets if the cache should be enabled or not. + * @param int|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp. + * @param bool|null $cacheEnabled Sets if the cache should be enabled or not. */ public function startCache( $cacheExpiry = null, $cacheEnabled = null ) { if ( !isset( $this->cacheHelper ) ) { @@ -142,7 +141,11 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { * @param string|null $key */ public function addCachedHTML( $computeFunction, $args = array(), $key = null ) { - $this->getOutput()->addHTML( $this->cacheHelper->getCachedValue( $computeFunction, $args, $key ) ); + $this->getOutput()->addHTML( $this->cacheHelper->getCachedValue( + $computeFunction, + $args, + $key + ) ); } /** @@ -158,11 +161,12 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { } /** - * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry. + * Sets the time to live for the cache, in seconds or a unix timestamp + * indicating the point of expiry. * * @since 1.20 * - * @param integer $cacheExpiry + * @param int $cacheExpiry */ public function setExpiry( $cacheExpiry ) { $this->cacheHelper->setExpiry( $cacheExpiry ); @@ -187,7 +191,7 @@ abstract class SpecialCachedPage extends SpecialPage implements ICacheHelper { * * @since 1.20 * - * @param boolean $hasCached + * @param bool $hasCached */ public function onCacheInitialized( $hasCached ) { if ( $hasCached ) { diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php index d01bfd7d..95f9efd2 100644 --- a/includes/specials/SpecialCategories.php +++ b/includes/specials/SpecialCategories.php @@ -26,18 +26,55 @@ */ class SpecialCategories extends SpecialPage { - function __construct() { + /** + * @var PageLinkRenderer + */ + protected $linkRenderer = null; + + public function __construct() { parent::__construct( 'Categories' ); + + // Since we don't control the constructor parameters, we can't inject services that way. + // Instead, we initialize services in the execute() method, and allow them to be overridden + // using the initServices() method. + } + + /** + * Initialize or override the PageLinkRenderer SpecialCategories collaborates with. + * Useful mainly for testing. + * + * @todo the pager should also be injected, and de-coupled from the rendering logic. + * + * @param PageLinkRenderer $linkRenderer + */ + public function setPageLinkRenderer( + PageLinkRenderer $linkRenderer + ) { + $this->linkRenderer = $linkRenderer; + } + + /** + * Initialize any services we'll need (unless it has already been provided via a setter). + * This allows for dependency injection even though we don't control object creation. + */ + private function initServices() { + if ( !$this->linkRenderer ) { + $lang = $this->getContext()->getLanguage(); + $titleFormatter = new MediaWikiTitleCodec( $lang, GenderCache::singleton() ); + $this->linkRenderer = new MediaWikiPageLinkRenderer( $titleFormatter ); + } } - function execute( $par ) { + public function execute( $par ) { + $this->initServices(); + $this->setHeaders(); $this->outputHeader(); $this->getOutput()->allowClickjacking(); $from = $this->getRequest()->getText( 'from', $par ); - $cap = new CategoryPager( $this->getContext(), $from ); + $cap = new CategoryPager( $this->getContext(), $from, $this->linkRenderer ); $cap->doQuery(); $this->getOutput()->addHTML( @@ -63,7 +100,19 @@ class SpecialCategories extends SpecialPage { * @ingroup SpecialPage Pager */ class CategoryPager extends AlphabeticPager { - function __construct( IContextSource $context, $from ) { + + /** + * @var PageLinkRenderer + */ + protected $linkRenderer; + + /** + * @param IContextSource $context + * @param string $from + * @param PageLinkRenderer $linkRenderer + */ + public function __construct( IContextSource $context, $from, PageLinkRenderer $linkRenderer + ) { parent::__construct( $context ); $from = str_replace( ' ', '_', $from ); if ( $from !== '' ) { @@ -71,6 +120,8 @@ class CategoryPager extends AlphabeticPager { $this->setOffset( $from ); $this->setIncludeOffset( true ); } + + $this->linkRenderer = $linkRenderer; } function getQueryInfo() { @@ -120,19 +171,18 @@ class CategoryPager extends AlphabeticPager { } function formatRow( $result ) { - $title = Title::makeTitle( NS_CATEGORY, $result->cat_title ); - $titleText = Linker::link( $title, htmlspecialchars( $title->getText() ) ); - $count = $this->msg( 'nmembers' )->numParams( $result->cat_pages )->escaped(); + $title = new TitleValue( NS_CATEGORY, $result->cat_title ); + $text = $title->getText(); + $link = $this->linkRenderer->renderHtmlLink( $title, $text ); - return Xml::tags( 'li', null, $this->getLanguage()->specialList( $titleText, $count ) ) . "\n"; + $count = $this->msg( 'nmembers' )->numParams( $result->cat_pages )->escaped(); + return Html::rawElement( 'li', null, $this->getLanguage()->specialList( $link, $count ) ) . "\n"; } public function getStartForm( $from ) { - global $wgScript; - return Xml::tags( 'form', - array( 'method' => 'get', 'action' => $wgScript ), + array( 'method' => 'get', 'action' => wfScript() ), Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . Xml::fieldset( $this->msg( 'categories' )->text(), diff --git a/includes/specials/SpecialChangeEmail.php b/includes/specials/SpecialChangeEmail.php index aab839fd..e0be838b 100644 --- a/includes/specials/SpecialChangeEmail.php +++ b/includes/specials/SpecialChangeEmail.php @@ -26,26 +26,18 @@ * * @ingroup SpecialPage */ -class SpecialChangeEmail extends UnlistedSpecialPage { - - /** - * Users password - * @var string - */ - protected $mPassword; - +class SpecialChangeEmail extends FormSpecialPage { /** - * Users new email address - * @var string + * @var Status */ - protected $mNewEmail; + private $status; public function __construct() { parent::__construct( 'ChangeEmail', 'editmyprivateinfo' ); } /** - * @return Bool + * @return bool */ function isListed() { global $wgAuth; @@ -55,40 +47,24 @@ class SpecialChangeEmail extends UnlistedSpecialPage { /** * Main execution point + * @param string $par */ function execute( $par ) { - global $wgAuth; - - $this->setHeaders(); - $this->outputHeader(); - $out = $this->getOutput(); $out->disallowUserJs(); $out->addModules( 'mediawiki.special.changeemail' ); - if ( !$wgAuth->allowPropChange( 'emailaddress' ) ) { - $this->error( 'cannotchangeemail' ); - - return; - } - - $user = $this->getUser(); - $request = $this->getRequest(); - - if ( !$request->wasPosted() && !$user->isLoggedIn() ) { - $this->error( 'changeemail-no-info' ); - - return; - } + return parent::execute( $par ); + } - if ( $request->wasPosted() && $request->getBool( 'wpCancel' ) ) { - $this->doReturnTo(); + protected function checkExecutePermissions( User $user ) { + global $wgAuth; - return; + if ( !$wgAuth->allowPropChange( 'emailaddress' ) ) { + throw new ErrorPageError( 'changeemail', 'cannotchangeemail' ); } - $this->checkReadOnly(); - $this->checkPermissions(); + $this->requireLogin( 'changeemail-no-info' ); // This could also let someone check the current email address, so // require both permissions. @@ -96,156 +72,106 @@ class SpecialChangeEmail extends UnlistedSpecialPage { throw new PermissionsError( 'viewmyprivateinfo' ); } - $this->mPassword = $request->getVal( 'wpPassword' ); - $this->mNewEmail = $request->getVal( 'wpNewEmail' ); + parent::checkExecutePermissions( $user ); + } - if ( $request->wasPosted() - && $user->matchEditToken( $request->getVal( 'token' ) ) - ) { - $info = $this->attemptChange( $user, $this->mPassword, $this->mNewEmail ); - if ( $info === true ) { - $this->doReturnTo(); - } elseif ( $info === 'eauth' ) { - # Notify user that a confirmation email has been sent... - $out->wrapWikiMsg( "
    \n$1\n
    ", - 'eauthentsent', $user->getName() ); - $this->doReturnTo( 'soft' ); // just show the link to go back - return; // skip form - } - } + protected function getFormFields() { + $user = $this->getUser(); - $this->showForm(); - } + $fields = array( + 'Name' => array( + 'type' => 'info', + 'label-message' => 'username', + 'default' => $user->getName(), + ), + 'OldEmail' => array( + 'type' => 'info', + 'label-message' => 'changeemail-oldemail', + 'default' => $user->getEmail() ?: $this->msg( 'changeemail-none' )->text(), + ), + 'NewEmail' => array( + 'type' => 'email', + 'label-message' => 'changeemail-newemail', + ), + ); - /** - * @param $type string - */ - protected function doReturnTo( $type = 'hard' ) { - $titleObj = Title::newFromText( $this->getRequest()->getVal( 'returnto' ) ); - if ( !$titleObj instanceof Title ) { - $titleObj = Title::newMainPage(); + if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) { + $fields['Password'] = array( + 'type' => 'password', + 'label-message' => 'changeemail-password', + 'autofocus' => true, + ); } - if ( $type == 'hard' ) { - $this->getOutput()->redirect( $titleObj->getFullURL() ); - } else { - $this->getOutput()->addReturnTo( $titleObj ); - } - } - /** - * @param $msg string - */ - protected function error( $msg ) { - $this->getOutput()->wrapWikiMsg( "

    \n$1\n

    ", $msg ); + return $fields; } - protected function showForm() { - global $wgRequirePasswordforEmailChange; - $user = $this->getUser(); + protected function alterForm( HTMLForm $form ) { + $form->setDisplayFormat( 'vform' ); + $form->setId( 'mw-changeemail-form' ); + $form->setTableId( 'mw-changeemail-table' ); + $form->setWrapperLegend( false ); + $form->setSubmitTextMsg( 'changeemail-submit' ); + $form->addHiddenField( 'returnto', $this->getRequest()->getVal( 'returnto' ) ); + } - $oldEmailText = $user->getEmail() - ? $user->getEmail() - : $this->msg( 'changeemail-none' )->text(); - - $this->getOutput()->addHTML( - Xml::fieldset( $this->msg( 'changeemail-header' )->text() ) . - Xml::openElement( 'form', - array( - 'method' => 'post', - 'action' => $this->getTitle()->getLocalURL(), - 'id' => 'mw-changeemail-form' ) ) . "\n" . - Html::hidden( 'token', $user->getEditToken() ) . "\n" . - Html::hidden( 'returnto', $this->getRequest()->getVal( 'returnto' ) ) . "\n" . - $this->msg( 'changeemail-text' )->parseAsBlock() . "\n" . - Xml::openElement( 'table', array( 'id' => 'mw-changeemail-table' ) ) . "\n" - ); - $items = array( - array( 'wpName', 'username', 'text', $user->getName() ), - array( 'wpOldEmail', 'changeemail-oldemail', 'text', $oldEmailText ), - array( 'wpNewEmail', 'changeemail-newemail', 'email', $this->mNewEmail ), - ); - if ( $wgRequirePasswordforEmailChange ) { - $items[] = array( 'wpPassword', 'changeemail-password', 'password', $this->mPassword ); + public function onSubmit( array $data ) { + if ( $this->getRequest()->getBool( 'wpCancel' ) ) { + $status = Status::newGood( true ); + } else { + $password = isset( $data['Password'] ) ? $data['Password'] : null; + $status = $this->attemptChange( $this->getUser(), $password, $data['NewEmail'] ); } - $this->getOutput()->addHTML( - $this->pretty( $items ) . - "\n" . - "\n" . - "\n" . - '' . - Xml::submitButton( $this->msg( 'changeemail-submit' )->text() ) . - Xml::submitButton( $this->msg( 'changeemail-cancel' )->text(), array( 'name' => 'wpCancel' ) ) . - "\n" . - "\n" . - Xml::closeElement( 'table' ) . - Xml::closeElement( 'form' ) . - Xml::closeElement( 'fieldset' ) . "\n" - ); + $this->status = $status; + + return $status; } - /** - * @param $fields array - * @return string - */ - protected function pretty( $fields ) { - $out = ''; - foreach ( $fields as $list ) { - list( $name, $label, $type, $value ) = $list; - if ( $type == 'text' ) { - $field = htmlspecialchars( $value ); - } else { - $attribs = array( 'id' => $name ); - if ( $name == 'wpPassword' ) { - $attribs[] = 'autofocus'; - } - $field = Html::input( $name, $value, $type, $attribs ); - } - $out .= "\n"; - $out .= "\t"; - if ( $type != 'text' ) { - $out .= Xml::label( $this->msg( $label )->text(), $name ); - } else { - $out .= $this->msg( $label )->escaped(); - } - $out .= "\n"; - $out .= "\t"; - $out .= $field; - $out .= "\n"; - $out .= ""; + public function onSuccess() { + $titleObj = Title::newFromText( $this->getRequest()->getVal( 'returnto' ) ); + if ( !$titleObj instanceof Title ) { + $titleObj = Title::newMainPage(); } - return $out; + if ( $this->status->value === true ) { + $this->getOutput()->redirect( $titleObj->getFullURL() ); + } elseif ( $this->status->value === 'eauth' ) { + # Notify user that a confirmation email has been sent... + $this->getOutput()->wrapWikiMsg( "
    \n$1\n
    ", + 'eauthentsent', $this->getUser()->getName() ); + $this->getOutput()->addReturnTo( $titleObj ); // just show the link to go back + } } /** - * @param $user User - * @param $pass string - * @param $newaddr string - * @return bool|string true or string on success, false on failure + * @param User $user + * @param string $pass + * @param string $newaddr + * @return Status */ protected function attemptChange( User $user, $pass, $newaddr ) { - global $wgAuth, $wgPasswordAttemptThrottle; + global $wgAuth; if ( $newaddr != '' && !Sanitizer::validateEmail( $newaddr ) ) { - $this->error( 'invalidemailaddress' ); - - return false; + return Status::newFatal( 'invalidemailaddress' ); } $throttleCount = LoginForm::incLoginThrottle( $user->getName() ); if ( $throttleCount === true ) { $lang = $this->getLanguage(); - $this->error( array( 'login-throttled', $lang->formatDuration( $wgPasswordAttemptThrottle['seconds'] ) ) ); - - return false; + $throttleInfo = $this->getConfig()->get( 'PasswordAttemptThrottle' ); + return Status::newFatal( + 'changeemail-throttled', + $lang->formatDuration( $throttleInfo['seconds'] ) + ); } - global $wgRequirePasswordforEmailChange; - if ( $wgRequirePasswordforEmailChange && !$user->checkTemporaryPassword( $pass ) && !$user->checkPassword( $pass ) ) { - $this->error( 'wrongpassword' ); - - return false; + if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) + && !$user->checkTemporaryPassword( $pass ) + && !$user->checkPassword( $pass ) + ) { + return Status::newFatal( 'wrongpassword' ); } if ( $throttleCount ) { @@ -255,12 +181,7 @@ class SpecialChangeEmail extends UnlistedSpecialPage { $oldaddr = $user->getEmail(); $status = $user->setEmailWithConfirmation( $newaddr ); if ( !$status->isGood() ) { - $this->getOutput()->addHTML( - '

    ' . - $this->getOutput()->parseInline( $status->getWikiText( 'mailerror' ) ) . - '

    ' ); - - return false; + return $status; } wfRunHooks( 'PrefsEmailAudit', array( $user, $oldaddr, $newaddr ) ); @@ -269,7 +190,11 @@ class SpecialChangeEmail extends UnlistedSpecialPage { $wgAuth->updateExternalDB( $user ); - return $status->value; + return $status; + } + + public function requiresUnblock() { + return false; } protected function getGroupName() { diff --git a/includes/specials/SpecialChangePassword.php b/includes/specials/SpecialChangePassword.php index a75e7e83..24664edb 100644 --- a/includes/specials/SpecialChangePassword.php +++ b/includes/specials/SpecialChangePassword.php @@ -26,228 +26,216 @@ * * @ingroup SpecialPage */ -class SpecialChangePassword extends UnlistedSpecialPage { +class SpecialChangePassword extends FormSpecialPage { + protected $mUserName; + protected $mDomain; - protected $mUserName, $mOldpass, $mNewpass, $mRetype, $mDomain; + // Optional Wikitext Message to show above the password change form + protected $mPreTextMessage = null; + + // label for old password input + protected $mOldPassMsg = null; public function __construct() { parent::__construct( 'ChangePassword', 'editmyprivateinfo' ); + $this->listed( false ); } /** * Main execution point + * @param string|null $par */ function execute( $par ) { - global $wgAuth; - - $this->setHeaders(); - $this->outputHeader(); $this->getOutput()->disallowUserJs(); - $request = $this->getRequest(); - $this->mUserName = trim( $request->getVal( 'wpName' ) ); - $this->mOldpass = $request->getVal( 'wpPassword' ); - $this->mNewpass = $request->getVal( 'wpNewPassword' ); - $this->mRetype = $request->getVal( 'wpRetype' ); - $this->mDomain = $request->getVal( 'wpDomain' ); - - $user = $this->getUser(); - - if ( !$user->isLoggedIn() && !LoginForm::getLoginToken() ) { - LoginForm::setLoginToken(); - } - - if ( !$request->wasPosted() && !$user->isLoggedIn() ) { - $this->error( $this->msg( 'resetpass-no-info' )->text() ); - - return; - } - - if ( $request->wasPosted() && $request->getBool( 'wpCancel' ) ) { - $titleObj = Title::newFromText( $request->getVal( 'returnto' ) ); - if ( !$titleObj instanceof Title ) { - $titleObj = Title::newMainPage(); - } - $query = $request->getVal( 'returntoquery' ); - $this->getOutput()->redirect( $titleObj->getFullURL( $query ) ); + parent::execute( $par ); + } - return; - } + protected function checkExecutePermissions( User $user ) { + parent::checkExecutePermissions( $user ); - $this->checkReadOnly(); - $this->checkPermissions(); - - if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'token' ) ) ) { - try { - $this->mDomain = $wgAuth->getDomain(); - if ( !$wgAuth->allowPasswordChange() ) { - $this->error( $this->msg( 'resetpass_forbidden' )->text() ); - - return; - } - - if ( !$user->isLoggedIn() - && $request->getVal( 'wpLoginOnChangeToken' ) !== LoginForm::getLoginToken() - ) { - // Potential CSRF (bug 62497) - $this->error( $this->msg( 'sessionfailure' )->text() ); - return false; - } - - $this->attemptReset( $this->mNewpass, $this->mRetype ); - - if ( $user->isLoggedIn() ) { - $this->getOutput()->wrapWikiMsg( - "
    \n$1\n
    ", - 'changepassword-success' - ); - $this->getOutput()->returnToMain(); - } else { - LoginForm::setLoginToken(); - $token = LoginForm::getLoginToken(); - $data = array( - 'action' => 'submitlogin', - 'wpName' => $this->mUserName, - 'wpDomain' => $this->mDomain, - 'wpLoginToken' => $token, - 'wpPassword' => $request->getVal( 'wpNewPassword' ), - ) + $request->getValues( 'wpRemember', 'returnto', 'returntoquery' ); - $login = new LoginForm( new DerivativeRequest( $request, $data, true ) ); - $login->setContext( $this->getContext() ); - $login->execute( null ); - } - - return; - } catch ( PasswordError $e ) { - $this->error( $e->getMessage() ); - } + if ( !$this->getRequest()->wasPosted() ) { + $this->requireLogin( 'resetpass-no-info' ); } - $this->showForm(); } /** - * @param $msg string + * Set a message at the top of the Change Password form + * @since 1.23 + * @param Message $msg Message to parse and add to the form header */ - function error( $msg ) { - $this->getOutput()->addHTML( Xml::element( 'p', array( 'class' => 'error' ), $msg ) ); + public function setChangeMessage( Message $msg ) { + $this->mPreTextMessage = $msg; } - function showForm() { - global $wgCookieExpiration; + /** + * Set a message at the top of the Change Password form + * @since 1.23 + * @param string $msg Message label for old/temp password field + */ + public function setOldPasswordMessage( $msg ) { + $this->mOldPassMsg = $msg; + } + protected function getFormFields() { $user = $this->getUser(); - if ( !$this->mUserName ) { - $this->mUserName = $user->getName(); + $request = $this->getRequest(); + + $oldpassMsg = $this->mOldPassMsg; + if ( $oldpassMsg === null ) { + $oldpassMsg = $user->isLoggedIn() ? 'oldpassword' : 'resetpass-temp-password'; } - $rememberMe = ''; - if ( !$user->isLoggedIn() ) { - $rememberMe = '' . - '' . - '' . - Xml::checkLabel( - $this->msg( 'remembermypassword' )->numParams( ceil( $wgCookieExpiration / ( 3600 * 24 ) ) )->text(), - 'wpRemember', 'wpRemember', - $this->getRequest()->getCheck( 'wpRemember' ) ) . - '' . - ''; - $submitMsg = 'resetpass_submit'; - $oldpassMsg = 'resetpass-temp-password'; - } else { - $oldpassMsg = 'oldpassword'; - $submitMsg = 'resetpass-submit-loggedin'; + + $fields = array( + 'Name' => array( + 'type' => 'info', + 'label-message' => 'username', + 'default' => $request->getVal( 'wpName', $user->getName() ), + ), + 'Password' => array( + 'type' => 'password', + 'label-message' => $oldpassMsg, + ), + 'NewPassword' => array( + 'type' => 'password', + 'label-message' => 'newpassword', + ), + 'Retype' => array( + 'type' => 'password', + 'label-message' => 'retypenew', + ), + ); + + if ( !$this->getUser()->isLoggedIn() ) { + if ( !LoginForm::getLoginToken() ) { + LoginForm::setLoginToken(); + } + $fields['LoginOnChangeToken'] = array( + 'type' => 'hidden', + 'label' => 'Change Password Token', + 'default' => LoginForm::getLoginToken(), + ); } + $extraFields = array(); wfRunHooks( 'ChangePasswordForm', array( &$extraFields ) ); - $prettyFields = array( - array( 'wpName', 'username', 'text', $this->mUserName ), - array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ), - array( 'wpNewPassword', 'newpassword', 'password', null ), - array( 'wpRetype', 'retypenew', 'password', null ), - ); - $prettyFields = array_merge( $prettyFields, $extraFields ); - $hiddenFields = array( - 'token' => $user->getEditToken(), - 'wpName' => $this->mUserName, - 'wpDomain' => $this->mDomain, - ) + $this->getRequest()->getValues( 'returnto', 'returntoquery' ); - if ( !$user->isLoggedIn() ) { - $hiddenFields['wpLoginOnChangeToken'] = LoginForm::getLoginToken(); + foreach ( $extraFields as $extra ) { + list( $name, $label, $type, $default ) = $extra; + $fields[$name] = array( + 'type' => $type, + 'name' => $name, + 'label-message' => $label, + 'default' => $default, + ); } - $hiddenFieldsStr = ''; - foreach ( $hiddenFields as $fieldname => $fieldvalue ) { - $hiddenFieldsStr .= Html::hidden( $fieldname, $fieldvalue ) . "\n"; + + if ( !$user->isLoggedIn() ) { + $fields['Remember'] = array( + 'type' => 'check', + 'label' => $this->msg( 'remembermypassword' ) + ->numParams( + ceil( $this->getConfig()->get( 'CookieExpiration' ) / ( 3600 * 24 ) ) + )->text(), + 'default' => $request->getVal( 'wpRemember' ), + ); } - $this->getOutput()->addHTML( - Xml::fieldset( $this->msg( 'resetpass_header' )->text() ) . - Xml::openElement( 'form', - array( - 'method' => 'post', - 'action' => $this->getTitle()->getLocalURL(), - 'id' => 'mw-resetpass-form' ) ) . "\n" . - $hiddenFieldsStr . - $this->msg( 'resetpass_text' )->parseAsBlock() . "\n" . - Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) . "\n" . - $this->pretty( $prettyFields ) . "\n" . - $rememberMe . - "\n" . - "\n" . - '' . - Xml::submitButton( $this->msg( $submitMsg )->text() ) . - Xml::submitButton( $this->msg( 'resetpass-submit-cancel' )->text(), array( 'name' => 'wpCancel' ) ) . - "\n" . - "\n" . - Xml::closeElement( 'table' ) . - Xml::closeElement( 'form' ) . - Xml::closeElement( 'fieldset' ) . "\n" + + return $fields; + } + + protected function alterForm( HTMLForm $form ) { + $form->setId( 'mw-resetpass-form' ); + $form->setTableId( 'mw-resetpass-table' ); + $form->setWrapperLegendMsg( 'resetpass_header' ); + $form->setSubmitTextMsg( + $this->getUser()->isLoggedIn() + ? 'resetpass-submit-loggedin' + : 'resetpass_submit' ); + $form->addButton( 'wpCancel', $this->msg( 'resetpass-submit-cancel' )->text() ); + $form->setHeaderText( $this->msg( 'resetpass_text' )->parseAsBlock() ); + if ( $this->mPreTextMessage instanceof Message ) { + $form->addPreText( $this->mPreTextMessage->parseAsBlock() ); + } + $form->addHiddenFields( + $this->getRequest()->getValues( 'wpName', 'wpDomain', 'returnto', 'returntoquery' ) ); } - /** - * @param $fields array - * @return string - */ - function pretty( $fields ) { - $out = ''; - foreach ( $fields as $list ) { - list( $name, $label, $type, $value ) = $list; - if ( $type == 'text' ) { - $field = htmlspecialchars( $value ); - } else { - $attribs = array( 'id' => $name ); - if ( $name == 'wpNewPassword' || $name == 'wpRetype' ) { - $attribs = array_merge( $attribs, - User::passwordChangeInputAttribs() ); - } - if ( $name == 'wpPassword' ) { - $attribs[] = 'autofocus'; - } - $field = Html::input( $name, $value, $type, $attribs ); + public function onSubmit( array $data ) { + global $wgAuth; + + $request = $this->getRequest(); + + if ( $request->getCheck( 'wpLoginToken' ) ) { + // This comes from Special:Userlogin when logging in with a temporary password + return false; + } + + if ( !$this->getUser()->isLoggedIn() + && $request->getVal( 'wpLoginOnChangeToken' ) !== LoginForm::getLoginToken() + ) { + // Potential CSRF (bug 62497) + return false; + } + + if ( $request->getCheck( 'wpCancel' ) ) { + $titleObj = Title::newFromText( $request->getVal( 'returnto' ) ); + if ( !$titleObj instanceof Title ) { + $titleObj = Title::newMainPage(); } - $out .= "\n"; - $out .= "\t"; + $query = $request->getVal( 'returntoquery' ); + $this->getOutput()->redirect( $titleObj->getFullURL( $query ) ); + + return true; + } + + try { + $this->mUserName = $request->getVal( 'wpName', $this->getUser()->getName() ); + $this->mDomain = $wgAuth->getDomain(); - if ( $type != 'text' ) { - $out .= Xml::label( $this->msg( $label )->text(), $name ); - } else { - $out .= $this->msg( $label )->escaped(); + if ( !$wgAuth->allowPasswordChange() ) { + throw new ErrorPageError( 'changepassword', 'resetpass_forbidden' ); } - $out .= "\n"; - $out .= "\t"; - $out .= $field; - $out .= "\n"; - $out .= ""; + $this->attemptReset( $data['Password'], $data['NewPassword'], $data['Retype'] ); + + return true; + } catch ( PasswordError $e ) { + return $e->getMessage(); } + } - return $out; + public function onSuccess() { + if ( $this->getUser()->isLoggedIn() ) { + $this->getOutput()->wrapWikiMsg( + "
    \n$1\n
    ", + 'changepassword-success' + ); + $this->getOutput()->returnToMain(); + } else { + $request = $this->getRequest(); + LoginForm::setLoginToken(); + $token = LoginForm::getLoginToken(); + $data = array( + 'action' => 'submitlogin', + 'wpName' => $this->mUserName, + 'wpDomain' => $this->mDomain, + 'wpLoginToken' => $token, + 'wpPassword' => $request->getVal( 'wpNewPassword' ), + ) + $request->getValues( 'wpRemember', 'returnto', 'returntoquery' ); + $login = new LoginForm( new DerivativeRequest( $request, $data, true ) ); + $login->setContext( $this->getContext() ); + $login->execute( null ); + } } /** - * @throws PasswordError when cannot set the new password because requirements not met. + * @param string $oldpass + * @param string $newpass + * @param string $retype + * @throws PasswordError When cannot set the new password because requirements not met. */ - protected function attemptReset( $newpass, $retype ) { - global $wgPasswordAttemptThrottle; - + protected function attemptReset( $oldpass, $newpass, $retype ) { $isSelf = ( $this->mUserName === $this->getUser()->getName() ); if ( $isSelf ) { $user = $this->getUser(); @@ -267,32 +255,40 @@ class SpecialChangePassword extends UnlistedSpecialPage { $throttleCount = LoginForm::incLoginThrottle( $this->mUserName ); if ( $throttleCount === true ) { $lang = $this->getLanguage(); - throw new PasswordError( $this->msg( 'login-throttled' ) - ->params( $lang->formatDuration( $wgPasswordAttemptThrottle['seconds'] ) ) + $throttleInfo = $this->getConfig()->get( 'PasswordAttemptThrottle' ); + throw new PasswordError( $this->msg( 'changepassword-throttled' ) + ->params( $lang->formatDuration( $throttleInfo['seconds'] ) ) ->text() ); } + // @todo Make these separate messages, since the message is written for both cases + if ( !$user->checkTemporaryPassword( $oldpass ) && !$user->checkPassword( $oldpass ) ) { + wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'wrongpassword' ) ); + throw new PasswordError( $this->msg( 'resetpass-wrong-oldpass' )->text() ); + } + + // User is resetting their password to their old password + if ( $oldpass === $newpass ) { + throw new PasswordError( $this->msg( 'resetpass-recycled' )->text() ); + } + + // Do AbortChangePassword after checking mOldpass, so we don't leak information + // by possibly aborting a new password before verifying the old password. $abortMsg = 'resetpass-abort-generic'; - if ( !wfRunHooks( 'AbortChangePassword', array( $user, $this->mOldpass, $newpass, &$abortMsg ) ) ) { + if ( !wfRunHooks( 'AbortChangePassword', array( $user, $oldpass, $newpass, &$abortMsg ) ) ) { wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'abortreset' ) ); throw new PasswordError( $this->msg( $abortMsg )->text() ); } - if ( !$user->checkTemporaryPassword( $this->mOldpass ) && !$user->checkPassword( $this->mOldpass ) ) { - wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'wrongpassword' ) ); - throw new PasswordError( $this->msg( 'resetpass-wrong-oldpass' )->text() ); - } - // Please reset throttle for successful logins, thanks! if ( $throttleCount ) { LoginForm::clearLoginThrottle( $this->mUserName ); } try { - $user->setPassword( $this->mNewpass ); + $user->setPassword( $newpass ); wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'success' ) ); - $this->mNewpass = $this->mOldpass = $this->mRetype = ''; } catch ( PasswordError $e ) { wfRunHooks( 'PrefsPasswordAudit', array( $user, $newpass, 'error' ) ); throw new PasswordError( $e->getMessage() ); @@ -301,12 +297,17 @@ class SpecialChangePassword extends UnlistedSpecialPage { if ( $isSelf ) { // This is needed to keep the user connected since // changing the password also modifies the user's token. - $user->setCookies(); + $remember = $this->getRequest()->getCookie( 'Token' ) !== null; + $user->setCookies( null, null, $remember ); } - + $user->resetPasswordExpiration(); $user->saveSettings(); } + public function requiresUnblock() { + return false; + } + protected function getGroupName() { return 'users'; } diff --git a/includes/specials/SpecialComparePages.php b/includes/specials/SpecialComparePages.php index fc6b0c58..da1a54cd 100644 --- a/includes/specials/SpecialComparePages.php +++ b/includes/specials/SpecialComparePages.php @@ -43,8 +43,8 @@ class SpecialComparePages extends SpecialPage { /** * Show a form for filtering namespace and username * - * @param $par String - * @return String + * @param string $par + * @return string */ public function execute( $par ) { $this->setHeaders(); diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php index 3828b1c6..d771589d 100644 --- a/includes/specials/SpecialConfirmemail.php +++ b/includes/specials/SpecialConfirmemail.php @@ -45,6 +45,8 @@ class EmailConfirmation extends UnlistedSpecialPage { $this->checkReadOnly(); $this->checkPermissions(); + $this->requireLogin( 'confirmemail_needlogin' ); + // This could also let someone check the current email address, so // require both permissions. if ( !$this->getUser()->isAllowed( 'viewmyprivateinfo' ) ) { @@ -52,22 +54,10 @@ class EmailConfirmation extends UnlistedSpecialPage { } if ( $code === null || $code === '' ) { - if ( $this->getUser()->isLoggedIn() ) { - if ( Sanitizer::validateEmail( $this->getUser()->getEmail() ) ) { - $this->showRequestForm(); - } else { - $this->getOutput()->addWikiMsg( 'confirmemail_noemail' ); - } + if ( Sanitizer::validateEmail( $this->getUser()->getEmail() ) ) { + $this->showRequestForm(); } else { - $llink = Linker::linkKnown( - SpecialPage::getTitleFor( 'Userlogin' ), - $this->msg( 'loginreqlink' )->escaped(), - array(), - array( 'returnto' => $this->getTitle()->getPrefixedText() ) - ); - $this->getOutput()->addHTML( - $this->msg( 'confirmemail_needlogin' )->rawParams( $llink )->parse() - ); + $this->getOutput()->addWikiMsg( 'confirmemail_noemail' ); } } else { $this->attemptConfirm( $code ); @@ -90,19 +80,17 @@ class EmailConfirmation extends UnlistedSpecialPage { } else { $out->addWikiText( $status->getWikiText( 'confirmemail_sendfailed' ) ); } + } elseif ( $user->isEmailConfirmed() ) { + // date and time are separate parameters to facilitate localisation. + // $time is kept for backward compat reasons. + // 'emailauthenticated' is also used in SpecialPreferences.php + $lang = $this->getLanguage(); + $emailAuthenticated = $user->getEmailAuthenticationTimestamp(); + $time = $lang->userTimeAndDate( $emailAuthenticated, $user ); + $d = $lang->userDate( $emailAuthenticated, $user ); + $t = $lang->userTime( $emailAuthenticated, $user ); + $out->addWikiMsg( 'emailauthenticated', $time, $d, $t ); } else { - if ( $user->isEmailConfirmed() ) { - // date and time are separate parameters to facilitate localisation. - // $time is kept for backward compat reasons. - // 'emailauthenticated' is also used in SpecialPreferences.php - $lang = $this->getLanguage(); - $emailAuthenticated = $user->getEmailAuthenticationTimestamp(); - $time = $lang->userTimeAndDate( $emailAuthenticated, $user ); - $d = $lang->userDate( $emailAuthenticated, $user ); - $t = $lang->userTime( $emailAuthenticated, $user ); - $out->addWikiMsg( 'emailauthenticated', $time, $d, $t ); - } - if ( $user->isEmailConfirmationPending() ) { $out->wrapWikiMsg( "
    \n$1\n
    ", @@ -113,7 +101,7 @@ class EmailConfirmation extends UnlistedSpecialPage { $out->addWikiMsg( 'confirmemail_text' ); $form = Html::openElement( 'form', - array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL() ) + array( 'method' => 'post', 'action' => $this->getPageTitle()->getLocalURL() ) ) . "\n"; $form .= Html::hidden( 'token', $user->getEditToken() ) . "\n"; $form .= Xml::submitButton( $this->msg( 'confirmemail_send' )->text() ) . "\n"; diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 1fe98190..32a887c4 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -26,8 +26,7 @@ * * @ingroup SpecialPage */ - -class SpecialContributions extends SpecialPage { +class SpecialContributions extends IncludableSpecialPage { protected $opts; public function __construct() { @@ -63,7 +62,9 @@ class SpecialContributions extends SpecialPage { $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' ); if ( !strlen( $target ) ) { - $out->addHTML( $this->getForm() ); + if ( !$this->including() ) { + $out->addHTML( $this->getForm() ); + } return; } @@ -73,6 +74,7 @@ class SpecialContributions extends SpecialPage { $this->opts['limit'] = $request->getInt( 'limit', $user->getOption( 'rclimit' ) ); $this->opts['target'] = $target; $this->opts['topOnly'] = $request->getBool( 'topOnly' ); + $this->opts['newOnly'] = $request->getBool( 'newOnly' ); $nt = Title::makeTitleSafe( NS_USER, $target ); if ( !$nt ) { @@ -94,14 +96,14 @@ class SpecialContributions extends SpecialPage { $out->setHTMLTitle( $this->msg( 'pagetitle', $this->msg( 'contributions-title', $target )->plain() - ) ); + )->inContentLanguage() ); $this->getSkin()->setRelevantUser( $userObj ); } else { $out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) ); $out->setHTMLTitle( $this->msg( 'pagetitle', $this->msg( 'sp-contributions-newbies-title' )->plain() - ) ); + )->inContentLanguage() ); } if ( ( $ns = $request->getVal( 'namespace', null ) ) !== null && $ns !== '' ) { @@ -131,34 +133,40 @@ class SpecialContributions extends SpecialPage { } $feedType = $request->getVal( 'feed' ); + + $feedParams = array( + 'action' => 'feedcontributions', + 'user' => $target, + ); + if ( $this->opts['topOnly'] ) { + $feedParams['toponly'] = true; + } + if ( $this->opts['newOnly'] ) { + $feedParams['newonly'] = true; + } + if ( $this->opts['deletedOnly'] ) { + $feedParams['deletedonly'] = true; + } + if ( $this->opts['tagfilter'] !== '' ) { + $feedParams['tagfilter'] = $this->opts['tagfilter']; + } + if ( $this->opts['namespace'] !== '' ) { + $feedParams['namespace'] = $this->opts['namespace']; + } + // Don't use year and month for the feed URL, but pass them on if + // we redirect to API (if $feedType is specified) + if ( $feedType && $this->opts['year'] !== null ) { + $feedParams['year'] = $this->opts['year']; + } + if ( $feedType && $this->opts['month'] !== null ) { + $feedParams['month'] = $this->opts['month']; + } + if ( $feedType ) { - // Maintain some level of backwards compatability + // Maintain some level of backwards compatibility // If people request feeds using the old parameters, redirect to API - $apiParams = array( - 'action' => 'feedcontributions', - 'feedformat' => $feedType, - 'user' => $target, - ); - if ( $this->opts['topOnly'] ) { - $apiParams['toponly'] = true; - } - if ( $this->opts['deletedOnly'] ) { - $apiParams['deletedonly'] = true; - } - if ( $this->opts['tagfilter'] !== '' ) { - $apiParams['tagfilter'] = $this->opts['tagfilter']; - } - if ( $this->opts['namespace'] !== '' ) { - $apiParams['namespace'] = $this->opts['namespace']; - } - if ( $this->opts['year'] !== null ) { - $apiParams['year'] = $this->opts['year']; - } - if ( $this->opts['month'] !== null ) { - $apiParams['month'] = $this->opts['month']; - } - - $url = wfAppendQuery( wfScript( 'api' ), $apiParams ); + $feedParams['feedformat'] = $feedType; + $url = wfAppendQuery( wfScript( 'api' ), $feedParams ); $out->redirect( $url, '301' ); @@ -166,11 +174,12 @@ class SpecialContributions extends SpecialPage { } // Add RSS/atom links - $this->addFeedLinks( array( 'action' => 'feedcontributions', 'user' => $target ) ); - - if ( wfRunHooks( 'SpecialContributionsBeforeMainOutput', array( $id ) ) ) { - $out->addHTML( $this->getForm() ); + $this->addFeedLinks( $feedParams ); + if ( wfRunHooks( 'SpecialContributionsBeforeMainOutput', array( $id, $userObj, $this ) ) ) { + if ( !$this->including() ) { + $out->addHTML( $this->getForm() ); + } $pager = new ContribsPager( $this->getContext(), array( 'target' => $target, 'contribs' => $this->opts['contribs'], @@ -180,6 +189,7 @@ class SpecialContributions extends SpecialPage { 'month' => $this->opts['month'], 'deletedOnly' => $this->opts['deletedOnly'], 'topOnly' => $this->opts['topOnly'], + 'newOnly' => $this->opts['newOnly'], 'nsInvert' => $this->opts['nsInvert'], 'associated' => $this->opts['associated'], ) ); @@ -193,11 +203,13 @@ class SpecialContributions extends SpecialPage { $out->showLagWarning( $lag ); } - $out->addHTML( - '

    ' . $pager->getNavigationBar() . '

    ' . - $pager->getBody() . - '

    ' . $pager->getNavigationBar() . '

    ' - ); + $output = $pager->getBody(); + if ( !$this->including() ) { + $output = '

    ' . $pager->getNavigationBar() . '

    ' . + $output . + '

    ' . $pager->getNavigationBar() . '

    '; + } + $out->addHTML( $output ); } $out->preventClickjacking( $pager->getPreventClickjacking() ); @@ -214,10 +226,12 @@ class SpecialContributions extends SpecialPage { } if ( $message ) { - if ( !$this->msg( $message, $target )->isDisabled() ) { - $out->wrapWikiMsg( - "", - array( $message, $target ) ); + if ( !$this->including() ) { + if ( !$this->msg( $message, $target )->isDisabled() ) { + $out->wrapWikiMsg( + "", + array( $message, $target ) ); + } } } } @@ -225,13 +239,26 @@ class SpecialContributions extends SpecialPage { /** * Generates the subheading with links - * @param $userObj User object for the target - * @return String: appropriately-escaped HTML to be output literally + * @param User $userObj User object for the target + * @return string Appropriately-escaped HTML to be output literally * @todo FIXME: Almost the same as getSubTitle in SpecialDeletedContributions.php. * Could be combined. */ protected function contributionsSub( $userObj ) { if ( $userObj->isAnon() ) { + // Show a warning message that the user being searched for doesn't exists + if ( !User::isIP( $userObj->getName() ) ) { + $this->getOutput()->wrapWikiMsg( + "
    \n\$1\n
    ", + array( + 'contributions-userdoesnotexist', + wfEscapeWikiText( $userObj->getName() ), + ) + ); + if ( !$this->including() ) { + $this->getOutput()->setStatusCode( 404 ); + } + } $user = htmlspecialchars( $userObj->getName() ); } else { $user = Linker::link( $userObj->getUserPage(), htmlspecialchars( $userObj->getName() ) ); @@ -246,25 +273,32 @@ class SpecialContributions extends SpecialPage { // Show a note if the user is blocked and display the last block log entry. // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs, // and also this will display a totally irrelevant log entry as a current block. - if ( $userObj->isBlocked() && $userObj->getBlock()->getType() != Block::TYPE_AUTO ) { - $out = $this->getOutput(); // showLogExtract() wants first parameter by reference - LogEventsList::showLogExtract( - $out, - 'block', - $nt, - '', - array( - 'lim' => 1, - 'showIfEmpty' => false, - 'msgKey' => array( - $userObj->isAnon() ? - 'sp-contributions-blocked-notice-anon' : - 'sp-contributions-blocked-notice', - $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice' - ), - 'offset' => '' # don't use WebRequest parameter offset - ) - ); + if ( !$this->including() ) { + $block = Block::newFromTarget( $userObj, $userObj ); + if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) { + if ( $block->getType() == Block::TYPE_RANGE ) { + $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(); + } + + $out = $this->getOutput(); // showLogExtract() wants first parameter by reference + LogEventsList::showLogExtract( + $out, + 'block', + $nt, + '', + array( + 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => array( + $userObj->isAnon() ? + 'sp-contributions-blocked-notice-anon' : + 'sp-contributions-blocked-notice', + $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice' + ), + 'offset' => '' # don't use WebRequest parameter offset + ) + ); + } } } @@ -273,9 +307,9 @@ class SpecialContributions extends SpecialPage { /** * Links to different places. - * @param $userpage Title: Target user page - * @param $talkpage Title: Talk page - * @param $target User: Target user object + * @param Title $userpage Target user page + * @param Title $talkpage Talk page + * @param User $target Target user object * @return array */ public function getUserLinks( Title $userpage, Title $talkpage, User $target ) { @@ -311,6 +345,16 @@ class SpecialContributions extends SpecialPage { array(), array( 'page' => $userpage->getPrefixedText() ) ); + + # Suppression log link (bug 59120) + if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) { + $tools[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Log', 'suppress' ), + $this->msg( 'sp-contributions-suppresslog' )->escaped(), + array(), + array( 'offender' => $username ) + ); + } } # Uploads $tools[] = Linker::linkKnown( @@ -349,12 +393,10 @@ class SpecialContributions extends SpecialPage { /** * Generates the namespace selector form with hidden attributes. - * @return String: HTML fragment + * @return string HTML fragment */ protected function getForm() { - global $wgScript; - - $this->opts['title'] = $this->getTitle()->getPrefixedText(); + $this->opts['title'] = $this->getPageTitle()->getPrefixedText(); if ( !isset( $this->opts['target'] ) ) { $this->opts['target'] = ''; } else { @@ -397,11 +439,15 @@ class SpecialContributions extends SpecialPage { $this->opts['topOnly'] = false; } + if ( !isset( $this->opts['newOnly'] ) ) { + $this->opts['newOnly'] = false; + } + $form = Html::openElement( 'form', array( 'method' => 'get', - 'action' => $wgScript, + 'action' => wfScript(), 'class' => 'mw-contributions-form' ) ); @@ -416,6 +462,7 @@ class SpecialContributions extends SpecialPage { 'year', 'month', 'topOnly', + 'newOnly', 'associated' ); @@ -548,10 +595,21 @@ class SpecialContributions extends SpecialPage { array( 'class' => 'mw-input' ) ) ); + $checkLabelNewOnly = Html::rawElement( + 'span', + array( 'style' => 'white-space: nowrap' ), + Xml::checkLabel( + $this->msg( 'sp-contributions-newonly' )->text(), + 'newOnly', + 'mw-show-new-only', + $this->opts['newOnly'], + array( 'class' => 'mw-input' ) + ) + ); $extraOptions = Html::rawElement( 'td', array( 'colspan' => 2 ), - $deletedOnlyCheck . $checkLabelTopOnly + $deletedOnlyCheck . $checkLabelTopOnly . $checkLabelNewOnly ); $dateSelectionAndSubmit = Xml::tags( 'td', array( 'colspan' => 2 ), @@ -594,13 +652,16 @@ class SpecialContributions extends SpecialPage { * @ingroup SpecialPage Pager */ class ContribsPager extends ReverseChronologicalPager { - public $mDefaultDirection = true; + public $mDefaultDirection = IndexPager::DIR_DESCENDING; public $messages; public $target; public $namespace = ''; public $mDb; public $preventClickjacking = false; + /** @var DatabaseBase */ + public $mDbSecondary; + /** * @var array */ @@ -632,11 +693,16 @@ class ContribsPager extends ReverseChronologicalPager { $this->deletedOnly = !empty( $options['deletedOnly'] ); $this->topOnly = !empty( $options['topOnly'] ); + $this->newOnly = !empty( $options['newOnly'] ); $year = isset( $options['year'] ) ? $options['year'] : false; $month = isset( $options['month'] ) ? $options['month'] : false; $this->getDateCond( $year, $month ); + // Most of this code will use the 'contributions' group DB, which can map to slaves + // with extra user based indexes or partioning by user. The additional metadata + // queries should use a regular slave since the lookup pattern is not all by user. + $this->mDbSecondary = wfGetDB( DB_SLAVE ); // any random slave $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); } @@ -651,9 +717,9 @@ class ContribsPager extends ReverseChronologicalPager { * This method basically executes the exact same code as the parent class, though with * a hook added, to allow extentions to add additional queries. * - * @param string $offset index offset, inclusive - * @param $limit Integer: exact query limit - * @param $descending Boolean: query direction, false for ascending, true for descending + * @param string $offset Index offset, inclusive + * @param int $limit Exact query limit + * @param bool $descending Query direction, false for ascending, true for descending * @return ResultWrapper */ function reallyDoQuery( $offset, $limit, $descending ) { @@ -725,7 +791,7 @@ class ContribsPager extends ReverseChronologicalPager { // Paranoia: avoid brute force searches (bug 17342) if ( !$user->isAllowed( 'deletedhistory' ) ) { $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'; - } elseif ( !$user->isAllowed( 'suppressrevision' ) ) { + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { $conds[] = $this->mDb->bitAnd( 'rev_deleted', Revision::SUPPRESSED_USER ) . ' != ' . Revision::SUPPRESSED_USER; } @@ -735,6 +801,11 @@ class ContribsPager extends ReverseChronologicalPager { # Get the current user name for accounts $join_cond['user'] = Revision::userJoinCond(); + $options = array(); + if ( $index ) { + $options['USE INDEX'] = array( 'revision' => $index ); + } + $queryInfo = array( 'tables' => $tables, 'fields' => array_merge( @@ -744,7 +815,7 @@ class ContribsPager extends ReverseChronologicalPager { 'page_latest', 'page_is_redirect', 'page_len' ) ), 'conds' => $conds, - 'options' => array( 'USE INDEX' => array( 'revision' => $index ) ), + 'options' => $options, 'join_conds' => $join_cond ); @@ -766,10 +837,10 @@ class ContribsPager extends ReverseChronologicalPager { $condition = array(); $join_conds = array(); $tables = array( 'revision', 'page', 'user' ); + $index = false; if ( $this->contribs == 'newbie' ) { $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ ); $condition[] = 'rev_user >' . (int)( $max - $max / 100 ); - $index = 'user_timestamp'; # ignore local groups with the bot right # @todo FIXME: Global groups may have 'bot' rights $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' ); @@ -802,6 +873,10 @@ class ContribsPager extends ReverseChronologicalPager { $condition[] = 'rev_id = page_latest'; } + if ( $this->newOnly ) { + $condition[] = 'rev_parent_id = 0'; + } + return array( $tables, $index, $condition, $join_conds ); } @@ -851,7 +926,7 @@ class ContribsPager extends ReverseChronologicalPager { $batch->add( $row->page_namespace, $row->page_title ); } } - $this->mParentLens = Revision::getParentLengths( $this->getDatabase(), $revIds ); + $this->mParentLens = Revision::getParentLengths( $this->mDbSecondary, $revIds ); $batch->execute(); $this->mResult->seek( 0 ); } @@ -879,7 +954,7 @@ class ContribsPager extends ReverseChronologicalPager { * was not written by the target user. * * @todo This would probably look a lot nicer in a table. - * @param $row + * @param object $row * @return string */ function formatRow( $row ) { @@ -896,8 +971,12 @@ class ContribsPager extends ReverseChronologicalPager { * to extensions to subscribe to the hook to parse the row. */ wfSuppressWarnings(); - $rev = new Revision( $row ); - $validRevision = (bool)$rev->getId(); + try { + $rev = new Revision( $row ); + $validRevision = (bool)$rev->getId(); + } catch ( MWException $e ) { + $validRevision = false; + } wfRestoreWarnings(); if ( $validRevision ) { @@ -986,7 +1065,8 @@ class ContribsPager extends ReverseChronologicalPager { # Show user names for /newbies as there may be different users. # Note that we already excluded rows with hidden user names. if ( $this->contribs == 'newbie' ) { - $userlink = ' . . ' . $lang->getDirMark() . Linker::userLink( $rev->getUser(), $rev->getUserText() ); + $userlink = ' . . ' . $lang->getDirMark() + . Linker::userLink( $rev->getUser(), $rev->getUserText() ); $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( Linker::userTalkLink( $rev->getUser(), $rev->getUserText() ) )->escaped() . ' '; } else { @@ -1036,7 +1116,7 @@ class ContribsPager extends ReverseChronologicalPager { wfRunHooks( 'ContributionsLineEnding', array( $this, &$ret, $row, &$classes ) ); if ( $classes === array() && $ret === '' ) { - wfDebug( 'Dropping Special:Contribution row that could not be formatted' ); + wfDebug( "Dropping Special:Contribution row that could not be formatted\n" ); $ret = "\n"; } else { $ret = Html::rawElement( 'li', array( 'class' => $classes ), $ret ) . "\n"; diff --git a/includes/specials/SpecialCreateAccount.php b/includes/specials/SpecialCreateAccount.php new file mode 100644 index 00000000..30e3833c --- /dev/null +++ b/includes/specials/SpecialCreateAccount.php @@ -0,0 +1,56 @@ + Special:UserLogin/signup. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup SpecialPage + */ + +/** + * Redirect page: Special:CreateAccount --> Special:UserLogin/signup. + * @todo FIXME: This (and the rest of the login frontend) needs to die a horrible painful death + * + * @ingroup SpecialPage + */ +class SpecialCreateAccount extends SpecialRedirectToSpecial { + function __construct() { + parent::__construct( + 'CreateAccount', + 'Userlogin', + 'signup', + array( 'returnto', 'returntoquery', 'uselang' ) + ); + } + + // No reason to hide this link on Special:Specialpages + public function isListed() { + return true; + } + + public function isRestricted() { + return !User::groupHasPermission( '*', 'createaccount' ); + } + + public function userCanExecute( User $user ) { + return $user->isAllowed( 'createaccount' ); + } + + protected function getGroupName() { + return 'login'; + } +} diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php index 9b9888ad..68f2c469 100644 --- a/includes/specials/SpecialDeletedContributions.php +++ b/includes/specials/SpecialDeletedContributions.php @@ -26,7 +26,7 @@ * @ingroup SpecialPage */ class DeletedContribsPager extends IndexPager { - public $mDefaultDirection = true; + public $mDefaultDirection = IndexPager::DIR_DESCENDING; public $messages; public $target; public $namespace = ''; @@ -62,7 +62,7 @@ class DeletedContribsPager extends IndexPager { // Paranoia: avoid brute force searches (bug 17792) if ( !$user->isAllowed( 'deletedhistory' ) ) { $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::DELETED_USER ) . ' = 0'; - } elseif ( !$user->isAllowed( 'suppressrevision' ) ) { + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { $conds[] = $this->mDb->bitAnd( 'ar_deleted', Revision::SUPPRESSED_USER ) . ' != ' . Revision::SUPPRESSED_USER; } @@ -147,7 +147,7 @@ class DeletedContribsPager extends IndexPager { * written by the target user. * * @todo This would probably look a lot nicer in a table. - * @param $row + * @param stdClass $row * @return string */ function formatRow( $row ) { @@ -276,7 +276,7 @@ class DeletedContribsPager extends IndexPager { class DeletedContributionsPage extends SpecialPage { function __construct() { parent::__construct( 'DeletedContributions', 'deletedhistory', - /*listed*/true, /*function*/false, /*file*/false ); + /*listed*/true, /*function*/false, /*file*/false ); } /** @@ -286,8 +286,6 @@ class DeletedContributionsPage extends SpecialPage { * @param string $par (optional) user name of the user for which to show the contributions */ function execute( $par ) { - global $wgQueryPageDefaultLimit; - $this->setHeaders(); $this->outputHeader(); @@ -317,7 +315,7 @@ class DeletedContributionsPage extends SpecialPage { return; } - $options['limit'] = $request->getInt( 'limit', $wgQueryPageDefaultLimit ); + $options['limit'] = $request->getInt( 'limit', $this->getConfig()->get( 'QueryPageDefaultLimit' ) ); $options['target'] = $target; $userObj = User::newFromName( $target, false ); @@ -375,8 +373,8 @@ class DeletedContributionsPage extends SpecialPage { /** * Generates the subheading with links - * @param $userObj User object for the target - * @return String: appropriately-escaped HTML to be output literally + * @param User $userObj User object for the target + * @return string Appropriately-escaped HTML to be output literally * @todo FIXME: Almost the same as contributionsSub in SpecialContributions.php. Could be combined. */ function getSubTitle( $userObj ) { @@ -427,6 +425,15 @@ class DeletedContributionsPage extends SpecialPage { 'page' => $nt->getPrefixedText() ) ); + # Suppression log link (bug 59120) + if ( $this->getUser()->isAllowed( 'suppressionlog' ) ) { + $tools[] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Log', 'suppress' ), + $this->msg( 'sp-contributions-suppresslog' )->escaped(), + array(), + array( 'offender' => $userObj->getName() ) + ); + } } # Uploads @@ -463,7 +470,12 @@ class DeletedContributionsPage extends SpecialPage { $links = $this->getLanguage()->pipeList( $tools ); // Show a note if the user is blocked and display the last block log entry. - if ( $userObj->isBlocked() ) { + $block = Block::newFromTarget( $userObj, $userObj ); + if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) { + if ( $block->getType() == Block::TYPE_RANGE ) { + $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(); + } + // LogEventsList::showLogExtract() wants the first parameter by ref $out = $this->getOutput(); LogEventsList::showLogExtract( @@ -476,7 +488,7 @@ class DeletedContributionsPage extends SpecialPage { 'showIfEmpty' => false, 'msgKey' => array( 'sp-contributions-blocked-notice', - $nt->getText() # Support GENDER in 'sp-contributions-blocked-notice' + $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice' ), 'offset' => '' # don't use $this->getRequest() parameter offset ) @@ -489,13 +501,11 @@ class DeletedContributionsPage extends SpecialPage { /** * Generates the namespace selector form with hidden attributes. - * @param array $options the options to be included. + * @param array $options The options to be included. * @return string */ function getForm( $options ) { - global $wgScript; - - $options['title'] = $this->getTitle()->getPrefixedText(); + $options['title'] = $this->getPageTitle()->getPrefixedText(); if ( !isset( $options['target'] ) ) { $options['target'] = ''; } else { @@ -514,7 +524,7 @@ class DeletedContributionsPage extends SpecialPage { $options['target'] = ''; } - $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => wfScript() ) ); foreach ( $options as $name => $value ) { if ( in_array( $name, array( 'namespace', 'target', 'contribs' ) ) ) { diff --git a/includes/specials/SpecialDiff.php b/includes/specials/SpecialDiff.php new file mode 100644 index 00000000..77d23173 --- /dev/null +++ b/includes/specials/SpecialDiff.php @@ -0,0 +1,61 @@ +mAllowedRedirectParams = array(); + } + + function getRedirect( $subpage ) { + $parts = explode( '/', $subpage ); + + // Try to parse the values given, generating somewhat pretty URLs if possible + if ( count( $parts ) === 1 && $parts[0] !== '' ) { + $this->mAddedRedirectParams['diff'] = $parts[0]; + } elseif ( count( $parts ) === 2 ) { + $this->mAddedRedirectParams['oldid'] = $parts[0]; + $this->mAddedRedirectParams['diff'] = $parts[1]; + } else { + // Wrong number of parameters, bail out + throw new ErrorPageError( 'nopagetitle', 'nopagetext' ); + } + + return true; + } +} diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php index 501552e9..3656b9cc 100644 --- a/includes/specials/SpecialEditWatchlist.php +++ b/includes/specials/SpecialEditWatchlist.php @@ -36,7 +36,8 @@ */ class SpecialEditWatchlist extends UnlistedSpecialPage { /** - * Editing modes + * Editing modes. EDIT_CLEAR is no longer used; the "Clear" link scared people + * too much. Now it's passed on to the raw editor, from which it's very easy to clear. */ const EDIT_CLEAR = 1; const EDIT_RAW = 2; @@ -55,34 +56,21 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { /** * Main execution point * - * @param $mode int + * @param int $mode */ public function execute( $mode ) { $this->setHeaders(); - $out = $this->getOutput(); - # Anons don't get a watchlist - if ( $this->getUser()->isAnon() ) { - $out->setPageTitle( $this->msg( 'watchnologin' ) ); - $llink = Linker::linkKnown( - SpecialPage::getTitleFor( 'Userlogin' ), - $this->msg( 'loginreqlink' )->escaped(), - array(), - array( 'returnto' => $this->getTitle()->getPrefixedText() ) - ); - $out->addHTML( $this->msg( 'watchlistanontext' )->rawParams( $llink )->parse() ); + $this->requireLogin( 'watchlistanontext' ); - return; - } + $out = $this->getOutput(); $this->checkPermissions(); $this->checkReadOnly(); $this->outputHeader(); - - $out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName() ) - ->rawParams( SpecialEditWatchlist::buildTools( null ) ) ); + $this->outputSubtitle(); # B/C: $mode used to be waaay down the parameter list, and the first parameter # was $wgUser @@ -95,10 +83,6 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $mode = self::getMode( $this->getRequest(), $mode ); switch ( $mode ) { - case self::EDIT_CLEAR: - // The "Clear" link scared people too much. - // Pass on to the raw editor, from which it's very easy to clear. - case self::EDIT_RAW: $out->setPageTitle( $this->msg( 'watchlistedit-raw-title' ) ); $form = $this->getRawForm(); @@ -107,26 +91,73 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) ); } break; - - case self::EDIT_NORMAL: - default: - $out->setPageTitle( $this->msg( 'watchlistedit-normal-title' ) ); - $form = $this->getNormalForm(); + case self::EDIT_CLEAR: + $out->setPageTitle( $this->msg( 'watchlistedit-clear-title' ) ); + $form = $this->getClearForm(); if ( $form->show() ) { $out->addHTML( $this->successMessage ); $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) ); - } elseif ( $this->toc !== false ) { - $out->prependHTML( $this->toc ); } break; + + case self::EDIT_NORMAL: + default: + $this->executeViewEditWatchlist(); + break; + } + } + + /** + * Renders a subheader on the watchlist page. + */ + protected function outputSubtitle() { + $out = $this->getOutput(); + $out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName() ) + ->rawParams( SpecialEditWatchlist::buildTools( null ) ) ); + } + + /** + * Executes an edit mode for the watchlist view, from which you can manage your watchlist + * + */ + protected function executeViewEditWatchlist() { + $out = $this->getOutput(); + $out->setPageTitle( $this->msg( 'watchlistedit-normal-title' ) ); + $form = $this->getNormalForm(); + if ( $form->show() ) { + $out->addHTML( $this->successMessage ); + $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) ); + } elseif ( $this->toc !== false ) { + $out->prependHTML( $this->toc ); + $out->addModules( 'mediawiki.toc' ); } } + /** + * Return an array of subpages beginning with $search that this special page will accept. + * + * @param string $search Prefix to search for + * @param int $limit Maximum number of results to return + * @return string[] Matching subpages + */ + public function prefixSearchSubpages( $search, $limit = 10 ) { + return self::prefixSearchArray( + $search, + $limit, + // SpecialWatchlist uses SpecialEditWatchlist::getMode, so new types should be added + // here and there - no 'edit' here, because that the default for this page + array( + 'clear', + 'raw', + ) + ); + } + /** * Extract a list of titles from a blob of text, returning * (prefixed) strings; unwatchable titles are ignored * - * @param $list String + * @param string $list * @return array */ private function extractTitles( $list ) { @@ -176,14 +207,14 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { } if ( count( $toWatch ) > 0 ) { - $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added' - )->numParams( count( $toWatch ) )->parse(); + $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added' ) + ->numParams( count( $toWatch ) )->parse(); $this->showTitles( $toWatch, $this->successMessage ); } if ( count( $toUnwatch ) > 0 ) { - $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' - )->numParams( count( $toUnwatch ) )->parse(); + $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' ) + ->numParams( count( $toUnwatch ) )->parse(); $this->showTitles( $toUnwatch, $this->successMessage ); } } else { @@ -204,19 +235,35 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { return true; } + public function submitClear( $data ) { + $current = $this->getWatchlist(); + $this->clearWatchlist(); + $this->getUser()->invalidateCache(); + $this->successMessage = $this->msg( 'watchlistedit-clear-done' )->parse(); + $this->successMessage .= ' ' . $this->msg( 'watchlistedit-clear-removed' ) + ->numParams( count( $current ) )->parse(); + $this->showTitles( $current, $this->successMessage ); + + return true; + } + /** * Print out a list of linked titles * * $titles can be an array of strings or Title objects; the former * is preferred, since Titles are very memory-heavy * - * @param array $titles of strings, or Title objects - * @param $output String + * @param array $titles Array of strings, or Title objects + * @param string $output */ private function showTitles( $titles, &$output ) { $talk = $this->msg( 'talkpagelinktext' )->escaped(); // Do a batch existence check $batch = new LinkBatch(); + if ( count( $titles ) >= 100 ) { + $output = wfMessage( 'watchlistedit-too-many' )->parse(); + return; + } foreach ( $titles as $title ) { if ( !$title instanceof Title ) { $title = Title::newFromText( $title ); @@ -300,7 +347,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * * @return array */ - private function getWatchlistInfo() { + protected function getWatchlistInfo() { $titles = array(); $dbr = wfGetDB( DB_MASTER ); @@ -332,7 +379,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * @param Title $title * @param int $namespace * @param string $dbKey - * @return bool: Whether this item is valid + * @return bool Whether this item is valid */ private function checkTitle( $title, $namespace, $dbKey ) { if ( $title @@ -403,7 +450,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * $titles can be an array of strings or Title objects; the former * is preferred, since Titles are very memory-heavy * - * @param array $titles of strings, or Title objects + * @param array $titles Array of strings, or Title objects */ private function watchTitles( $titles ) { $dbw = wfGetDB( DB_MASTER ); @@ -439,7 +486,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * $titles can be an array of strings or Title objects; the former * is preferred, since Titles are very memory-heavy * - * @param array $titles of strings, or Title objects + * @param array $titles Array of strings, or Title objects */ private function unwatchTitles( $titles ) { $dbw = wfGetDB( DB_MASTER ); @@ -506,24 +553,35 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { $fields = array(); $count = 0; - foreach ( $this->getWatchlistInfo() as $namespace => $pages ) { - if ( $namespace >= 0 ) { - $fields['TitlesNs' . $namespace] = array( - 'class' => 'EditWatchlistCheckboxSeriesField', - 'options' => array(), - 'section' => "ns$namespace", - ); - } + // Allow subscribers to manipulate the list of watched pages (or use it + // to preload lots of details at once) + $watchlistInfo = $this->getWatchlistInfo(); + wfRunHooks( + 'WatchlistEditorBeforeFormRender', + array( &$watchlistInfo ) + ); + + foreach ( $watchlistInfo as $namespace => $pages ) { + $options = array(); foreach ( array_keys( $pages ) as $dbkey ) { $title = Title::makeTitleSafe( $namespace, $dbkey ); if ( $this->checkTitle( $title, $namespace, $dbkey ) ) { $text = $this->buildRemoveLine( $title ); - $fields['TitlesNs' . $namespace]['options'][$text] = $title->getPrefixedText(); + $options[$text] = $title->getPrefixedText(); $count++; } } + + // checkTitle can filter some options out, avoid empty sections + if ( count( $options ) > 0 ) { + $fields['TitlesNs' . $namespace] = array( + 'class' => 'EditWatchlistCheckboxSeriesField', + 'options' => $options, + 'section' => "ns$namespace", + ); + } } $this->cleanupWatchlist(); @@ -548,10 +606,11 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { } $context = new DerivativeContext( $this->getContext() ); - $context->setTitle( $this->getTitle() ); // Remove subpage + $context->setTitle( $this->getPageTitle() ); // Remove subpage $form = new EditWatchlistNormalHTMLForm( $fields, $context ); $form->setSubmitTextMsg( 'watchlistedit-normal-submit' ); - # Used message keys: 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit' + # Used message keys: + # 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit' $form->setSubmitTooltip( 'watchlistedit-normal-submit' ); $form->setWrapperLegendMsg( 'watchlistedit-normal-legend' ); $form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() ); @@ -563,21 +622,16 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { /** * Build the label for a checkbox, with a link to the title, and various additional bits * - * @param $title Title + * @param Title $title * @return string */ private function buildRemoveLine( $title ) { $link = Linker::link( $title ); - if ( $title->isRedirect() ) { - // Linker already makes class mw-redirect, so this is redundant - $link = '' . $link . ''; - } - - $tools[] = Linker::link( $title->getTalkPage(), $this->msg( 'talkpagelinktext' )->escaped() ); + $tools['talk'] = Linker::link( $title->getTalkPage(), $this->msg( 'talkpagelinktext' )->escaped() ); if ( $title->exists() ) { - $tools[] = Linker::linkKnown( + $tools['history'] = Linker::linkKnown( $title, $this->msg( 'history_short' )->escaped(), array(), @@ -586,13 +640,21 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { } if ( $title->getNamespace() == NS_USER && !$title->isSubpage() ) { - $tools[] = Linker::linkKnown( + $tools['contributions'] = Linker::linkKnown( SpecialPage::getTitleFor( 'Contributions', $title->getText() ), $this->msg( 'contributions' )->escaped() ); } - wfRunHooks( 'WatchlistEditorBuildRemoveLine', array( &$tools, $title, $title->isRedirect(), $this->getSkin() ) ); + wfRunHooks( + 'WatchlistEditorBuildRemoveLine', + array( &$tools, $title, $title->isRedirect(), $this->getSkin(), &$link ) + ); + + if ( $title->isRedirect() ) { + // Linker already makes class mw-redirect, so this is redundant + $link = '' . $link . ''; + } return $link . " (" . $this->getLanguage()->pipeList( $tools ) . ")"; } @@ -612,7 +674,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { ), ); $context = new DerivativeContext( $this->getContext() ); - $context->setTitle( $this->getTitle( 'raw' ) ); // Reset subpage + $context->setTitle( $this->getPageTitle( 'raw' ) ); // Reset subpage $form = new HTMLForm( $fields, $context ); $form->setSubmitTextMsg( 'watchlistedit-raw-submit' ); # Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit' @@ -624,12 +686,31 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { return $form; } + /** + * Get a form for clearing the watchlist + * + * @return HTMLForm + */ + protected function getClearForm() { + $context = new DerivativeContext( $this->getContext() ); + $context->setTitle( $this->getPageTitle( 'clear' ) ); // Reset subpage + $form = new HTMLForm( array(), $context ); + $form->setSubmitTextMsg( 'watchlistedit-clear-submit' ); + # Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit' + $form->setSubmitTooltip( 'watchlistedit-clear-submit' ); + $form->setWrapperLegendMsg( 'watchlistedit-clear-legend' ); + $form->addHeaderText( $this->msg( 'watchlistedit-clear-explain' )->parse() ); + $form->setSubmitCallback( array( $this, 'submitClear' ) ); + + return $form; + } + /** * Determine whether we are editing the watchlist, and if so, what * kind of editing operation * - * @param $request WebRequest - * @param $par mixed + * @param WebRequest $request + * @param string $par * @return int */ public static function getMode( $request, $par ) { @@ -654,7 +735,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * Build a set of links for convenient navigation * between watchlist viewing and editing modes * - * @param $unused + * @param null $unused * @return string */ public static function buildTools( $unused ) { @@ -665,6 +746,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { 'view' => array( 'Watchlist', false ), 'edit' => array( 'EditWatchlist', false ), 'raw' => array( 'EditWatchlist', 'raw' ), + 'clear' => array( 'EditWatchlist', 'clear' ), ); foreach ( $modes as $mode => $arr ) { @@ -683,10 +765,6 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { } } -# B/C since 1.18 -class WatchlistEditor extends SpecialEditWatchlist { -} - /** * Extend HTMLForm purely so we can have a more sane way of getting the section headers */ @@ -712,9 +790,9 @@ class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField { * form is open (bug 32126), but we know that invalid items will * be harmless so we can override it here. * - * @param string $value the value the field was submitted with - * @param array $alldata the data collected from the form - * @return Mixed Bool true on success, or String error to display. + * @param string $value The value the field was submitted with + * @param array $alldata The data collected from the form + * @return bool|string Bool true on success, or String error to display. */ function validate( $value, $alldata ) { // Need to call into grandparent to be a good citizen. :) diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index 2e90d996..20532a92 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -113,7 +113,8 @@ class SpecialEmailUser extends UnlistedSpecialPage { // error out if sending user cannot do this $error = self::getPermissionsError( $this->getUser(), - $this->getRequest()->getVal( 'wpEditToken' ) + $this->getRequest()->getVal( 'wpEditToken' ), + $this->getConfig() ); switch ( $error ) { @@ -138,18 +139,19 @@ class SpecialEmailUser extends UnlistedSpecialPage { $ret = self::getTarget( $this->mTarget ); if ( !$ret instanceof User ) { if ( $this->mTarget != '' ) { + // Messages used here: notargettext, noemailtext, nowikiemailtext $ret = ( $ret == 'notarget' ) ? 'emailnotarget' : ( $ret . 'text' ); $out->wrapWikiMsg( "

    $1

    ", $ret ); } $out->addHTML( $this->userForm( $this->mTarget ) ); - return false; + return; } $this->mTargetObj = $ret; $context = new DerivativeContext( $this->getContext() ); - $context->setTitle( $this->getTitle() ); // Remove subpage + $context->setTitle( $this->getPageTitle() ); // Remove subpage $form = new HTMLForm( $this->getFormFields(), $context ); // By now we are supposed to be sure that $this->mTarget is a user name $form->addPreText( $this->msg( 'emailpagetext', $this->mTarget )->parse() ); @@ -159,7 +161,7 @@ class SpecialEmailUser extends UnlistedSpecialPage { $form->loadData(); if ( !wfRunHooks( 'EmailUserForm', array( &$form ) ) ) { - return false; + return; } $result = $form->show(); @@ -174,8 +176,8 @@ class SpecialEmailUser extends UnlistedSpecialPage { /** * Validate target User * - * @param string $target target user name - * @return User object on success or a string on error + * @param string $target Target user name + * @return User User object on success or a string on error */ public static function getTarget( $target ) { if ( $target == '' ) { @@ -205,14 +207,17 @@ class SpecialEmailUser extends UnlistedSpecialPage { /** * Check whether a user is allowed to send email * - * @param $user User object - * @param string $editToken edit token - * @return null on success or string on error + * @param User $user + * @param string $editToken Edit token + * @param Config $config optional for backwards compatibility + * @return string|null Null on success or string on error */ - public static function getPermissionsError( $user, $editToken ) { - global $wgEnableEmail, $wgEnableUserEmail; - - if ( !$wgEnableEmail || !$wgEnableUserEmail ) { + public static function getPermissionsError( $user, $editToken, Config $config = null ) { + if ( $config === null ) { + wfDebug( __METHOD__ . ' called without a Config instance passed to it' ); + $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); + } + if ( !$config->get( 'EnableEmail' ) || !$config->get( 'EnableUserEmail' ) ) { return 'usermaildisabled'; } @@ -251,16 +256,15 @@ class SpecialEmailUser extends UnlistedSpecialPage { /** * Form to ask for target user name. * - * @param string $name user name submitted. - * @return String: form asking for user name. + * @param string $name User name submitted. + * @return string Form asking for user name. */ protected function userForm( $name ) { - global $wgScript; $string = Xml::openElement( 'form', - array( 'method' => 'get', 'action' => $wgScript, 'id' => 'askusername' ) + array( 'method' => 'get', 'action' => wfScript(), 'id' => 'askusername' ) ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . + Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) . Xml::openElement( 'fieldset' ) . Html::rawElement( 'legend', null, $this->msg( 'emailtarget' )->parse() ) . Xml::inputLabel( @@ -282,8 +286,8 @@ class SpecialEmailUser extends UnlistedSpecialPage { * Submit callback for an HTMLForm object, will simply call submit(). * * @since 1.20 - * @param $data array - * @param $form HTMLForm object + * @param array $data + * @param HTMLForm $form * @return Status|string|bool */ public static function uiSubmit( array $data, HTMLForm $form ) { @@ -297,19 +301,20 @@ class SpecialEmailUser extends UnlistedSpecialPage { * * @param array $data * @param IContextSource $context - * @return Mixed: Status object, or potentially a String on error + * @return Status|string|bool Status object, or potentially a String on error * or maybe even true on success if anything uses the EmailUser hook. */ public static function submit( array $data, IContextSource $context ) { - global $wgUserEmailUseReplyTo; + $config = $context->getConfig(); $target = self::getTarget( $data['Target'] ); if ( !$target instanceof User ) { + // Messages used here: notargettext, noemailtext, nowikiemailtext return $context->msg( $target . 'text' )->parseAsBlock(); } - $to = new MailAddress( $target ); - $from = new MailAddress( $context->getUser() ); + $to = MailAddress::newFromUser( $target ); + $from = MailAddress::newFromUser( $context->getUser() ); $subject = $data['Subject']; $text = $data['Text']; @@ -323,16 +328,15 @@ class SpecialEmailUser extends UnlistedSpecialPage { return $error; } - if ( $wgUserEmailUseReplyTo ) { + if ( $config->get( 'UserEmailUseReplyTo' ) ) { // Put the generic wiki autogenerated address in the From: // header and reserve the user for Reply-To. // // This is a bit ugly, but will serve to differentiate // wiki-borne mails from direct mails and protects against // SPF and bounce problems with some mailers (see below). - global $wgPasswordSender, $wgPasswordSenderName; - - $mailFrom = new MailAddress( $wgPasswordSender, $wgPasswordSenderName ); + $mailFrom = new MailAddress( $config->get( 'PasswordSender' ), + wfMessage( 'emailsender' )->inContentLanguage()->text() ); $replyTo = $from; } else { // Put the sending user's e-mail address in the From: header. diff --git a/includes/specials/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php new file mode 100644 index 00000000..62f957fc --- /dev/null +++ b/includes/specials/SpecialExpandTemplates.php @@ -0,0 +1,286 @@ + tags in the expanded wikitext */ + protected $removeNowiki; + + /** @var int Maximum size in bytes to include. 50MB allows fixing those huge pages */ + const MAX_INCLUDE_SIZE = 50000000; + + function __construct() { + parent::__construct( 'ExpandTemplates' ); + } + + /** + * Show the special page + * @param string|null $subpage + */ + function execute( $subpage ) { + global $wgParser; + + $this->setHeaders(); + + $request = $this->getRequest(); + $titleStr = $request->getText( 'wpContextTitle' ); + $title = Title::newFromText( $titleStr ); + + if ( !$title ) { + $title = $this->getPageTitle(); + } + $input = $request->getText( 'wpInput' ); + $this->generateXML = $request->getBool( 'wpGenerateXml' ); + $this->generateRawHtml = $request->getBool( 'wpGenerateRawHtml' ); + + if ( strlen( $input ) ) { + $this->removeComments = $request->getBool( 'wpRemoveComments', false ); + $this->removeNowiki = $request->getBool( 'wpRemoveNowiki', false ); + $options = ParserOptions::newFromContext( $this->getContext() ); + $options->setRemoveComments( $this->removeComments ); + $options->setTidy( true ); + $options->setMaxIncludeSize( self::MAX_INCLUDE_SIZE ); + + if ( $this->generateXML ) { + $wgParser->startExternalParse( $title, $options, OT_PREPROCESS ); + $dom = $wgParser->preprocessToDom( $input ); + + if ( method_exists( $dom, 'saveXML' ) ) { + $xml = $dom->saveXML(); + } else { + $xml = $dom->__toString(); + } + } + + $output = $wgParser->preprocess( $input, $title, $options ); + } else { + $this->removeComments = $request->getBool( 'wpRemoveComments', true ); + $this->removeNowiki = $request->getBool( 'wpRemoveNowiki', false ); + $output = false; + } + + $out = $this->getOutput(); + $out->addWikiMsg( 'expand_templates_intro' ); + $out->addHTML( $this->makeForm( $titleStr, $input ) ); + + if ( $output !== false ) { + if ( $this->generateXML && strlen( $output ) > 0 ) { + $out->addHTML( $this->makeOutput( $xml, 'expand_templates_xml_output' ) ); + } + + $tmp = $this->makeOutput( $output ); + + if ( $this->removeNowiki ) { + $tmp = preg_replace( + array( '_<nowiki>_', '_</nowiki>_', '_<nowiki */>_' ), + '', + $tmp + ); + } + + $config = $this->getConfig(); + if ( ( $config->get( 'UseTidy' ) && $options->getTidy() ) || $config->get( 'AlwaysUseTidy' ) ) { + $tmp = MWTidy::tidy( $tmp ); + } + + $out->addHTML( $tmp ); + + $pout = $this->generateHtml( $title, $output ); + $rawhtml = $pout->getText(); + if ( $this->generateRawHtml && strlen( $rawhtml ) > 0 ) { + $out->addHTML( $this->makeOutput( $rawhtml, 'expand_templates_html_output' ) ); + } + + $this->showHtmlPreview( $title, $pout, $out ); + } + } + + /** + * Generate a form allowing users to enter information + * + * @param string $title Value for context title field + * @param string $input Value for input textbox + * @return string + */ + private function makeForm( $title, $input ) { + $self = $this->getPageTitle(); + $request = $this->getRequest(); + $user = $this->getUser(); + + $form = Xml::openElement( + 'form', + array( 'method' => 'post', 'action' => $self->getLocalUrl() ) + ); + $form .= "
    " . $this->msg( 'expandtemplates' )->escaped() . "\n"; + + $form .= '

    ' . Xml::inputLabel( + $this->msg( 'expand_templates_title' )->plain(), + 'wpContextTitle', + 'contexttitle', + 60, + $title, + array( 'autofocus' => true ) + ) . '

    '; + $form .= '

    ' . Xml::label( + $this->msg( 'expand_templates_input' )->text(), + 'input' + ) . '

    '; + $form .= Xml::textarea( + 'wpInput', + $input, + 10, + 10, + array( 'id' => 'input' ) + ); + + $form .= '

    ' . Xml::checkLabel( + $this->msg( 'expand_templates_remove_comments' )->text(), + 'wpRemoveComments', + 'removecomments', + $this->removeComments + ) . '

    '; + $form .= '

    ' . Xml::checkLabel( + $this->msg( 'expand_templates_remove_nowiki' )->text(), + 'wpRemoveNowiki', + 'removenowiki', + $this->removeNowiki + ) . '

    '; + $form .= '

    ' . Xml::checkLabel( + $this->msg( 'expand_templates_generate_xml' )->text(), + 'wpGenerateXml', + 'generate_xml', + $this->generateXML + ) . '

    '; + $form .= '

    ' . Xml::checkLabel( + $this->msg( 'expand_templates_generate_rawhtml' )->text(), + 'wpGenerateRawHtml', + 'generate_rawhtml', + $this->generateRawHtml + ) . '

    '; + $form .= '

    ' . Xml::submitButton( + $this->msg( 'expand_templates_ok' )->text(), + array( 'accesskey' => 's' ) + ) . '

    '; + $form .= "
    \n"; + $form .= Html::hidden( 'wpEditToken', $user->getEditToken( '', $request ) ); + $form .= Xml::closeElement( 'form' ); + + return $form; + } + + /** + * Generate a nice little box with a heading for output + * + * @param string $output Wiki text output + * @param string $heading + * @return string + */ + private function makeOutput( $output, $heading = 'expand_templates_output' ) { + $out = "

    " . $this->msg( $heading )->escaped() . "

    \n"; + $out .= Xml::textarea( + 'output', + $output, + 10, + 10, + array( 'id' => 'output', 'readonly' => 'readonly' ) + ); + + return $out; + } + + /** + * Renders the supplied wikitext as html + * + * @param Title $title + * @param string $text + * @return ParserOutput + */ + private function generateHtml( Title $title, $text ) { + global $wgParser; + + $popts = ParserOptions::newFromContext( $this->getContext() ); + $popts->setTargetLanguage( $title->getPageLanguage() ); + return $wgParser->parse( $text, $title, $popts ); + } + + /** + * Wraps the provided html code in a div and outputs it to the page + * + * @param Title $title + * @param ParserOutput $pout + * @param OutputPage $out + */ + private function showHtmlPreview( Title $title, ParserOutput $pout, OutputPage $out ) { + $lang = $title->getPageViewLanguage(); + $out->addHTML( "

    " . $this->msg( 'expand_templates_preview' )->escaped() . "

    \n" ); + + if ( $this->getConfig()->get( 'RawHtml' ) ) { + $request = $this->getRequest(); + $user = $this->getUser(); + + // To prevent cross-site scripting attacks, don't show the preview if raw HTML is + // allowed and a valid edit token is not provided (bug 71111). However, MediaWiki + // does not currently provide logged-out users with CSRF protection; in that case, + // do not show the preview unless anonymous editing is allowed. + if ( $user->isAnon() && !$user->isAllowed( 'edit' ) ) { + $error = array( 'expand_templates_preview_fail_html_anon' ); + } elseif ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ), '', $request ) ) { + $error = array( 'expand_templates_preview_fail_html' ); + } else { + $error = false; + } + + if ( $error ) { + $out->wrapWikiMsg( "
    \n$1\n
    ", $error ); + return; + } + } + + $out->addHTML( Html::openElement( 'div', array( + 'class' => 'mw-content-' . $lang->getDir(), + 'dir' => $lang->getDir(), + 'lang' => $lang->getHtmlCode(), + ) ) ); + $out->addParserOutputContent( $pout ); + $out->addHTML( Html::closeElement( 'div' ) ); + } + + protected function getGroupName() { + return 'wiki'; + } +} diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php index 61ed34d4..38c52a01 100644 --- a/includes/specials/SpecialExport.php +++ b/includes/specials/SpecialExport.php @@ -37,12 +37,9 @@ class SpecialExport extends SpecialPage { } public function execute( $par ) { - global $wgSitename, $wgExportAllowListContributors, $wgExportFromNamespaces; - global $wgExportAllowHistory, $wgExportMaxHistory, $wgExportMaxLinkDepth; - global $wgExportAllowAll; - $this->setHeaders(); $this->outputHeader(); + $config = $this->getConfig(); // Set some variables $this->curonly = true; @@ -74,7 +71,7 @@ class SpecialExport extends SpecialPage { } } } - } elseif ( $request->getCheck( 'addns' ) && $wgExportFromNamespaces ) { + } elseif ( $request->getCheck( 'addns' ) && $config->get( 'ExportFromNamespaces' ) ) { $page = $request->getText( 'pages' ); $nsindex = $request->getText( 'nsindex', '' ); @@ -87,7 +84,7 @@ class SpecialExport extends SpecialPage { $page .= "\n" . implode( "\n", $nspages ); } } - } elseif ( $request->getCheck( 'exportall' ) && $wgExportAllowAll ) { + } elseif ( $request->getCheck( 'exportall' ) && $config->get( 'ExportAllowAll' ) ) { $this->doExport = true; $exportall = true; @@ -108,19 +105,20 @@ class SpecialExport extends SpecialPage { $offset = null; } + $maxHistory = $config->get( 'ExportMaxHistory' ); $limit = $request->getInt( 'limit' ); $dir = $request->getVal( 'dir' ); $history = array( 'dir' => 'asc', 'offset' => false, - 'limit' => $wgExportMaxHistory, + 'limit' => $maxHistory, ); $historyCheck = $request->getCheck( 'history' ); if ( $this->curonly ) { $history = WikiExporter::CURRENT; } elseif ( !$historyCheck ) { - if ( $limit > 0 && ( $wgExportMaxHistory == 0 || $limit < $wgExportMaxHistory ) ) { + if ( $limit > 0 && ( $maxHistory == 0 || $limit < $maxHistory ) ) { $history['limit'] = $limit; } @@ -152,13 +150,13 @@ class SpecialExport extends SpecialPage { } } - if ( !$wgExportAllowHistory ) { + if ( !$config->get( 'ExportAllowHistory' ) ) { // Override $history = WikiExporter::CURRENT; } $list_authors = $request->getCheck( 'listauthors' ); - if ( !$this->curonly || !$wgExportAllowListContributors ) { + if ( !$this->curonly || !$config->get( 'ExportAllowListContributors' ) ) { $list_authors = false; } @@ -172,7 +170,7 @@ class SpecialExport extends SpecialPage { if ( $request->getCheck( 'wpDownload' ) ) { // Provide a sane filename suggestion - $filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' ); + $filename = urlencode( $config->get( 'Sitename' ) . '-' . wfTimestampNow() . '.xml' ); $request->response()->header( "Content-disposition: attachment;filename={$filename}" ); } @@ -185,7 +183,7 @@ class SpecialExport extends SpecialPage { $out->addWikiMsg( 'exporttext' ); $form = Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $this->getTitle()->getLocalURL( 'action=submit' ) ) ); + 'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ) ) ); $form .= Xml::inputLabel( $this->msg( 'export-addcattext' )->text(), 'catname', @@ -197,7 +195,7 @@ class SpecialExport extends SpecialPage { array( 'name' => 'addcat' ) ) . '
    '; - if ( $wgExportFromNamespaces ) { + if ( $config->get( 'ExportFromNamespaces' ) ) { $form .= Html::namespaceSelector( array( 'selected' => $nsindex, @@ -214,7 +212,7 @@ class SpecialExport extends SpecialPage { ) . '
    '; } - if ( $wgExportAllowAll ) { + if ( $config->get( 'ExportAllowAll' ) ) { $form .= Xml::checkLabel( $this->msg( 'exportall' )->text(), 'exportall', @@ -231,7 +229,7 @@ class SpecialExport extends SpecialPage { ); $form .= '
    '; - if ( $wgExportAllowHistory ) { + if ( $config->get( 'ExportAllowHistory' ) ) { $form .= Xml::checkLabel( $this->msg( 'exportcuronly' )->text(), 'curonly', @@ -249,7 +247,7 @@ class SpecialExport extends SpecialPage { $request->wasPosted() ? $request->getCheck( 'templates' ) : false ) . '
    '; - if ( $wgExportMaxLinkDepth || $this->userCanOverrideExportDepth() ) { + if ( $config->get( 'ExportMaxLinkDepth' ) || $this->userCanOverrideExportDepth() ) { $form .= Xml::inputLabel( $this->msg( 'export-pagelinks' )->text(), 'pagelink-depth', @@ -259,8 +257,14 @@ class SpecialExport extends SpecialPage { ) . '
    '; } - // Enable this when we can do something useful exporting/importing image information. :) - //$form .= Xml::checkLabel( $this->msg( 'export-images' )->text(), 'images', 'wpExportImages', false ) . '
    '; + /* Enable this when we can do something useful exporting/importing image information. + $form .= Xml::checkLabel( + $this->msg( 'export-images' )->text(), + 'images', + 'wpExportImages', + false + ) . '
    '; + */ $form .= Xml::checkLabel( $this->msg( 'export-download' )->text(), 'wpDownload', @@ -268,7 +272,7 @@ class SpecialExport extends SpecialPage { $request->wasPosted() ? $request->getCheck( 'wpDownload' ) : true ) . '
    '; - if ( $wgExportAllowListContributors ) { + if ( $config->get( 'ExportAllowListContributors' ) ) { $form .= Xml::checkLabel( $this->msg( 'exportlistauthors' )->text(), 'listauthors', @@ -296,11 +300,11 @@ class SpecialExport extends SpecialPage { /** * Do the actual page exporting * - * @param string $page user input on what page(s) to export - * @param $history Mixed: one of the WikiExporter history export constants - * @param $list_authors Boolean: Whether to add distinct author list (when - * not returning full history) - * @param $exportall Boolean: Whether to export everything + * @param string $page User input on what page(s) to export + * @param int $history One of the WikiExporter history export constants + * @param bool $list_authors Whether to add distinct author list (when + * not returning full history) + * @param bool $exportall Whether to export everything */ private function doExport( $page, $history, $list_authors, $exportall ) { @@ -315,7 +319,7 @@ class SpecialExport extends SpecialPage { foreach ( explode( "\n", $page ) as $pageName ) { $pageName = trim( $pageName ); $title = Title::newFromText( $pageName ); - if ( $title && $title->getInterwiki() == '' && $title->getText() !== '' ) { + if ( $title && !$title->isExternal() && $title->getText() !== '' ) { // Only record each page once! $pageSet[$title->getPrefixedText()] = true; } @@ -397,7 +401,7 @@ class SpecialExport extends SpecialPage { } /** - * @param $title Title + * @param Title $title * @return array */ private function getPagesFromCategory( $title ) { @@ -430,7 +434,7 @@ class SpecialExport extends SpecialPage { } /** - * @param $nsindex int + * @param int $nsindex * @return array */ private function getPagesFromNamespace( $nsindex ) { @@ -463,9 +467,9 @@ class SpecialExport extends SpecialPage { /** * Expand a list of pages to include templates used in those pages. - * @param $inputPages array, list of titles to look up - * @param $pageSet array, associative array indexed by titles for output - * @return array associative array index by titles + * @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 getTemplates( $inputPages, $pageSet ) { return $this->getLinks( $inputPages, $pageSet, @@ -477,19 +481,18 @@ class SpecialExport extends SpecialPage { /** * Validate link depth setting, if available. - * @param $depth int + * @param int $depth * @return int */ private function validateLinkDepth( $depth ) { - global $wgExportMaxLinkDepth; - if ( $depth < 0 ) { return 0; } if ( !$this->userCanOverrideExportDepth() ) { - if ( $depth > $wgExportMaxLinkDepth ) { - return $wgExportMaxLinkDepth; + $maxLinkDepth = $this->getConfig()->get( 'ExportMaxLinkDepth' ); + if ( $depth > $maxLinkDepth ) { + return $maxLinkDepth; } } @@ -504,13 +507,15 @@ class SpecialExport extends SpecialPage { /** * Expand a list of pages to include pages linked to from that page. - * @param $inputPages array - * @param $pageSet array - * @param $depth int + * @param array $inputPages + * @param array $pageSet + * @param int $depth * @return array */ private function getPageLinks( $inputPages, $pageSet, $depth ) { + // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect for ( ; $depth > 0; --$depth ) { + // @codingStandardsIgnoreEnd $pageSet = $this->getLinks( $inputPages, $pageSet, 'pagelinks', array( 'namespace' => 'pl_namespace', 'title' => 'pl_title' ), @@ -525,10 +530,10 @@ class SpecialExport extends SpecialPage { /** * Expand a list of pages to include images used in those pages. * - * @param $inputPages array, list of titles to look up - * @param $pageSet array, associative array indexed by titles for output + * @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 + * @return array Associative array index by titles */ private function getImages( $inputPages, $pageSet ) { return $this->getLinks( diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php index 47a4d75f..dc9d57c2 100644 --- a/includes/specials/SpecialFewestrevisions.php +++ b/includes/specials/SpecialFewestrevisions.php @@ -71,7 +71,7 @@ class FewestrevisionsPage extends QueryPage { /** * @param Skin $skin * @param object $result Database row - * @return String + * @return string */ function formatResult( $skin, $result ) { global $wgContLang; diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php index 4c6593b2..fc26c903 100644 --- a/includes/specials/SpecialFileDuplicateSearch.php +++ b/includes/specials/SpecialFileDuplicateSearch.php @@ -59,7 +59,7 @@ class FileDuplicateSearchPage extends QueryPage { /** * Fetch dupes from all connected file repositories. * - * @return array of File objects + * @return array Array of File objects */ function getDupes() { return RepoGroup::singleton()->findBySha1( $this->hash ); @@ -67,7 +67,7 @@ class FileDuplicateSearchPage extends QueryPage { /** * - * @param array $dupes of File objects + * @param array $dupes Array of File objects */ function showList( $dupes ) { $html = array(); @@ -96,12 +96,10 @@ class FileDuplicateSearchPage extends QueryPage { } function execute( $par ) { - global $wgScript; - $this->setHeaders(); $this->outputHeader(); - $this->filename = isset( $par ) ? $par : $this->getRequest()->getText( 'filename' ); + $this->filename = $par !== null ? $par : $this->getRequest()->getText( 'filename' ); $this->file = null; $this->hash = ''; $title = Title::newFromText( $this->filename, NS_FILE ); @@ -115,9 +113,9 @@ class FileDuplicateSearchPage extends QueryPage { $out->addHTML( Html::openElement( 'form', - array( 'id' => 'fileduplicatesearch', 'method' => 'get', 'action' => $wgScript ) + array( 'id' => 'fileduplicatesearch', 'method' => 'get', 'action' => wfScript() ) ) . "\n" . - Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n" . + Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . "\n" . Html::openElement( 'fieldset' ) . "\n" . Html::element( 'legend', null, $this->msg( 'fileduplicatesearch-legend' )->text() ) . "\n" . Xml::inputLabel( diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php index e7ced52a..5860f636 100644 --- a/includes/specials/SpecialFilepath.php +++ b/includes/specials/SpecialFilepath.php @@ -36,7 +36,13 @@ class SpecialFilepath extends RedirectSpecialPage { // implement by redirecting through Special:Redirect/file function getRedirect( $par ) { $file = $par ?: $this->getRequest()->getText( 'file' ); - return SpecialPage::getSafeTitleFor( 'Redirect', 'file/' . $file ); + + if ( $file ) { + $argument = "file/$file"; + } else { + $argument = 'file'; + } + return SpecialPage::getSafeTitleFor( 'Redirect', $argument ); } protected function getGroupName() { diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php index d7d860de..eab4784c 100644 --- a/includes/specials/SpecialImport.php +++ b/includes/specials/SpecialImport.php @@ -3,7 +3,7 @@ * Implements Special:Import * * Copyright © 2003,2005 Brion Vibber - * http://www.mediawiki.org/ + * https://www.mediawiki.org/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -31,6 +31,8 @@ */ class SpecialImport extends SpecialPage { private $interwiki = false; + private $subproject; + private $fullInterwikiPrefix; private $namespace; private $rootpage = ''; private $frompage = ''; @@ -44,17 +46,19 @@ class SpecialImport extends SpecialPage { */ public function __construct() { parent::__construct( 'Import', 'import' ); - global $wgImportTargetNamespace; - $this->namespace = $wgImportTargetNamespace; + $this->namespace = $this->getConfig()->get( 'ImportTargetNamespace' ); } /** * Execute + * @param string|null $par */ function execute( $par ) { $this->setHeaders(); $this->outputHeader(); + $this->getOutput()->addModules( 'mediawiki.special.import' ); + $user = $this->getUser(); if ( !$user->isAllowedAny( 'import', 'importupload' ) ) { throw new PermissionsError( 'import' ); @@ -64,11 +68,11 @@ class SpecialImport extends SpecialPage { # @todo FIXME: Title::checkSpecialsAndNSPermissions() has a very wierd expectation of what # getUserPermissionsErrors() might actually be used for, hence the 'ns-specialprotected' $errors = wfMergeErrorArrays( - $this->getTitle()->getUserPermissionsErrors( + $this->getPageTitle()->getUserPermissionsErrors( 'import', $user, true, array( 'ns-specialprotected', 'badaccess-group0', 'badaccess-groups' ) ), - $this->getTitle()->getUserPermissionsErrors( + $this->getPageTitle()->getUserPermissionsErrors( 'importupload', $user, true, array( 'ns-specialprotected', 'badaccess-group0', 'badaccess-groups' ) ) @@ -91,15 +95,15 @@ class SpecialImport extends SpecialPage { * Do the actual import */ private function doImport() { - global $wgImportSources, $wgExportMaxLinkDepth; - $isUpload = false; $request = $this->getRequest(); $this->namespace = $request->getIntOrNull( 'namespace' ); $sourceName = $request->getVal( "source" ); $this->logcomment = $request->getText( 'log-comment' ); - $this->pageLinkDepth = $wgExportMaxLinkDepth == 0 ? 0 : $request->getIntOrNull( 'pagelink-depth' ); + $this->pageLinkDepth = $this->getConfig()->get( 'ExportMaxLinkDepth' ) == 0 + ? 0 + : $request->getIntOrNull( 'pagelink-depth' ); $this->rootpage = $request->getText( 'rootpage' ); $user = $this->getUser(); @@ -116,19 +120,30 @@ class SpecialImport extends SpecialPage { if ( !$user->isAllowed( 'import' ) ) { throw new PermissionsError( 'import' ); } - $this->interwiki = $request->getVal( 'interwiki' ); - if ( !in_array( $this->interwiki, $wgImportSources ) ) { + $this->interwiki = $this->fullInterwikiPrefix = $request->getVal( 'interwiki' ); + // does this interwiki have subprojects? + $importSources = $this->getConfig()->get( 'ImportSources' ); + $hasSubprojects = array_key_exists( $this->interwiki, $importSources ); + if ( !$hasSubprojects && !in_array( $this->interwiki, $importSources ) ) { $source = Status::newFatal( "import-invalid-interwiki" ); } else { - $this->history = $request->getCheck( 'interwikiHistory' ); - $this->frompage = $request->getText( "frompage" ); - $this->includeTemplates = $request->getCheck( 'interwikiTemplates' ); - $source = ImportStreamSource::newFromInterwiki( - $this->interwiki, - $this->frompage, - $this->history, - $this->includeTemplates, - $this->pageLinkDepth ); + if ( $hasSubprojects ) { + $this->subproject = $request->getVal( 'subproject' ); + $this->fullInterwikiPrefix .= ':' . $request->getVal( 'subproject' ); + } + if ( $hasSubprojects && !in_array( $this->subproject, $importSources[$this->interwiki] ) ) { + $source = Status::newFatal( "import-invalid-interwiki" ); + } else { + $this->history = $request->getCheck( 'interwikiHistory' ); + $this->frompage = $request->getText( "frompage" ); + $this->includeTemplates = $request->getCheck( 'interwikiTemplates' ); + $source = ImportStreamSource::newFromInterwiki( + $this->fullInterwikiPrefix, + $this->frompage, + $this->history, + $this->includeTemplates, + $this->pageLinkDepth ); + } } } else { $source = Status::newFatal( "importunknownsource" ); @@ -166,7 +181,7 @@ class SpecialImport extends SpecialPage { $reporter = new ImportReporter( $importer, $isUpload, - $this->interwiki, + $this->fullInterwikiPrefix, $this->logcomment ); $reporter->setContext( $this->getContext() ); @@ -201,11 +216,10 @@ class SpecialImport extends SpecialPage { } private function showForm() { - global $wgImportSources, $wgExportMaxLinkDepth; - - $action = $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ); + $action = $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ); $user = $this->getUser(); $out = $this->getOutput(); + $importSources = $this->getConfig()->get( 'ImportSources' ); if ( $user->isAllowed( 'importupload' ) ) { $out->addHTML( @@ -242,7 +256,10 @@ class SpecialImport extends SpecialPage { " . - Xml::label( $this->msg( 'import-interwiki-rootpage' )->text(), 'mw-interwiki-rootpage-upload' ) . + Xml::label( + $this->msg( 'import-interwiki-rootpage' )->text(), + 'mw-interwiki-rootpage-upload' + ) . " " . Xml::input( 'rootpage', 50, $this->rootpage, @@ -261,15 +278,15 @@ class SpecialImport extends SpecialPage { Xml::closeElement( 'fieldset' ) ); } else { - if ( empty( $wgImportSources ) ) { + if ( empty( $importSources ) ) { $out->addWikiMsg( 'importnosources' ); } } - if ( $user->isAllowed( 'import' ) && !empty( $wgImportSources ) ) { + if ( $user->isAllowed( 'import' ) && !empty( $importSources ) ) { # Show input field for import depth only if $wgExportMaxLinkDepth > 0 $importDepth = ''; - if ( $wgExportMaxLinkDepth > 0 ) { + if ( $this->getConfig()->get( 'ExportMaxLinkDepth' ) > 0 ) { $importDepth = " " . $this->msg( 'export-pagelinks' )->parse() . @@ -297,7 +314,7 @@ class SpecialImport extends SpecialPage { Xml::openElement( 'table', array( 'id' => 'mw-import-table-interwiki' ) ) . " " . - Xml::label( $this->msg( 'import-interwiki-source' )->text(), 'interwiki' ) . + Xml::label( $this->msg( 'import-interwiki-sourcewiki' )->text(), 'interwiki' ) . " " . Xml::openElement( @@ -306,13 +323,63 @@ class SpecialImport extends SpecialPage { ) ); - foreach ( $wgImportSources as $prefix ) { - $selected = ( $this->interwiki === $prefix ) ? ' selected="selected"' : ''; - $out->addHTML( Xml::option( $prefix, $prefix, $selected ) ); + $needSubprojectField = false; + foreach ( $importSources as $key => $value ) { + if ( is_int( $key ) ) { + $key = $value; + } elseif ( $value !== $key ) { + $needSubprojectField = true; + } + + $attribs = array( + 'value' => $key, + ); + if ( is_array( $value ) ) { + $attribs['data-subprojects'] = implode( ' ', $value ); + } + if ( $this->interwiki === $key ) { + $attribs['selected'] = 'selected'; + } + $out->addHTML( Html::element( 'option', $attribs, $key ) ); + } + + $out->addHTML( + Xml::closeElement( 'select' ) + ); + + if ( $needSubprojectField ) { + $out->addHTML( + Xml::openElement( + 'select', + array( 'name' => 'subproject', 'id' => 'subproject' ) + ) + ); + + $subprojectsToAdd = array(); + foreach ( $importSources as $key => $value ) { + if ( is_array( $value ) ) { + $subprojectsToAdd = array_merge( $subprojectsToAdd, $value ); + } + } + $subprojectsToAdd = array_unique( $subprojectsToAdd ); + sort( $subprojectsToAdd ); + foreach ( $subprojectsToAdd as $subproject ) { + $out->addHTML( Xml::option( $subproject, $subproject, $this->subproject === $subproject ) ); + } + + $out->addHTML( + Xml::closeElement( 'select' ) + ); } $out->addHTML( - Xml::closeElement( 'select' ) . + " + + + " . + Xml::label( $this->msg( 'import-interwiki-sourcepage' )->text(), 'frompage' ) . + " + " . Xml::input( 'frompage', 50, $this->frompage, array( 'id' => 'frompage' ) ) . " @@ -411,11 +478,10 @@ class ImportReporter extends ContextSource { private $mOriginalPageOutCallback = null; private $mLogItemCount = 0; - /** * @param WikiImporter $importer - * @param $upload - * @param $interwiki + * @param bool $upload + * @param string $interwiki * @param string|bool $reason */ function __construct( $importer, $upload, $interwiki, $reason = false ) { @@ -435,7 +501,9 @@ class ImportReporter extends ContextSource { } function reportNotice( $msg, array $params ) { - $this->getOutput()->addHTML( Html::element( 'li', array(), $this->msg( $msg, $params )->text() ) ); + $this->getOutput()->addHTML( + Html::element( 'li', array(), $this->msg( $msg, $params )->text() ) + ); } function reportLogItem( /* ... */ ) { @@ -449,8 +517,8 @@ class ImportReporter extends ContextSource { * @param Title $title * @param Title $origTitle * @param int $revisionCount - * @param $successCount - * @param $pageInfo + * @param int $successCount + * @param array $pageInfo * @return void */ function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) { @@ -476,7 +544,8 @@ class ImportReporter extends ContextSource { $detail = $this->msg( 'import-logentry-upload-detail' )->numParams( $successCount )->inContentLanguage()->text(); if ( $this->reason ) { - $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason; + $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() + . $this->reason; } $log->addEntry( 'upload', $title, $detail, array(), $this->getUser() ); } else { @@ -485,7 +554,8 @@ class ImportReporter extends ContextSource { $detail = $this->msg( 'import-logentry-interwiki-detail' )->numParams( $successCount )->params( $interwiki )->inContentLanguage()->text(); if ( $this->reason ) { - $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->reason; + $detail .= $this->msg( 'colon-separator' )->inContentLanguage()->text() + . $this->reason; } $log->addEntry( 'interwiki', $title, $detail, array(), $this->getUser() ); } @@ -493,13 +563,23 @@ class ImportReporter extends ContextSource { $comment = $detail; // quick $dbw = wfGetDB( DB_MASTER ); $latest = $title->getLatestRevID(); - $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleID(), $comment, true ); + $nullRevision = Revision::newNullRevision( + $dbw, + $title->getArticleID(), + $comment, + true, + $this->getUser() + ); + if ( !is_null( $nullRevision ) ) { $nullRevision->insertOn( $dbw ); $page = WikiPage::factory( $title ); # Update page record $page->updateRevisionOn( $dbw, $nullRevision ); - wfRunHooks( 'NewRevisionFromEditComplete', array( $page, $nullRevision, $latest, $this->getUser() ) ); + wfRunHooks( + 'NewRevisionFromEditComplete', + array( $page, $nullRevision, $latest, $this->getUser() ) + ); } } else { $this->getOutput()->addHTML( "
  • " . Linker::linkKnown( $title ) . " " . diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php index 7069d527..0efebb3e 100644 --- a/includes/specials/SpecialJavaScriptTest.php +++ b/includes/specials/SpecialJavaScriptTest.php @@ -25,13 +25,12 @@ * @ingroup SpecialPage */ class SpecialJavaScriptTest extends SpecialPage { - /** - * @var $frameworks Array: Mapping of framework ids and their initilizer methods + * @var array Mapping of framework ids and their initilizer methods * in this class. If a framework is requested but not in this array, * the 'unknownframework' error is served. */ - static $frameworks = array( + private static $frameworks = array( 'qunit' => 'initQUnitTesting', ); @@ -68,7 +67,7 @@ class SpecialJavaScriptTest extends SpecialPage { $this->msg( "javascripttest-$framework-name" )->plain() ) ); $out->setSubtitle( $this->msg( 'javascripttest-backlink' ) - ->rawParams( Linker::linkKnown( $this->getTitle() ) ) ); + ->rawParams( Linker::linkKnown( $this->getPageTitle() ) ) ); $this->{self::$frameworks[$framework]}(); } else { // Framework not found, display error @@ -97,7 +96,7 @@ class SpecialJavaScriptTest extends SpecialPage { 'li', array(), Linker::link( - $this->getTitle( $framework ), + $this->getPageTitle( $framework ), // Message: javascripttest-qunit-name $this->msg( "javascripttest-$framework-name" )->escaped() ) @@ -135,16 +134,15 @@ class SpecialJavaScriptTest extends SpecialPage { * Initialize the page for QUnit. */ private function initQUnitTesting() { - global $wgJavaScriptTestConfig; - $out = $this->getOutput(); + $testConfig = $this->getConfig()->get( 'JavaScriptTestConfig' ); - $out->addModules( 'mediawiki.tests.qunit.testrunner' ); + $out->addModules( 'test.mediawiki.qunit.testrunner' ); $qunitTestModules = $out->getResourceLoader()->getTestModuleNames( 'qunit' ); $out->addModules( $qunitTestModules ); $summary = $this->msg( 'javascripttest-qunit-intro' ) - ->params( $wgJavaScriptTestConfig['qunit']['documentation'] ) + ->params( $testConfig['qunit']['documentation'] ) ->parseAsBlock(); $header = $this->msg( 'javascripttest-qunit-heading' )->escaped(); $userDir = $this->getLanguage()->getDir(); @@ -170,7 +168,22 @@ HTML; // $wgJavaScriptTestConfig in DefaultSettings.php $out->addJsConfigVars( 'QUnitTestSwarmInjectJSPath', - $wgJavaScriptTestConfig['qunit']['testswarm-injectjs'] + $testConfig['qunit']['testswarm-injectjs'] + ); + } + + /** + * Return an array of subpages beginning with $search that this special page will accept. + * + * @param string $search Prefix to search for + * @param int $limit Maximum number of results to return + * @return string[] Matching subpages + */ + public function prefixSearchSubpages( $search, $limit = 10 ) { + return self::prefixSearchArray( + $search, + $limit, + array_keys( self::$frameworks ) ); } diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php index 5b0c56e5..371469bb 100644 --- a/includes/specials/SpecialLinkSearch.php +++ b/includes/specials/SpecialLinkSearch.php @@ -27,6 +27,12 @@ * @ingroup SpecialPage */ class LinkSearchPage extends QueryPage { + + /** + * @var PageLinkRenderer + */ + protected $linkRenderer = null; + function setParams( $params ) { $this->mQuery = $params['query']; $this->mNs = $params['namespace']; @@ -35,6 +41,36 @@ class LinkSearchPage extends QueryPage { function __construct( $name = 'LinkSearch' ) { parent::__construct( $name ); + + // Since we don't control the constructor parameters, we can't inject services that way. + // Instead, we initialize services in the execute() method, and allow them to be overridden + // using the setServices() method. + } + + /** + * Initialize or override the PageLinkRenderer LinkSearchPage collaborates with. + * Useful mainly for testing. + * + * @todo query logic and rendering logic should be split and also injected + * + * @param PageLinkRenderer $linkRenderer + */ + public function setPageLinkRenderer( + PageLinkRenderer $linkRenderer + ) { + $this->linkRenderer = $linkRenderer; + } + + /** + * Initialize any services we'll need (unless it has already been provided via a setter). + * This allows for dependency injection even though we don't control object creation. + */ + private function initServices() { + if ( !$this->linkRenderer ) { + $lang = $this->getContext()->getLanguage(); + $titleFormatter = new MediaWikiTitleCodec( $lang, GenderCache::singleton() ); + $this->linkRenderer = new MediaWikiPageLinkRenderer( $titleFormatter ); + } } function isCacheable() { @@ -42,7 +78,7 @@ class LinkSearchPage extends QueryPage { } function execute( $par ) { - global $wgUrlProtocols, $wgMiserMode, $wgScript; + $this->initServices(); $this->setHeaders(); $this->outputHeader(); @@ -55,32 +91,26 @@ class LinkSearchPage extends QueryPage { $namespace = $request->getIntorNull( 'namespace', null ); $protocols_list = array(); - foreach ( $wgUrlProtocols as $prot ) { + foreach ( $this->getConfig()->get( 'UrlProtocols' ) as $prot ) { if ( $prot !== '//' ) { $protocols_list[] = $prot; } } $target2 = $target; - $protocol = ''; - $pr_sl = strpos( $target2, '//' ); - $pr_cl = strpos( $target2, ':' ); - if ( $pr_sl ) { - // For protocols with '//' - $protocol = substr( $target2, 0, $pr_sl + 2 ); - $target2 = substr( $target2, $pr_sl + 2 ); - } elseif ( !$pr_sl && $pr_cl ) { - // For protocols without '//' like 'mailto:' - $protocol = substr( $target2, 0, $pr_cl + 1 ); - $target2 = substr( $target2, $pr_cl + 1 ); - } elseif ( $protocol == '' && $target2 != '' ) { - // default - $protocol = 'http://'; - } - if ( $protocol != '' && !in_array( $protocol, $protocols_list ) ) { - // unsupported protocol, show original search request - $target2 = $target; - $protocol = ''; + // Get protocol, default is http:// + $protocol = 'http://'; + $bits = wfParseUrl( $target ); + if ( isset( $bits['scheme'] ) && isset( $bits['delimiter'] ) ) { + $protocol = $bits['scheme'] . $bits['delimiter']; + // Make sure wfParseUrl() didn't make some well-intended correction in the + // protocol + if ( strcasecmp( $protocol, substr( $target, 0, strlen( $protocol ) ) ) === 0 ) { + $target2 = substr( $target, strlen( $protocol ) ); + } else { + // If it did, let LinkFilter::makeLikeArray() handle this + $protocol = ''; + } } $out->addWikiMsg( @@ -90,9 +120,9 @@ class LinkSearchPage extends QueryPage { ); $s = Html::openElement( 'form', - array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => $wgScript ) + array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => wfScript() ) ) . "\n" . - Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n" . + Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . "\n" . Html::openElement( 'fieldset' ) . "\n" . Html::element( 'legend', array(), $this->msg( 'linksearch' )->text() ) . "\n" . Xml::inputLabel( @@ -100,10 +130,14 @@ class LinkSearchPage extends QueryPage { 'target', 'target', 50, - $target + $target, + array( + // URLs are always ltr + 'dir' => 'ltr', + ) ) . "\n"; - if ( !$wgMiserMode ) { + if ( !$this->getConfig()->get( 'MiserMode' ) ) { $s .= Html::namespaceSelector( array( 'selected' => $namespace, @@ -145,18 +179,26 @@ class LinkSearchPage extends QueryPage { /** * Return an appropriately formatted LIKE query and the clause * - * @param string $query - * @param string $prot + * @param string $query Search pattern to search for + * @param string $prot Protocol, e.g. 'http://' + * * @return array */ static function mungeQuery( $query, $prot ) { $field = 'el_index'; - $rv = LinkFilter::makeLikeArray( $query, $prot ); + $dbr = wfGetDB( DB_SLAVE ); + + if ( $query === '*' && $prot !== '' ) { + // Allow queries like 'ftp://*' to find all ftp links + $rv = array( $prot, $dbr->anyString() ); + } else { + $rv = LinkFilter::makeLikeArray( $query, $prot ); + } + if ( $rv === false ) { // LinkFilter doesn't handle wildcard in IP, so we'll have to munge here. $pattern = '/^(:?[0-9]{1,3}\.)+\*\s*$|^(:?[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]*\*\s*$/'; if ( preg_match( $pattern, $query ) ) { - $dbr = wfGetDB( DB_SLAVE ); $rv = array( $prot . rtrim( $query, " \t*" ), $dbr->anyString() ); $field = 'el_to'; } @@ -166,10 +208,9 @@ class LinkSearchPage extends QueryPage { } function linkParameters() { - global $wgMiserMode; $params = array(); $params['target'] = $this->mProt . $this->mQuery; - if ( isset( $this->mNs ) && !$wgMiserMode ) { + if ( $this->mNs !== null && !$this->getConfig()->get( 'MiserMode' ) ) { $params['namespace'] = $this->mNs; } @@ -177,7 +218,6 @@ class LinkSearchPage extends QueryPage { } function getQueryInfo() { - global $wgMiserMode; $dbr = wfGetDB( DB_SLAVE ); // strip everything past first wildcard, so that // index-based-only lookup would be done @@ -204,7 +244,7 @@ class LinkSearchPage extends QueryPage { 'options' => array( 'USE INDEX' => $clause ) ); - if ( isset( $this->mNs ) && !$wgMiserMode ) { + if ( $this->mNs !== null && !$this->getConfig()->get( 'MiserMode' ) ) { $retval['conds']['page_namespace'] = $this->mNs; } @@ -217,9 +257,10 @@ class LinkSearchPage extends QueryPage { * @return string */ function formatResult( $skin, $result ) { - $title = Title::makeTitle( $result->namespace, $result->title ); + $title = new TitleValue( (int)$result->namespace, $result->title ); + $pageLink = $this->linkRenderer->renderHtmlLink( $title ); + $url = $result->url; - $pageLink = Linker::linkKnown( $title ); $urlLink = Linker::makeExternalLink( $url, $url ); return $this->msg( 'linksearch-line' )->rawParams( $urlLink, $pageLink )->escaped(); @@ -227,6 +268,9 @@ class LinkSearchPage extends QueryPage { /** * Override to check query validity. + * + * @param mixed $offset Numerical offset or false for no offset + * @param mixed $limit Numerical limit or false for no limit */ function doQuery( $offset = false, $limit = false ) { list( $this->mMungedQuery, ) = LinkSearchPage::mungeQuery( $this->mQuery, $this->mProt ); diff --git a/includes/specials/SpecialListDuplicatedFiles.php b/includes/specials/SpecialListDuplicatedFiles.php new file mode 100644 index 00000000..26672706 --- /dev/null +++ b/includes/specials/SpecialListDuplicatedFiles.php @@ -0,0 +1,113 @@ + array( 'image' ), + 'fields' => array( + 'namespace' => NS_FILE, + 'title' => 'MIN(img_name)', + 'value' => 'count(*)' + ), + 'options' => array( + 'GROUP BY' => 'img_sha1', + 'HAVING' => 'count(*) > 1', + ), + ); + } + + /** + * Pre-fill the link cache + * + * @param DatabaseBase $db + * @param ResultWrapper $res + */ + function preprocessResults( $db, $res ) { + if ( $res->numRows() > 0 ) { + $linkBatch = new LinkBatch(); + + foreach ( $res as $row ) { + $linkBatch->add( $row->namespace, $row->title ); + } + + $res->seek( 0 ); + $linkBatch->execute(); + } + } + + + /** + * @param Skin $skin + * @param object $result Result row + * @return string + */ + function formatResult( $skin, $result ) { + // Future version might include a list of the first 5 duplicates + // perhaps separated by an "↔". + $image1 = Title::makeTitle( $result->namespace, $result->title ); + $dupeSearch = SpecialPage::getTitleFor( 'FileDuplicateSearch', $image1->getDBKey() ); + + $msg = $this->msg( 'listduplicatedfiles-entry' ) + ->params( $image1->getText() ) + ->numParams( $result->value - 1 ) + ->params( $dupeSearch->getPrefixedDBKey() ); + + return $msg->parse(); + } + + protected function getGroupName() { + return 'media'; + } +} diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php index dff1cf70..04a83c8f 100644 --- a/includes/specials/SpecialListfiles.php +++ b/includes/specials/SpecialListfiles.php @@ -33,6 +33,7 @@ class SpecialListFiles extends IncludableSpecialPage { if ( $this->including() ) { $userName = $par; $search = ''; + $showAll = false; } else { $userName = $this->getRequest()->getText( 'user', $par ); $search = $this->getRequest()->getText( 'ilsearch', '' ); @@ -47,15 +48,13 @@ class SpecialListFiles extends IncludableSpecialPage { $showAll ); + $out = $this->getOutput(); if ( $this->including() ) { - $html = $pager->getBody(); + $out->addParserOutputContent( $pager->getBodyOutput() ); } else { - $form = $pager->getForm(); - $body = $pager->getBody(); - $nav = $pager->getNavigationBar(); - $html = "$form
    \n$body
    \n$nav"; + $out->addHTML( $pager->getForm() ); + $out->addParserOutputContent( $pager->getFullOutput() ); } - $this->getOutput()->addHTML( $html ); } protected function getGroupName() { @@ -67,20 +66,24 @@ class SpecialListFiles extends IncludableSpecialPage { * @ingroup SpecialPage Pager */ class ImageListPager extends TablePager { - var $mFieldNames = null; + protected $mFieldNames = null; + // Subclasses should override buildQueryConds instead of using $mQueryConds variable. - var $mQueryConds = array(); - var $mUserName = null; - var $mSearch = ''; - var $mIncluding = false; - var $mShowAll = false; - var $mTableName = 'image'; + protected $mQueryConds = array(); + + protected $mUserName = null; + + protected $mSearch = ''; + + protected $mIncluding = false; + + protected $mShowAll = false; + + protected $mTableName = 'image'; function __construct( IContextSource $context, $userName = null, $search = '', $including = false, $showAll = false ) { - global $wgMiserMode; - $this->mIncluding = $including; $this->mShowAll = $showAll; @@ -91,7 +94,7 @@ class ImageListPager extends TablePager { } } - if ( $search !== '' && !$wgMiserMode ) { + if ( $search !== '' && !$this->getConfig()->get( 'MiserMode' ) ) { $this->mSearch = $search; $nt = Title::newFromURL( $this->mSearch ); @@ -105,12 +108,12 @@ class ImageListPager extends TablePager { if ( !$including ) { if ( $context->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) { - $this->mDefaultDirection = true; + $this->mDefaultDirection = IndexPager::DIR_DESCENDING; } else { - $this->mDefaultDirection = false; + $this->mDefaultDirection = IndexPager::DIR_ASCENDING; } } else { - $this->mDefaultDirection = true; + $this->mDefaultDirection = IndexPager::DIR_DESCENDING; } parent::__construct( $context ); @@ -120,7 +123,7 @@ class ImageListPager extends TablePager { * Build the where clause of the query. * * Replaces the older mQueryConds member variable. - * @param $table String Either "image" or "oldimage" + * @param string $table Either "image" or "oldimage" * @return array The query conditions. */ protected function buildQueryConds( $table ) { @@ -128,7 +131,7 @@ class ImageListPager extends TablePager { $conds = array(); if ( !is_null( $this->mUserName ) ) { - $conds[ $prefix . '_user_text' ] = $this->mUserName; + $conds[$prefix . '_user_text'] = $this->mUserName; } if ( $this->mSearch !== '' ) { @@ -153,20 +156,24 @@ class ImageListPager extends TablePager { } /** - * @return Array + * @return array */ function getFieldNames() { if ( !$this->mFieldNames ) { - global $wgMiserMode; $this->mFieldNames = array( 'img_timestamp' => $this->msg( 'listfiles_date' )->text(), 'img_name' => $this->msg( 'listfiles_name' )->text(), 'thumb' => $this->msg( 'listfiles_thumb' )->text(), 'img_size' => $this->msg( 'listfiles_size' )->text(), - 'img_user_text' => $this->msg( 'listfiles_user' )->text(), - 'img_description' => $this->msg( 'listfiles_description' )->text(), ); - if ( !$wgMiserMode && !$this->mShowAll ) { + if ( is_null( $this->mUserName ) ) { + // Do not show username if filtering by username + $this->mFieldNames['img_user_text'] = $this->msg( 'listfiles_user' )->text(); + } + // img_description down here, in order so that its still after the username field. + $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text(); + + if ( !$this->getConfig()->get( 'MiserMode' ) && !$this->mShowAll ) { $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text(); } if ( $this->mShowAll ) { @@ -178,7 +185,6 @@ class ImageListPager extends TablePager { } function isFieldSortable( $field ) { - global $wgMiserMode; if ( $this->mIncluding ) { return false; } @@ -190,14 +196,14 @@ class ImageListPager extends TablePager { * In particular that means we cannot sort by timestamp when not filtering * by user and including old images in the results. Which is sad. */ - if ( $wgMiserMode && !is_null( $this->mUserName ) ) { + if ( $this->getConfig()->get( 'MiserMode' ) && !is_null( $this->mUserName ) ) { // If we're sorting by user, the index only supports sorting by time. if ( $field === 'img_timestamp' ) { return true; } else { return false; } - } elseif ( $wgMiserMode && $this->mShowAll /* && mUserName === null */ ) { + } elseif ( $this->getConfig()->get( 'MiserMode' ) && $this->mShowAll /* && mUserName === null */ ) { // no oi_timestamp index, so only alphabetical sorting in this case. if ( $field === 'img_name' ) { return true; @@ -214,6 +220,7 @@ class ImageListPager extends TablePager { // for two different tables, without reimplementing // the pager class. $qi = $this->getQueryInfoReal( $this->mTableName ); + return $qi; } @@ -224,7 +231,7 @@ class ImageListPager extends TablePager { * * This is a bit hacky. * - * @param $table String Either 'image' or 'oldimage' + * @param string $table Either 'image' or 'oldimage' * @return array Query info */ protected function getQueryInfoReal( $table ) { @@ -240,7 +247,7 @@ class ImageListPager extends TablePager { } $field = $prefix . substr( $field, 3 ) . ' AS ' . $field; } - $fields[array_search('top', $fields)] = "'no' AS top"; + $fields[array_search( 'top', $fields )] = "'no' AS top"; } else { if ( $this->mShowAll ) { $fields[array_search( 'top', $fields )] = "'yes' AS top"; @@ -289,11 +296,16 @@ class ImageListPager extends TablePager { * @note $asc is named $descending in IndexPager base class. However * it is true when the order is ascending, and false when the order * is descending, so I renamed it to $asc here. + * @param int $offset + * @param int $limit + * @param bool $asc + * @return array */ function reallyDoQuery( $offset, $limit, $asc ) { $prevTableName = $this->mTableName; $this->mTableName = 'image'; - list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $asc ); + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = + $this->buildQueryInfo( $offset, $limit, $asc ); $imageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); $this->mTableName = $prevTableName; @@ -310,7 +322,8 @@ class ImageListPager extends TablePager { } $this->mIndexField = 'oi_' . substr( $this->mIndexField, 4 ); - list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $asc ); + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = + $this->buildQueryInfo( $offset, $limit, $asc ); $oldimageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); $this->mTableName = $prevTableName; @@ -324,10 +337,10 @@ class ImageListPager extends TablePager { * * Note: This will throw away some results * - * @param $res1 ResultWrapper - * @param $res2 ResultWrapper - * @param $limit int - * @param $ascending boolean See note about $asc in $this->reallyDoQuery + * @param ResultWrapper $res1 + * @param ResultWrapper $res2 + * @param int $limit + * @param bool $ascending See note about $asc in $this->reallyDoQuery * @return FakeResultWrapper $res1 and $res2 combined */ protected function combineResult( $res1, $res2, $limit, $ascending ) { @@ -337,7 +350,7 @@ class ImageListPager extends TablePager { $topRes2 = $res2->next(); $resultArray = array(); for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) { - if ( strcmp( $topRes1->{ $this->mIndexField }, $topRes2->{ $this->mIndexField } ) > 0 ) { + if ( strcmp( $topRes1->{$this->mIndexField}, $topRes2->{$this->mIndexField} ) > 0 ) { if ( !$ascending ) { $resultArray[] = $topRes1; $topRes1 = $res1->next(); @@ -355,20 +368,26 @@ class ImageListPager extends TablePager { } } } + + // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect for ( ; $i < $limit && $topRes1; $i++ ) { + // @codingStandardsIgnoreEnd $resultArray[] = $topRes1; $topRes1 = $res1->next(); } + + // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect for ( ; $i < $limit && $topRes2; $i++ ) { + // @codingStandardsIgnoreEnd $resultArray[] = $topRes2; $topRes2 = $res2->next(); } + return new FakeResultWrapper( $resultArray ); } function getDefaultSort() { - global $wgMiserMode; - if ( $this->mShowAll && $wgMiserMode && is_null( $this->mUserName ) ) { + if ( $this->mShowAll && $this->getConfig()->get( 'MiserMode' ) && is_null( $this->mUserName ) ) { // Unfortunately no index on oi_timestamp. return 'img_name'; } else { @@ -386,6 +405,20 @@ class ImageListPager extends TablePager { UserCache::singleton()->doQuery( $userIds, array( 'userpage' ), __METHOD__ ); } + /** + * @param string $field + * @param string $value + * @return Message|string|int The return type depends on the value of $field: + * - thumb: string + * - img_timestamp: string + * - img_name: string + * - img_user_text: string + * - img_size: string + * - img_description: string + * - count: int + * - top: Message + * @throws MWException + */ function formatValue( $field, $value ) { switch ( $field ) { case 'thumb': @@ -394,6 +427,7 @@ class ImageListPager extends TablePager { // If statement for paranoia if ( $file ) { $thumb = $file->transform( array( 'width' => 180, 'height' => 360 ) ); + return $thumb->toHtml( array( 'desc-link' => true ) ); } else { return htmlspecialchars( $value ); @@ -420,6 +454,19 @@ class ImageListPager extends TablePager { ); $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped(); + // Add delete links if allowed + // From https://github.com/Wikia/app/pull/3859 + if ( $filePage->userCan( 'delete', $this->getUser() ) ) { + $deleteMsg = $this->msg( 'listfiles-delete' )->escaped(); + + $delete = Linker::linkKnown( + $filePage, $deleteMsg, array(), array( 'action' => 'delete' ) + ); + $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped(); + + return "$link $download $delete"; + } + return "$link $download"; } else { return htmlspecialchars( $value ); @@ -445,58 +492,80 @@ class ImageListPager extends TablePager { case 'top': // Messages: listfiles-latestversion-yes, listfiles-latestversion-no return $this->msg( 'listfiles-latestversion-' . $value ); + default: + throw new MWException( "Unknown field '$field'" ); } } function getForm() { - global $wgScript, $wgMiserMode; - $inputForm = array(); - $inputForm['table_pager_limit_label'] = $this->getLimitSelect( array( 'tabindex' => 1 ) ); - if ( !$wgMiserMode ) { - $inputForm['listfiles_search_for'] = Html::input( - 'ilsearch', - $this->mSearch, - 'text', - array( - 'size' => '40', - 'maxlength' => '255', - 'id' => 'mw-ilsearch', - 'tabindex' => 2, - ) + $fields = array(); + $fields['limit'] = array( + 'type' => 'select', + 'name' => 'limit', + 'label-message' => 'table_pager_limit_label', + 'options' => $this->getLimitSelectList(), + 'default' => $this->mLimit, + ); + + if ( !$this->getConfig()->get( 'MiserMode' ) ) { + $fields['ilsearch'] = array( + 'type' => 'text', + 'name' => 'ilsearch', + 'id' => 'mw-ilsearch', + 'label-message' => 'listfiles_search_for', + 'default' => $this->mSearch, + 'size' => '40', + 'maxlength' => '255', ); } - $inputForm['username'] = Html::input( 'user', $this->mUserName, 'text', array( + + $fields['user'] = array( + 'type' => 'text', + 'name' => 'user', + 'id' => 'mw-listfiles-user', + 'label-message' => 'username', + 'default' => $this->mUserName, 'size' => '40', 'maxlength' => '255', - 'id' => 'mw-listfiles-user', - 'tabindex' => 3, - ) ); - - $inputForm['listfiles-show-all'] = Html::input( 'ilshowall', 1, 'checkbox', array( - 'checked' => $this->mShowAll, - 'tabindex' => 4, - ) ); - return Html::openElement( 'form', - array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listfiles-form' ) - ) . - Xml::fieldset( $this->msg( 'listfiles' )->text() ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - Xml::buildForm( $inputForm, 'table_pager_limit_submit', array( 'tabindex' => 5 ) ) . - $this->getHiddenFields( array( 'limit', 'ilsearch', 'user', 'title', 'ilshowall' ) ) . - Html::closeElement( 'fieldset' ) . - Html::closeElement( 'form' ) . "\n"; + ); + + $fields['ilshowall'] = array( + 'type' => 'check', + 'name' => 'ilshowall', + 'id' => 'mw-listfiles-show-all', + 'label-message' => 'listfiles-show-all', + 'default' => $this->mShowAll, + ); + + $query = $this->getRequest()->getQueryValues(); + unset( $query['title'] ); + unset( $query['limit'] ); + unset( $query['ilsearch'] ); + unset( $query['user'] ); + + $form = new HTMLForm( $fields, $this->getContext() ); + + $form->setMethod( 'get' ); + $form->setTitle( $this->getTitle() ); + $form->setId( 'mw-listfiles-form' ); + $form->setWrapperLegendMsg( 'listfiles' ); + $form->setSubmitTextMsg( 'table_pager_limit_submit' ); + $form->addHiddenFields( $query ); + + $form->prepareForm(); + $form->displayForm( '' ); } function getTableClass() { - return 'listfiles ' . parent::getTableClass(); + return parent::getTableClass() . ' listfiles'; } function getNavClass() { - return 'listfiles_nav ' . parent::getNavClass(); + return parent::getNavClass() . ' listfiles_nav'; } function getSortHeaderClass() { - return 'listfiles_sort ' . parent::getSortHeaderClass(); + return parent::getSortHeaderClass() . ' listfiles_sort'; } function getPagingQueries() { @@ -504,7 +573,9 @@ class ImageListPager extends TablePager { if ( !is_null( $this->mUserName ) ) { # Append the username to the query string foreach ( $queries as &$query ) { - $query['user'] = $this->mUserName; + if ( $query !== false ) { + $query['user'] = $this->mUserName; + } } } diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php index 82a4f70f..5bae28f0 100644 --- a/includes/specials/SpecialListgrouprights.php +++ b/includes/specials/SpecialListgrouprights.php @@ -29,21 +29,15 @@ * @author Petr Kadlec */ class SpecialListGroupRights extends SpecialPage { - /** - * Constructor - */ function __construct() { parent::__construct( 'Listgrouprights' ); } /** * Show the special page + * @param string|null $par */ public function execute( $par ) { - global $wgImplicitGroups; - global $wgGroupPermissions, $wgRevokePermissions, $wgAddGroups, $wgRemoveGroups; - global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; - $this->setHeaders(); $this->outputHeader(); @@ -60,19 +54,26 @@ class SpecialListGroupRights extends SpecialPage { '' ); + $config = $this->getConfig(); + $groupPermissions = $config->get( 'GroupPermissions' ); + $revokePermissions = $config->get( 'RevokePermissions' ); + $addGroups = $config->get( 'AddGroups' ); + $removeGroups = $config->get( 'RemoveGroups' ); + $groupsAddToSelf = $config->get( 'GroupsAddToSelf' ); + $groupsRemoveFromSelf = $config->get( 'GroupsRemoveFromSelf' ); $allGroups = array_unique( array_merge( - array_keys( $wgGroupPermissions ), - array_keys( $wgRevokePermissions ), - array_keys( $wgAddGroups ), - array_keys( $wgRemoveGroups ), - array_keys( $wgGroupsAddToSelf ), - array_keys( $wgGroupsRemoveFromSelf ) + array_keys( $groupPermissions ), + array_keys( $revokePermissions ), + array_keys( $addGroups ), + array_keys( $removeGroups ), + array_keys( $groupsAddToSelf ), + array_keys( $groupsRemoveFromSelf ) ) ); asort( $allGroups ); foreach ( $allGroups as $group ) { - $permissions = isset( $wgGroupPermissions[$group] ) - ? $wgGroupPermissions[$group] + $permissions = isset( $groupPermissions[$group] ) + ? $groupPermissions[$group] : array(); $groupname = ( $group == '*' ) // Replace * with a more descriptive groupname ? 'all' @@ -102,7 +103,7 @@ class SpecialListGroupRights extends SpecialPage { SpecialPage::getTitleFor( 'Listusers' ), $this->msg( 'listgrouprights-members' )->escaped() ); - } elseif ( !in_array( $group, $wgImplicitGroups ) ) { + } elseif ( !in_array( $group, $config->get( 'ImplicitGroups' ) ) ) { $grouplink = '
    ' . Linker::linkKnown( SpecialPage::getTitleFor( 'Listusers' ), $this->msg( 'listgrouprights-members' )->escaped(), @@ -114,15 +115,16 @@ class SpecialListGroupRights extends SpecialPage { $grouplink = ''; } - $revoke = isset( $wgRevokePermissions[$group] ) ? $wgRevokePermissions[$group] : array(); - $addgroups = isset( $wgAddGroups[$group] ) ? $wgAddGroups[$group] : array(); - $removegroups = isset( $wgRemoveGroups[$group] ) ? $wgRemoveGroups[$group] : array(); - $addgroupsSelf = isset( $wgGroupsAddToSelf[$group] ) ? $wgGroupsAddToSelf[$group] : array(); - $removegroupsSelf = isset( $wgGroupsRemoveFromSelf[$group] ) ? $wgGroupsRemoveFromSelf[$group] : array(); + $revoke = isset( $revokePermissions[$group] ) ? $revokePermissions[$group] : array(); + $addgroups = isset( $addGroups[$group] ) ? $addGroups[$group] : array(); + $removegroups = isset( $removeGroups[$group] ) ? $removeGroups[$group] : array(); + $addgroupsSelf = isset( $groupsAddToSelf[$group] ) ? $groupsAddToSelf[$group] : array(); + $removegroupsSelf = isset( $groupsRemoveFromSelf[$group] ) + ? $groupsRemoveFromSelf[$group] + : array(); $id = $group == '*' ? false : Sanitizer::escapeId( $group ); - $out->addHTML( Html::rawElement( 'tr', array( 'id' => $id ), - " + $out->addHTML( Html::rawElement( 'tr', array( 'id' => $id ), " $grouppage$grouplink " . $this->formatPermissions( $permissions, $revoke, $addgroups, $removegroups, @@ -132,17 +134,100 @@ class SpecialListGroupRights extends SpecialPage { ) ); } $out->addHTML( Xml::closeElement( 'table' ) ); + $this->outputNamespaceProtectionInfo(); + } + + private function outputNamespaceProtectionInfo() { + global $wgParser, $wgContLang; + $out = $this->getOutput(); + $namespaceProtection = $this->getConfig()->get( 'NamespaceProtection' ); + + if ( count( $namespaceProtection ) == 0 ) { + return; + } + + $header = $this->msg( 'listgrouprights-namespaceprotection-header' )->parse(); + $out->addHTML( + Html::rawElement( 'h2', array(), Html::element( 'span', array( + 'class' => 'mw-headline', + 'id' => $wgParser->guessSectionNameFromWikiText( $header ) + ), $header ) ) . + Xml::openElement( 'table', array( 'class' => 'wikitable' ) ) . + Html::element( + 'th', + array(), + $this->msg( 'listgrouprights-namespaceprotection-namespace' )->text() + ) . + Html::element( + 'th', + array(), + $this->msg( 'listgrouprights-namespaceprotection-restrictedto' )->text() + ) + ); + + ksort( $namespaceProtection ); + foreach ( $namespaceProtection as $namespace => $rights ) { + if ( !in_array( $namespace, MWNamespace::getValidNamespaces() ) ) { + continue; + } + + if ( $namespace == NS_MAIN ) { + $namespaceText = $this->msg( 'blanknamespace' )->text(); + } else { + $namespaceText = $wgContLang->convertNamespace( $namespace ); + } + + $out->addHTML( + Xml::openElement( 'tr' ) . + Html::rawElement( + 'td', + array(), + Linker::link( + SpecialPage::getTitleFor( 'Allpages' ), + $namespaceText, + array(), + array( 'namespace' => $namespace ) + ) + ) . + Xml::openElement( 'td' ) . Xml::openElement( 'ul' ) + ); + + if ( !is_array( $rights ) ) { + $rights = array( $rights ); + } + + foreach ( $rights as $right ) { + $out->addHTML( + Html::rawElement( 'li', array(), $this->msg( + 'listgrouprights-right-display', + User::getRightDescription( $right ), + Html::element( + 'span', + array( 'class' => 'mw-listgrouprights-right-name' ), + $right + ) + )->parse() ) + ); + } + + $out->addHTML( + Xml::closeElement( 'ul' ) . + Xml::closeElement( 'td' ) . + Xml::closeElement( 'tr' ) + ); + } + $out->addHTML( Xml::closeElement( 'table' ) ); } /** * Create a user-readable list of permissions from the given array. * - * @param array $permissions of permission => bool (from $wgGroupPermissions items) - * @param array $revoke of permission => bool (from $wgRevokePermissions items) - * @param array $add of groups this group is allowed to add or true - * @param array $remove of groups this group is allowed to remove or true - * @param array $addSelf of groups this group is allowed to add to self or true - * @param array $removeSelf of group this group is allowed to remove from self or true + * @param array $permissions Array of permission => bool (from $wgGroupPermissions items) + * @param array $revoke Array of permission => bool (from $wgRevokePermissions items) + * @param array $add Array of groups this group is allowed to add or true + * @param array $remove Array of groups this group is allowed to remove or true + * @param array $addSelf Array of groups this group is allowed to add to self or true + * @param array $removeSelf Array of group this group is allowed to remove from self or true * @return string List of all granted permissions, separated by comma separator */ private function formatPermissions( $permissions, $revoke, $add, $remove, $addSelf, $removeSelf ) { @@ -170,45 +255,54 @@ class SpecialListGroupRights extends SpecialPage { sort( $r ); $lang = $this->getLanguage(); + $allGroups = User::getAllGroups(); if ( $add === true ) { $r[] = $this->msg( 'listgrouprights-addgroup-all' )->escaped(); - } elseif ( is_array( $add ) && count( $add ) ) { - $add = array_values( array_unique( $add ) ); - $r[] = $this->msg( 'listgrouprights-addgroup', - $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ), - count( $add ) - )->parse(); + } elseif ( is_array( $add ) ) { + $add = array_intersect( array_values( array_unique( $add ) ), $allGroups ); + if ( count( $add ) ) { + $r[] = $this->msg( 'listgrouprights-addgroup', + $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ), + count( $add ) + )->parse(); + } } if ( $remove === true ) { $r[] = $this->msg( 'listgrouprights-removegroup-all' )->escaped(); - } elseif ( is_array( $remove ) && count( $remove ) ) { - $remove = array_values( array_unique( $remove ) ); - $r[] = $this->msg( 'listgrouprights-removegroup', - $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ), - count( $remove ) - )->parse(); + } elseif ( is_array( $remove ) ) { + $remove = array_intersect( array_values( array_unique( $remove ) ), $allGroups ); + if ( count( $remove ) ) { + $r[] = $this->msg( 'listgrouprights-removegroup', + $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ), + count( $remove ) + )->parse(); + } } if ( $addSelf === true ) { $r[] = $this->msg( 'listgrouprights-addgroup-self-all' )->escaped(); - } elseif ( is_array( $addSelf ) && count( $addSelf ) ) { - $addSelf = array_values( array_unique( $addSelf ) ); - $r[] = $this->msg( 'listgrouprights-addgroup-self', - $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $addSelf ) ), - count( $addSelf ) - )->parse(); + } elseif ( is_array( $addSelf ) ) { + $addSelf = array_intersect( array_values( array_unique( $addSelf ) ), $allGroups ); + if ( count( $addSelf ) ) { + $r[] = $this->msg( 'listgrouprights-addgroup-self', + $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $addSelf ) ), + count( $addSelf ) + )->parse(); + } } if ( $removeSelf === true ) { $r[] = $this->msg( 'listgrouprights-removegroup-self-all' )->parse(); - } elseif ( is_array( $removeSelf ) && count( $removeSelf ) ) { - $removeSelf = array_values( array_unique( $removeSelf ) ); - $r[] = $this->msg( 'listgrouprights-removegroup-self', - $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $removeSelf ) ), - count( $removeSelf ) - )->parse(); + } elseif ( is_array( $removeSelf ) ) { + $removeSelf = array_intersect( array_values( array_unique( $removeSelf ) ), $allGroups ); + if ( count( $removeSelf ) ) { + $r[] = $this->msg( 'listgrouprights-removegroup-self', + $lang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $removeSelf ) ), + count( $removeSelf ) + )->parse(); + } } if ( empty( $r ) ) { diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php index 2c8792ff..de05be41 100644 --- a/includes/specials/SpecialListredirects.php +++ b/includes/specials/SpecialListredirects.php @@ -76,20 +76,19 @@ class ListredirectsPage extends QueryPage { * @param ResultWrapper $res */ function preprocessResults( $db, $res ) { - $batch = new LinkBatch; + if ( !$res->numRows() ) { + return; + } + $batch = new LinkBatch; foreach ( $res as $row ) { $batch->add( $row->namespace, $row->title ); $batch->addObj( $this->getRedirectTarget( $row ) ); } - $batch->execute(); // Back to start for display - if ( $res->numRows() > 0 ) { - // If there are no rows we get an error seeking. - $db->dataSeek( $res, 0 ); - } + $res->seek( 0 ); } protected function getRedirectTarget( $row ) { diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php index 8cd9173c..dad9074d 100644 --- a/includes/specials/SpecialListusers.php +++ b/includes/specials/SpecialListusers.php @@ -35,9 +35,9 @@ class UsersPager extends AlphabeticPager { /** - * @param $context IContextSource + * @param IContextSource $context * @param array $par (Default null) - * @param $including boolean Whether this page is being transcluded in + * @param bool $including Whether this page is being transcluded in * another page */ function __construct( IContextSource $context = null, $par = null, $including = null ) { @@ -69,7 +69,9 @@ class UsersPager extends AlphabeticPager { $this->editsOnly = $request->getBool( 'editsOnly' ); $this->creationSort = $request->getBool( 'creationSort' ); $this->including = $including; - $this->mDefaultDirection = $request->getBool( 'desc' ); + $this->mDefaultDirection = $request->getBool( 'desc' ) + ? IndexPager::DIR_DESCENDING + : IndexPager::DIR_ASCENDING; $this->requestedUser = ''; @@ -92,7 +94,7 @@ class UsersPager extends AlphabeticPager { } /** - * @return Array + * @return array */ function getQueryInfo() { $dbr = wfGetDB( DB_SLAVE ); @@ -154,8 +156,8 @@ class UsersPager extends AlphabeticPager { } /** - * @param $row Object - * @return String + * @param stdClass $row + * @return string */ function formatRow( $row ) { if ( $row->user_id == 0 ) { #Bug 16487 @@ -191,12 +193,9 @@ class UsersPager extends AlphabeticPager { } $edits = ''; - global $wgEdititis; - if ( !$this->including && $wgEdititis ) { - // @fixme i18n issue: Hardcoded square brackets. - $edits = ' [' . - $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped() . - ']'; + if ( !$this->including && $this->getConfig()->get( 'Edititis' ) ) { + $count = $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped(); + $edits = $this->msg( 'word-separator' )->escaped() . $this->msg( 'brackets', $count )->escaped(); } $created = ''; @@ -232,14 +231,12 @@ class UsersPager extends AlphabeticPager { * @return string */ function getPageHeader() { - global $wgScript; - list( $self ) = explode( '/', $this->getTitle()->getPrefixedDBkey() ); # Form tag $out = Xml::openElement( 'form', - array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listusers-form' ) + array( 'method' => 'get', 'action' => wfScript(), 'id' => 'mw-listusers-form' ) ) . Xml::fieldset( $this->msg( 'listusers' )->text() ) . Html::hidden( 'title', $self ); @@ -333,7 +330,7 @@ class UsersPager extends AlphabeticPager { /** * Get a list of groups the specified user belongs to * - * @param $uid Integer: user id + * @param int $uid User id * @return array */ protected static function getGroups( $uid ) { @@ -346,7 +343,7 @@ class UsersPager extends AlphabeticPager { /** * Format a link to a group description page * - * @param string $group group name + * @param string $group Group name * @param string $username Username * @return string */ @@ -399,7 +396,41 @@ class SpecialListUsers extends IncludableSpecialPage { $this->getOutput()->addHTML( $s ); } + /** + * Return an array of subpages beginning with $search that this special page will accept. + * + * @param string $search Prefix to search for + * @param int $limit Maximum number of results to return + * @return string[] Matching subpages + */ + public function prefixSearchSubpages( $search, $limit = 10 ) { + $subpages = User::getAllGroups(); + return self::prefixSearchArray( $search, $limit, $subpages ); + } + protected function getGroupName() { return 'users'; } } + +/** + * Redirect page: Special:ListAdmins --> Special:ListUsers/sysop. + * + * @ingroup SpecialPage + */ +class SpecialListAdmins extends SpecialRedirectToSpecial { + function __construct() { + parent::__construct( 'Listadmins', 'Listusers', 'sysop' ); + } +} + +/** + * Redirect page: Special:ListBots --> Special:ListUsers/bot. + * + * @ingroup SpecialPage + */ +class SpecialListBots extends SpecialRedirectToSpecial { + function __construct() { + parent::__construct( 'Listbots', 'Listusers', 'bot' ); + } +} diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php index 95ef9510..1c1f1250 100644 --- a/includes/specials/SpecialLockdb.php +++ b/includes/specials/SpecialLockdb.php @@ -27,7 +27,7 @@ * @ingroup SpecialPage */ class SpecialLockdb extends FormSpecialPage { - var $reason = ''; + protected $reason = ''; public function __construct() { parent::__construct( 'Lockdb', 'siteadmin' ); @@ -38,11 +38,9 @@ class SpecialLockdb extends FormSpecialPage { } public function checkExecutePermissions( User $user ) { - global $wgReadOnlyFile; - parent::checkExecutePermissions( $user ); # If the lock file isn't writable, we can do sweet bugger all - if ( !is_writable( dirname( $wgReadOnlyFile ) ) ) { + if ( !is_writable( dirname( $this->getConfig()->get( 'ReadOnlyFile' ) ) ) ) { throw new ErrorPageError( 'lockdb', 'lockfilenotwritable' ); } } @@ -69,14 +67,14 @@ class SpecialLockdb extends FormSpecialPage { } public function onSubmit( array $data ) { - global $wgContLang, $wgReadOnlyFile; + global $wgContLang; if ( !$data['Confirm'] ) { return Status::newFatal( 'locknoconfirm' ); } wfSuppressWarnings(); - $fp = fopen( $wgReadOnlyFile, 'w' ); + $fp = fopen( $this->getConfig()->get( 'ReadOnlyFile' ), 'w' ); wfRestoreWarnings(); if ( false === $fp ) { diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php index 2ffdd89d..dc33801d 100644 --- a/includes/specials/SpecialLog.php +++ b/includes/specials/SpecialLog.php @@ -45,8 +45,6 @@ class SpecialLog extends SpecialPage { } public function execute( $par ) { - global $wgLogRestrictions; - $this->setHeaders(); $this->outputHeader(); @@ -77,11 +75,14 @@ class SpecialLog extends SpecialPage { // If the user doesn't have the right permission to view the specific // log type, throw a PermissionsError // If the log type is invalid, just show all public logs + $logRestrictions = $this->getConfig()->get( 'LogRestrictions' ); $type = $opts->getValue( 'type' ); if ( !LogPage::isLogType( $type ) ) { $opts->setValue( 'type', '' ); - } elseif ( isset( $wgLogRestrictions[$type] ) && !$this->getUser()->isAllowed( $wgLogRestrictions[$type] ) ) { - throw new PermissionsError( $wgLogRestrictions[$type] ); + } elseif ( isset( $logRestrictions[$type] ) + && !$this->getUser()->isAllowed( $logRestrictions[$type] ) + ) { + throw new PermissionsError( $logRestrictions[$type] ); } # Handle type-specific inputs @@ -98,6 +99,7 @@ class SpecialLog extends SpecialPage { # Some log types are only for a 'User:' title but we might have been given # only the username instead of the full title 'User:username'. This part try # to lookup for a user by that name and eventually fix user input. See bug 1697. + wfRunHooks( 'GetLogTypesOnUser', array( &$this->typeOnUser ) ); if ( in_array( $opts->getValue( 'type' ), $this->typeOnUser ) ) { # ok we have a type of log which expect a user title. $target = Title::newFromText( $opts->getValue( 'page' ) ); @@ -112,14 +114,26 @@ class SpecialLog extends SpecialPage { $this->show( $opts, $qc ); } - private function parseParams( FormOptions $opts, $par ) { - global $wgLogTypes; + /** + * Return an array of subpages beginning with $search that this special page will accept. + * + * @param string $search Prefix to search for + * @param int $limit Maximum number of results to return + * @return string[] Matching subpages + */ + public function prefixSearchSubpages( $search, $limit = 10 ) { + $subpages = $this->getConfig()->get( 'LogTypes' ); + $subpages[] = 'all'; + sort( $subpages ); + return self::prefixSearchArray( $search, $limit, $subpages ); + } + private function parseParams( FormOptions $opts, $par ) { # Get parameters $parms = explode( '/', ( $par = ( $par !== null ) ? $par : '' ) ); $symsForAll = array( '*', 'all' ); if ( $parms[0] != '' && - ( in_array( $par, $wgLogTypes ) || in_array( $par, $symsForAll ) ) + ( in_array( $par, $this->getConfig()->get( 'LogTypes' ) ) || in_array( $par, $symsForAll ) ) ) { $opts->setValue( 'type', $par ); } elseif ( count( $parms ) == 2 ) { @@ -193,10 +207,9 @@ class SpecialLog extends SpecialPage { } # Show button to hide log entries - global $wgScript; $s = Html::openElement( 'form', - array( 'action' => $wgScript, 'id' => 'mw-log-deleterevision-submit' ) + array( 'action' => wfScript(), 'id' => 'mw-log-deleterevision-submit' ) ) . "\n"; $s .= Html::hidden( 'title', SpecialPage::getTitleFor( 'Revisiondelete' ) ) . "\n"; $s .= Html::hidden( 'target', SpecialPage::getTitleFor( 'Log' ) ) . "\n"; @@ -217,7 +230,7 @@ class SpecialLog extends SpecialPage { /** * Set page title and show header for this log type - * @param $type string + * @param string $type * @since 1.19 */ protected function addHeader( $type ) { diff --git a/includes/specials/SpecialLonelypages.php b/includes/specials/SpecialLonelypages.php index 7c7771d7..f533234f 100644 --- a/includes/specials/SpecialLonelypages.php +++ b/includes/specials/SpecialLonelypages.php @@ -49,36 +49,40 @@ class LonelyPagesPage extends PageQueryPage { } function getQueryInfo() { - return array( - 'tables' => array( - 'page', 'pagelinks', - 'templatelinks' + $tables = array( 'page', 'pagelinks', 'templatelinks' ); + $conds = array( + 'pl_namespace IS NULL', + 'page_namespace' => MWNamespace::getContentNamespaces(), + 'page_is_redirect' => 0, + 'tl_namespace IS NULL' + ); + $joinConds = array( + 'pagelinks' => array( + 'LEFT JOIN', array( + 'pl_namespace = page_namespace', + 'pl_title = page_title' + ) ), + 'templatelinks' => array( + 'LEFT JOIN', array( + 'tl_namespace = page_namespace', + 'tl_title = page_title' + ) + ) + ); + + // Allow extensions to modify the query + wfRunHooks( 'LonelyPagesQuery', array( &$tables, &$conds, &$joinConds ) ); + + return array( + 'tables' => $tables, 'fields' => array( 'namespace' => 'page_namespace', 'title' => 'page_title', 'value' => 'page_title' ), - 'conds' => array( - 'pl_namespace IS NULL', - 'page_namespace' => MWNamespace::getContentNamespaces(), - 'page_is_redirect' => 0, - 'tl_namespace IS NULL' - ), - 'join_conds' => array( - 'pagelinks' => array( - 'LEFT JOIN', array( - 'pl_namespace = page_namespace', - 'pl_title = page_title' - ) - ), - 'templatelinks' => array( - 'LEFT JOIN', array( - 'tl_namespace = page_namespace', - 'tl_title = page_title' - ) - ) - ) + 'conds' => $conds, + 'join_conds' => $joinConds ); } diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php index 3eeae310..60225ea5 100644 --- a/includes/specials/SpecialMIMEsearch.php +++ b/includes/specials/SpecialMIMEsearch.php @@ -28,7 +28,7 @@ * @ingroup SpecialPage */ class MIMEsearchPage extends QueryPage { - protected $major, $minor; + protected $major, $minor, $mime; function __construct( $name = 'MIMEsearch' ) { parent::__construct( $name ); @@ -51,6 +51,11 @@ class MIMEsearchPage extends QueryPage { } public function getQueryInfo() { + $minorType = array(); + if ( $this->minor !== '*' ) { + // Allow wildcard searching + $minorType['img_minor_mime'] = $this->minor; + } $qi = array( 'tables' => array( 'image' ), 'fields' => array( @@ -67,7 +72,6 @@ class MIMEsearchPage extends QueryPage { ), 'conds' => array( 'img_major_mime' => $this->major, - 'img_minor_mime' => $this->minor, // This is in order to trigger using // the img_media_mime index in "range" mode. 'img_media_type' => array( @@ -82,8 +86,9 @@ class MIMEsearchPage extends QueryPage { MEDIATYPE_EXECUTABLE, MEDIATYPE_ARCHIVE, ), - ), + ) + $minorType, ); + return $qi; } @@ -94,38 +99,42 @@ class MIMEsearchPage extends QueryPage { * that this report gives results in a logical order). As an aditional * note, mysql seems to by default order things by img_name ASC, which * is what we ideally want, so everything works out fine anyhow. + * @return array */ function getOrderFields() { return array(); } - function execute( $par ) { - global $wgScript; - - $mime = $par ? $par : $this->getRequest()->getText( 'mime' ); + /** + * Return HTML to put just before the results. + */ + function getPageHeader() { - $this->setHeaders(); - $this->outputHeader(); - $this->getOutput()->addHTML( - Xml::openElement( + return Xml::openElement( 'form', - array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgScript ) + array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => wfScript() ) ) . - Xml::openElement( 'fieldset' ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - Xml::element( 'legend', null, $this->msg( 'mimesearch' )->text() ) . - Xml::inputLabel( $this->msg( 'mimetype' )->text(), 'mime', 'mime', 20, $mime ) . - ' ' . - Xml::submitButton( $this->msg( 'ilsubmit' )->text() ) . - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ) - ); + Xml::openElement( 'fieldset' ) . + Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) . + Xml::element( 'legend', null, $this->msg( 'mimesearch' )->text() ) . + Xml::inputLabel( $this->msg( 'mimetype' )->text(), 'mime', 'mime', 20, $this->mime ) . + ' ' . + Xml::submitButton( $this->msg( 'ilsubmit' )->text() ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ); + } - list( $this->major, $this->minor ) = File::splitMime( $mime ); + 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 ); if ( $this->major == '' || $this->minor == '' || $this->minor == 'unknown' || !self::isValidType( $this->major ) ) { + $this->setHeaders(); + $this->outputHeader(); + $this->getOutput()->addHTML( $this->getPageHeader() ); return; } @@ -165,7 +174,7 @@ class MIMEsearchPage extends QueryPage { } /** - * @param $type string + * @param string $type * @return bool */ protected static function isValidType( $type ) { @@ -179,7 +188,8 @@ class MIMEsearchPage extends QueryPage { 'video', 'message', 'model', - 'multipart' + 'multipart', + 'chemical' ); return in_array( $type, $types ); diff --git a/includes/specials/SpecialMediaStatistics.php b/includes/specials/SpecialMediaStatistics.php new file mode 100644 index 00000000..681c332f --- /dev/null +++ b/includes/specials/SpecialMediaStatistics.php @@ -0,0 +1,325 @@ +limit = 5000; + $this->shownavigation = false; + } + + function isExpensive() { + return true; + } + + /** + * Query to do. + * + * This abuses the query cache table by storing mime types as "titles". + * + * This will store entries like [[Media:BITMAP;image/jpeg;200;20000]] + * where the form is Media type;mime type;count;bytes. + * + * This relies on the behaviour that when value is tied, the order things + * come out of querycache table is the order they went in. Which is hacky. + * However, other special pages like Special:Deadendpages and + * Special:BrokenRedirects also rely on this. + */ + public function getQueryInfo() { + $dbr = wfGetDB( DB_SLAVE ); + $fakeTitle = $dbr->buildConcat( array( + 'img_media_type', + $dbr->addQuotes( ';' ), + 'img_major_mime', + $dbr->addQuotes( '/' ), + 'img_minor_mime', + $dbr->addQuotes( ';' ), + 'COUNT(*)', + $dbr->addQuotes( ';' ), + 'SUM( img_size )' + ) ); + return array( + 'tables' => array( 'image' ), + 'fields' => array( + 'title' => $fakeTitle, + 'namespace' => NS_MEDIA, /* needs to be something */ + 'value' => '1' + ), + 'options' => array( + 'GROUP BY' => array( + 'img_media_type', + 'img_major_mime', + 'img_minor_mime', + ) + ) + ); + } + + /** + * How to sort the results + * + * It's important that img_media_type come first, otherwise the + * tables will be fragmented. + * @return Array Fields to sort by + */ + function getOrderFields() { + return array( 'img_media_type', 'count(*)', 'img_major_mime', 'img_minor_mime' ); + } + + /** + * Output the results of the query. + * + * @param $out OutputPage + * @param $skin Skin (deprecated presumably) + * @param $dbr DatabaseBase + * @param $res ResultWrapper Results from query + * @param $num integer Number of results + * @param $offset integer Paging offset (Should always be 0 in our case) + */ + protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) { + $prevMediaType = null; + foreach ( $res as $row ) { + list( $mediaType, $mime, $totalCount, $totalBytes ) = $this->splitFakeTitle( $row->title ); + if ( $prevMediaType !== $mediaType ) { + if ( $prevMediaType !== null ) { + // We're not at beginning, so we have to + // close the previous table. + $this->outputTableEnd(); + } + $this->outputMediaType( $mediaType ); + $this->outputTableStart( $mediaType ); + $prevMediaType = $mediaType; + } + $this->outputTableRow( $mime, intval( $totalCount ), intval( $totalBytes ) ); + } + if ( $prevMediaType !== null ) { + $this->outputTableEnd(); + } + } + + /** + * Output closing + */ + protected function outputTableEnd() { + $this->getOutput()->addHtml( Html::closeElement( 'table' ) ); + } + + /** + * Output a row of the stats table + * + * @param $mime String mime type (e.g. image/jpeg) + * @param $count integer Number of images of this type + * @param $totalBytes integer Total space for images of this type + */ + protected function outputTableRow( $mime, $count, $bytes ) { + $mimeSearch = SpecialPage::getTitleFor( 'MIMEsearch', $mime ); + $row = Html::rawElement( + 'td', + array(), + Linker::link( $mimeSearch, htmlspecialchars( $mime ) ) + ); + $row .= Html::element( + 'td', + array(), + $this->getExtensionList( $mime ) + ); + $row .= Html::rawElement( + 'td', + array(), + $this->msg( 'mediastatistics-nfiles' ) + ->numParams( $count ) + /** @todo Check to be sure this really should have number formatting */ + ->numParams( $this->makePercentPretty( $count / $this->totalCount ) ) + ->parse() + ); + $row .= Html::rawElement( + 'td', + // Make sure js sorts it in numeric order + array( 'data-sort-value' => $bytes ), + $this->msg( 'mediastatistics-nbytes' ) + ->numParams( $bytes ) + ->sizeParams( $bytes ) + /** @todo Check to be sure this really should have number formatting */ + ->numParams( $this->makePercentPretty( $bytes / $this->totalBytes ) ) + ->parse() + ); + + $this->getOutput()->addHTML( Html::rawElement( 'tr', array(), $row ) ); + } + + /** + * @param float $decimal A decimal percentage (ie for 12.3%, this would be 0.123) + * @return String The percentage formatted so that 3 significant digits are shown. + */ + protected function makePercentPretty( $decimal ) { + $decimal *= 100; + // Always show three useful digits + if ( $decimal == 0 ) { + return '0'; + } + $percent = sprintf( "%." . max( 0, 2 - floor( log10( $decimal ) ) ) . "f", $decimal ); + // Then remove any trailing 0's + return preg_replace( '/\.?0*$/', '', $percent ); + } + + /** + * Given a mime type, return a comma separated list of allowed extensions. + * + * @param $mime String mime type + * @return String Comma separated list of allowed extensions (e.g. ".ogg, .oga") + */ + private function getExtensionList( $mime ) { + $exts = MimeMagic::singleton()->getExtensionsForType( $mime ); + if ( $exts === null ) { + return ''; + } + $extArray = explode( ' ', $exts ); + $extArray = array_unique( $extArray ); + foreach ( $extArray as &$ext ) { + $ext = '.' . $ext; + } + + return $this->getLanguage()->commaList( $extArray ); + } + + /** + * Output the start of the table + * + * Including opening , and first with column headers. + */ + protected function outputTableStart( $mediaType ) { + $this->getOutput()->addHTML( + Html::openElement( + 'table', + array( 'class' => array( + 'mw-mediastats-table', + 'mw-mediastats-table-' . strtolower( $mediaType ), + 'sortable', + 'wikitable' + )) + ) + ); + $this->getOutput()->addHTML( $this->getTableHeaderRow() ); + } + + /** + * Get (not output) the header row for the table + * + * @return String the header row of the able + */ + protected function getTableHeaderRow() { + $headers = array( 'mimetype', 'extensions', 'count', 'totalbytes' ); + $ths = ''; + foreach ( $headers as $header ) { + $ths .= Html::rawElement( + 'th', + array(), + // for grep: + // mediastatistics-table-mimetype, mediastatistics-table-extensions + // tatistics-table-count, mediastatistics-table-totalbytes + $this->msg( 'mediastatistics-table-' . $header )->parse() + ); + } + return Html::rawElement( 'tr', array(), $ths ); + } + + /** + * Output a header for a new media type section + * + * @param $mediaType string A media type (e.g. from the MEDIATYPE_xxx constants) + */ + protected function outputMediaType( $mediaType ) { + $this->getOutput()->addHTML( + Html::element( + 'h2', + 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, + // mediastatistics-header-video, mediastatistics-header-multimedia, + // mediastatistics-header-office, mediastatistics-header-text, + // mediastatistics-header-executable, mediastatistics-header-archive, + $this->msg( 'mediastatistics-header-' . strtolower( $mediaType ) )->text() + ) + ); + /** @todo Possibly could add a message here explaining what the different types are. + * not sure if it is needed though. + */ + } + + /** + * parse the fake title format that this special page abuses querycache with. + * + * @param $fakeTitle String A string formatted as ;;; + * @return Array The constituant parts of $fakeTitle + */ + private function splitFakeTitle( $fakeTitle ) { + return explode( ';', $fakeTitle, 4 ); + } + + /** + * What group to put the page in + * @return string + */ + protected function getGroupName() { + return 'media'; + } + + /** + * This method isn't used, since we override outputResults, but + * we need to implement since abstract in parent class. + * + * @param $skin Skin + * @param $result stdObject Result row + */ + public function formatResult( $skin, $result ) { + throw new MWException( "unimplemented" ); + } + + /** + * Initialize total values so we can figure out percentages later. + * + * @param $dbr DatabaseBase + * @param $res ResultWrapper + */ + public function preprocessResults( $dbr, $res ) { + $this->totalCount = $this->totalBytes = 0; + foreach ( $res as $row ) { + list( , , $count, $bytes ) = $this->splitFakeTitle( $row->title ); + $this->totalCount += $count; + $this->totalBytes += $bytes; + } + $res->seek( 0 ); + } +} diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php index fb5ea657..43f5a1ba 100644 --- a/includes/specials/SpecialMergeHistory.php +++ b/includes/specials/SpecialMergeHistory.php @@ -28,12 +28,38 @@ * @ingroup SpecialPage */ class SpecialMergeHistory extends SpecialPage { - var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment; + /** @var string */ + protected $mAction; - /** - * @var Title - */ - var $mTargetObj, $mDestObj; + /** @var string */ + protected $mTarget; + + /** @var string */ + protected $mDest; + + /** @var string */ + protected $mTimestamp; + + /** @var int */ + protected $mTargetID; + + /** @var int */ + protected $mDestID; + + /** @var string */ + protected $mComment; + + /** @var bool Was posted? */ + protected $mMerge; + + /** @var bool Was submitted? */ + protected $mSubmitted; + + /** @var Title */ + protected $mTargetObj; + + /** @var Title */ + protected $mDestObj; public function __construct() { parent::__construct( 'MergeHistory', 'mergehistory' ); @@ -57,7 +83,9 @@ class SpecialMergeHistory extends SpecialPage { } $this->mComment = $request->getText( 'wpComment' ); - $this->mMerge = $request->wasPosted() && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ); + $this->mMerge = $request->wasPosted() + && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ); + // target page if ( $this->mSubmitted ) { $this->mTargetObj = Title::newFromURL( $this->mTarget ); @@ -105,7 +133,7 @@ class SpecialMergeHistory extends SpecialPage { if ( !$this->mTargetObj instanceof Title ) { $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock(); } elseif ( !$this->mTargetObj->exists() ) { - $errors[] = $this->msg( 'mergehistory-no-source', array( 'parse' ), + $errors[] = $this->msg( 'mergehistory-no-source', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) )->parseAsBlock(); } @@ -113,7 +141,7 @@ class SpecialMergeHistory extends SpecialPage { if ( !$this->mDestObj instanceof Title ) { $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock(); } elseif ( !$this->mDestObj->exists() ) { - $errors[] = $this->msg( 'mergehistory-no-destination', array( 'parse' ), + $errors[] = $this->msg( 'mergehistory-no-destination', wfEscapeWikiText( $this->mDestObj->getPrefixedText() ) )->parseAsBlock(); } @@ -131,18 +159,16 @@ class SpecialMergeHistory extends SpecialPage { } function showMergeForm() { - global $wgScript; - $this->getOutput()->addWikiMsg( 'mergehistory-header' ); $this->getOutput()->addHTML( Xml::openElement( 'form', array( 'method' => 'get', - 'action' => $wgScript ) ) . + 'action' => wfScript() ) ) . '
    ' . Xml::element( 'legend', array(), $this->msg( 'mergehistory-box' )->text() ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . + Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . Html::hidden( 'submitted', '1' ) . Html::hidden( 'mergepoint', $this->mTimestamp ) . Xml::openElement( 'table' ) . @@ -171,7 +197,7 @@ class SpecialMergeHistory extends SpecialPage { $haveRevisions = $revisions && $revisions->getNumRows() > 0; $out = $this->getOutput(); - $titleObj = $this->getTitle(); + $titleObj = $this->getPageTitle(); $action = $titleObj->getLocalURL( array( 'action' => 'submit' ) ); # Start the form here $top = Xml::openElement( @@ -203,7 +229,10 @@ class SpecialMergeHistory extends SpecialPage {
    ' . Xml::closeElement( 'table' ) . @@ -252,7 +281,7 @@ class SpecialMergeHistory extends SpecialPage { $last = $this->message['last']; $ts = wfTimestamp( TS_MW, $row->rev_timestamp ); - $checkBox = Xml::radio( 'mergepoint', $ts, false ); + $checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) ); $user = $this->getUser(); @@ -290,9 +319,22 @@ class SpecialMergeHistory extends SpecialPage { $comment = Linker::revComment( $rev ); return Html::rawElement( 'li', array(), - $this->msg( 'mergehistory-revisionrow' )->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() ); + $this->msg( 'mergehistory-revisionrow' ) + ->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() ); } + /** + * Actually attempt the history move + * + * @todo if all versions of page A are moved to B and then a user + * tries to do a reverse-merge via the "unmerge" log link, then page + * A will still be a redirect (as it was after the original merge), + * though it will have the old revisions back from before (as expected). + * The user may have to "undo" the redirect manually to finish the "unmerge". + * Maybe this should delete redirects at the target page of merges? + * + * @return bool Success + */ function merge() { # Get the titles directly from the IDs, in case the target page params # were spoofed. The queries are done based on the IDs, so it's best to @@ -336,7 +378,7 @@ class SpecialMergeHistory extends SpecialPage { return false; } - # Update the revisions + # Get the timestamp pivot condition if ( $this->mTimestamp ) { $timewhere = "rev_timestamp <= {$this->mTimestamp}"; $timestampLimit = wfTimestamp( TS_MW, $this->mTimestamp ); @@ -344,6 +386,18 @@ class SpecialMergeHistory extends SpecialPage { $timewhere = "rev_timestamp <= {$maxtimestamp}"; $timestampLimit = wfTimestamp( TS_MW, $lasttimestamp ); } + # Check that there are not too many revisions to move + $limit = 5000; // avoid too much slave lag + $count = $dbw->selectRowCount( 'revision', '1', + array( 'rev_page' => $this->mTargetID, $timewhere ), + __METHOD__, + array( 'LIMIT' => $limit + 1 ) + ); + if ( $count > $limit ) { + $this->getOutput()->addWikiMsg( 'mergehistory-fail-toobig' ); + + return false; + } # Do the moving... $dbw->update( 'revision', @@ -396,6 +450,7 @@ class SpecialMergeHistory extends SpecialPage { $dbw->insert( 'pagelinks', array( 'pl_from' => $this->mDestID, + 'pl_from_namespace' => $destTitle->getNamespace(), 'pl_namespace' => $destTitle->getNamespace(), 'pl_title' => $destTitle->getDBkey() ), __METHOD__ @@ -420,8 +475,10 @@ class SpecialMergeHistory extends SpecialPage { array( $destTitle->getPrefixedText(), $timestampLimit ), $this->getUser() ); - $this->getOutput()->addWikiMsg( 'mergehistory-success', - $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ); + # @todo message should use redirect=no + $this->getOutput()->addWikiText( $this->msg( 'mergehistory-success', + $targetTitle->getPrefixedText(), $destTitle->getPrefixedText() )->numParams( + $count )->text() ); wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) ); @@ -434,9 +491,13 @@ class SpecialMergeHistory extends SpecialPage { } class MergeHistoryPager extends ReverseChronologicalPager { - public $mForm, $mConds; + /** @var IContextSource */ + public $mForm; + + /** @var array */ + public $mConds; - function __construct( $form, $conds = array(), $source, $dest ) { + function __construct( $form, $conds, $source, $dest ) { $this->mForm = $form; $this->mConds = $conds; $this->title = $source; @@ -490,7 +551,7 @@ class MergeHistoryPager extends ReverseChronologicalPager { function getQueryInfo() { $conds = $this->mConds; $conds['rev_page'] = $this->articleID; - $conds[] = "rev_timestamp < {$this->maxTimestamp}"; + $conds[] = "rev_timestamp < " . $this->mDb->addQuotes( $this->maxTimestamp ); return array( 'tables' => array( 'revision', 'page', 'user' ), diff --git a/includes/specials/SpecialMostinterwikis.php b/includes/specials/SpecialMostinterwikis.php index 98dd68e9..30ccbe5a 100644 --- a/includes/specials/SpecialMostinterwikis.php +++ b/includes/specials/SpecialMostinterwikis.php @@ -92,8 +92,8 @@ class MostinterwikisPage extends QueryPage { } /** - * @param $skin Skin - * @param $result + * @param Skin $skin + * @param object $result * @return string */ function formatResult( $skin, $result ) { diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php index 37593bf9..99f0ecf5 100644 --- a/includes/specials/SpecialMostlinked.php +++ b/includes/specials/SpecialMostlinked.php @@ -93,9 +93,9 @@ class MostlinkedPage extends QueryPage { /** * Make a link to "what links here" for the specified title * - * @param $title Title being queried - * @param string $caption text to display on the link - * @return String + * @param Title $title Title being queried + * @param string $caption Text to display on the link + * @return string */ function makeWlhLink( $title, $caption ) { $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() ); diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php index 0d4641b1..f61a1158 100644 --- a/includes/specials/SpecialMostlinkedcategories.php +++ b/includes/specials/SpecialMostlinkedcategories.php @@ -44,6 +44,7 @@ class MostlinkedCategoriesPage extends QueryPage { 'fields' => array( 'title' => 'cat_title', 'namespace' => NS_CATEGORY, 'value' => 'cat_pages' ), + 'conds' => array( 'cat_pages > 0' ), ); } diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php index c90acb1f..8e6a596d 100644 --- a/includes/specials/SpecialMostlinkedtemplates.php +++ b/includes/specials/SpecialMostlinkedtemplates.php @@ -36,7 +36,7 @@ class MostlinkedTemplatesPage extends QueryPage { /** * Is this report expensive, i.e should it be cached? * - * @return Boolean + * @return bool */ public function isExpensive() { return true; @@ -45,7 +45,7 @@ class MostlinkedTemplatesPage extends QueryPage { /** * Is there a feed available? * - * @return Boolean + * @return bool */ public function isSyndicated() { return false; @@ -54,7 +54,7 @@ class MostlinkedTemplatesPage extends QueryPage { /** * Sort the results in descending order? * - * @return Boolean + * @return bool */ public function sortDescending() { return true; @@ -68,7 +68,6 @@ class MostlinkedTemplatesPage extends QueryPage { 'title' => 'tl_title', 'value' => 'COUNT(*)' ), - 'conds' => array( 'tl_namespace' => NS_TEMPLATE ), 'options' => array( 'GROUP BY' => array( 'tl_namespace', 'tl_title' ) ) ); } @@ -76,7 +75,7 @@ class MostlinkedTemplatesPage extends QueryPage { /** * Pre-cache page existence to speed up link generation * - * @param $db DatabaseBase connection + * @param DatabaseBase $db * @param ResultWrapper $res */ public function preprocessResults( $db, $res ) { @@ -125,7 +124,7 @@ class MostlinkedTemplatesPage extends QueryPage { * * @param Title $title Title to make the link for * @param object $result Result row - * @return String + * @return string */ private function makeWlhLink( $title, $result ) { $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() ); diff --git a/includes/specials/SpecialMostrevisions.php b/includes/specials/SpecialMostrevisions.php index ad6b788d..0471cafe 100644 --- a/includes/specials/SpecialMostrevisions.php +++ b/includes/specials/SpecialMostrevisions.php @@ -23,6 +23,7 @@ * @ingroup SpecialPage * @author Ævar Arnfjörð Bjarmason */ + class MostrevisionsPage extends FewestrevisionsPage { function __construct( $name = 'Mostrevisions' ) { parent::__construct( $name ); diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index 253e6cc3..ec9593f7 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -27,15 +27,35 @@ * @ingroup SpecialPage */ class MovePageForm extends UnlistedSpecialPage { - /** - * Objects - * @var Title - */ - var $oldTitle, $newTitle; - // Text input - var $reason; + /** @var Title */ + protected $oldTitle; + + /** @var Title */ + protected $newTitle; + + + /** @var string Text input */ + protected $reason; + // Checks - var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect, $moveOverShared; + + /** @var bool */ + protected $moveTalk; + + /** @var bool */ + protected $deleteAndMove; + + /** @var bool */ + protected $moveSubpages; + + /** @var bool */ + protected $fixRedirects; + + /** @var bool */ + protected $leaveRedirect; + + /** @var bool */ + protected $moveOverShared; private $watch = false; @@ -106,12 +126,12 @@ class MovePageForm extends UnlistedSpecialPage { /** * Show the form * - * @param array $err error messages. Each item is an error message. + * @param array $err Error messages. Each item is an error message. * It may either be a string message name or array message name and * parameters, like the second argument to OutputPage::wrapWikiMsg(). */ function showForm( $err ) { - global $wgContLang, $wgFixDoubleRedirects, $wgMaximumMovedPages; + global $wgContLang; $this->getSkin()->setRelevantTitle( $this->oldTitle ); @@ -163,9 +183,14 @@ class MovePageForm extends UnlistedSpecialPage { "
    \n$1\n
    ", 'moveuserpage-warning' ); + } elseif ( $this->oldTitle->getNamespace() == NS_CATEGORY ) { + $out->wrapWikiMsg( + "
    \n$1\n
    ", + 'movecategorypage-warning' + ); } - $out->addWikiMsg( $wgFixDoubleRedirects ? + $out->addWikiMsg( $this->getConfig()->get( 'FixDoubleRedirects' ) ? 'movepagetext' : 'movepagetext-noredirectfixer' ); @@ -196,7 +221,7 @@ class MovePageForm extends UnlistedSpecialPage { || ( $oldTitleTalkSubpages && $canMoveSubpage ) ); $dbr = wfGetDB( DB_SLAVE ); - if ( $wgFixDoubleRedirects ) { + if ( $this->getConfig()->get( 'FixDoubleRedirects' ) ) { $hasRedirects = $dbr->selectField( 'redirect', '1', array( 'rd_namespace' => $this->oldTitle->getNamespace(), @@ -281,7 +306,7 @@ class MovePageForm extends UnlistedSpecialPage { 'form', array( 'method' => 'post', - 'action' => $this->getTitle()->getLocalURL( 'action=submit' ), + 'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) . @@ -351,7 +376,16 @@ class MovePageForm extends UnlistedSpecialPage { ); } - if ( $user->isAllowed( 'suppressredirect' ) && $handler->supportsRedirects() ) { + if ( $user->isAllowed( 'suppressredirect' ) ) { + if ( $handler->supportsRedirects() ) { + $isChecked = $this->leaveRedirect; + $options = array(); + } else { + $isChecked = false; + $options = array( + 'disabled' => 'disabled' + ); + } $out->addHTML( "
    @@ -360,7 +394,8 @@ class MovePageForm extends UnlistedSpecialPage { $this->msg( 'move-leave-redirect' )->text(), 'wpLeaveRedirect', 'wpLeaveRedirect', - $this->leaveRedirect + $isChecked, + $options ) . "" @@ -384,6 +419,7 @@ class MovePageForm extends UnlistedSpecialPage { } if ( $canMoveSubpage ) { + $maximumMovedPages = $this->getConfig()->get( 'MaximumMovedPages' ); $out->addHTML( " @@ -400,7 +436,7 @@ class MovePageForm extends UnlistedSpecialPage { ( $this->oldTitle->hasSubpages() ? 'move-subpages' : 'move-talk-subpages' ) - )->numParams( $wgMaximumMovedPages )->params( $wgMaximumMovedPages )->parse() + )->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse() ) . "" @@ -445,8 +481,6 @@ class MovePageForm extends UnlistedSpecialPage { } function doSubmit() { - global $wgMaximumMovedPages, $wgFixDoubleRedirects; - $user = $this->getUser(); if ( $user->pingLimiter( 'move' ) ) { @@ -457,7 +491,7 @@ class MovePageForm extends UnlistedSpecialPage { $nt = $this->newTitle; # don't allow moving to pages with # in - if ( !$nt || $nt->getFragment() != '' ) { + if ( !$nt || $nt->hasFragment() ) { $this->showForm( array( array( 'badtitletext' ) ) ); return; @@ -522,7 +556,7 @@ class MovePageForm extends UnlistedSpecialPage { return; } - if ( $wgFixDoubleRedirects && $this->fixRedirects ) { + if ( $this->getConfig()->get( 'FixDoubleRedirects' ) && $this->fixRedirects ) { DoubleRedirectJob::fixRedirects( 'move', $ot, $nt ); } @@ -532,10 +566,14 @@ class MovePageForm extends UnlistedSpecialPage { $oldLink = Linker::link( $ot, null, - array(), + array( 'id' => 'movepage-oldlink' ), array( 'redirect' => 'no' ) ); - $newLink = Linker::linkKnown( $nt ); + $newLink = Linker::linkKnown( + $nt, + null, + array( 'id' => 'movepage-newlink' ) + ); $oldText = $ot->getPrefixedText(); $newText = $nt->getPrefixedText(); @@ -583,8 +621,8 @@ class MovePageForm extends UnlistedSpecialPage { $dbr = wfGetDB( DB_MASTER ); if ( $this->moveSubpages && ( MWNamespace::hasSubpages( $nt->getNamespace() ) || ( - $this->moveTalk && - MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() ) + $this->moveTalk + && MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() ) ) ) ) { $conds = array( @@ -670,17 +708,21 @@ class MovePageForm extends UnlistedSpecialPage { ); $newLink = Linker::linkKnown( $newSubpage ); - $extraOutput[] = $this->msg( 'movepage-page-moved' )->rawParams( $oldLink, $newLink )->escaped(); + $extraOutput[] = $this->msg( 'movepage-page-moved' ) + ->rawParams( $oldLink, $newLink )->escaped(); ++$count; - if ( $count >= $wgMaximumMovedPages ) { - $extraOutput[] = $this->msg( 'movepage-max-pages' )->numParams( $wgMaximumMovedPages )->escaped(); + $maximumMovedPages = $this->getConfig()->get( 'MaximumMovedPages' ); + if ( $count >= $maximumMovedPages ) { + $extraOutput[] = $this->msg( 'movepage-max-pages' ) + ->numParams( $maximumMovedPages )->escaped(); break; } } else { $oldLink = Linker::linkKnown( $oldSubpage ); $newLink = Linker::link( $newSubpage ); - $extraOutput[] = $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink, $newLink )->escaped(); + $extraOutput[] = $this->msg( 'movepage-page-unmoved' ) + ->rawParams( $oldLink, $newLink )->escaped(); } } } diff --git a/includes/specials/SpecialMyLanguage.php b/includes/specials/SpecialMyLanguage.php new file mode 100644 index 00000000..71b18930 --- /dev/null +++ b/includes/specials/SpecialMyLanguage.php @@ -0,0 +1,93 @@ +findTitle( $par ); + // Go to the main page if given invalid title. + if ( !$title ) { + $title = Title::newMainPage(); + } + return $title; + } + + /** + * Assuming the user's interface language is fi. Given input Page, it + * returns Page/fi if it exists, otherwise Page. Given input Page/de, + * it returns Page/fi if it exists, otherwise Page/de if it exists, + * otherwise Page. + * + * @param string $par + * @return Title|null + */ + public function findTitle( $par ) { + // base = title without language code suffix + // provided = the title as it was given + $base = $provided = Title::newFromText( $par ); + + if ( $base && strpos( $par, '/' ) !== false ) { + $pos = strrpos( $par, '/' ); + $basepage = substr( $par, 0, $pos ); + $code = substr( $par, $pos + 1 ); + if ( strlen( $code ) && Language::isKnownLanguageTag( $code ) ) { + $base = Title::newFromText( $basepage ); + } + } + + if ( !$base ) { + return null; + } + + $uiCode = $this->getLanguage()->getCode(); + $proposed = $base->getSubpage( $uiCode ); + if ( $uiCode !== $this->getConfig()->get( 'LanguageCode' ) && $proposed && $proposed->exists() ) { + return $proposed; + } elseif ( $provided && $provided->exists() ) { + return $provided; + } else { + return $base; + } + } +} diff --git a/includes/specials/SpecialMyRedirectPages.php b/includes/specials/SpecialMyRedirectPages.php new file mode 100644 index 00000000..9b8d52bb --- /dev/null +++ b/includes/specials/SpecialMyRedirectPages.php @@ -0,0 +1,114 @@ +getUser()->getName() . '/' . $subpage ); + } else { + return Title::makeTitle( NS_USER, $this->getUser()->getName() ); + } + } +} + +/** + * Special page pointing to current user's talk page. + * + * @ingroup SpecialPage + */ +class SpecialMytalk extends RedirectSpecialArticle { + function __construct() { + parent::__construct( 'Mytalk' ); + } + + function getRedirect( $subpage ) { + if ( strval( $subpage ) !== '' ) { + return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() . '/' . $subpage ); + } else { + return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() ); + } + } +} + +/** + * Special page pointing to current user's contributions. + * + * @ingroup SpecialPage + */ +class SpecialMycontributions extends RedirectSpecialPage { + function __construct() { + parent::__construct( 'Mycontributions' ); + $this->mAllowedRedirectParams = array( 'limit', 'namespace', 'tagfilter', + 'offset', 'dir', 'year', 'month', 'feed' ); + } + + function getRedirect( $subpage ) { + return SpecialPage::getTitleFor( 'Contributions', $this->getUser()->getName() ); + } +} + +/** + * Special page pointing to current user's uploaded files. + * + * @ingroup SpecialPage + */ +class SpecialMyuploads extends RedirectSpecialPage { + function __construct() { + parent::__construct( 'Myuploads' ); + $this->mAllowedRedirectParams = array( 'limit', 'ilshowall', 'ilsearch' ); + } + + function getRedirect( $subpage ) { + return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); + } +} + +/** + * Special page pointing to current user's uploaded files (including old versions). + * + * @ingroup SpecialPage + */ +class SpecialAllMyUploads extends RedirectSpecialPage { + function __construct() { + parent::__construct( 'AllMyUploads' ); + $this->mAllowedRedirectParams = array( 'limit', 'ilsearch' ); + } + + function getRedirect( $subpage ) { + $this->mAddedRedirectParams['ilshowall'] = 1; + + return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); + } +} diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index 37d29734..546c1914 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -20,6 +20,7 @@ * @file * @ingroup SpecialPage */ + class SpecialNewFiles extends IncludableSpecialPage { public function __construct() { parent::__construct( 'Newimages' ); @@ -32,6 +33,7 @@ class SpecialNewFiles extends IncludableSpecialPage { $pager = new NewFilesPager( $this->getContext(), $par ); if ( !$this->including() ) { + $this->setTopText(); $form = $pager->getForm(); $form->prepareForm(); $form->displayForm( '' ); @@ -46,6 +48,25 @@ class SpecialNewFiles extends IncludableSpecialPage { protected function getGroupName() { return 'changes'; } + + /** + * Send the text to be displayed above the options + */ + function setTopText() { + global $wgContLang; + + $message = $this->msg( 'newimagestext' )->inContentLanguage(); + if ( !$message->isDisabled() ) { + $this->getOutput()->addWikiText( + Html::rawElement( 'p', + array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ), + "\n" . $message->plain() . "\n" + ), + /* $lineStart */ false, + /* $interface */ false + ); + } + } } /** @@ -55,7 +76,7 @@ class NewFilesPager extends ReverseChronologicalPager { /** * @var ImageGallery */ - var $gallery; + protected $gallery; function __construct( IContextSource $context, $par = null ) { $this->like = $context->getRequest()->getText( 'like' ); @@ -68,7 +89,6 @@ class NewFilesPager extends ReverseChronologicalPager { } function getQueryInfo() { - global $wgMiserMode; $conds = $jconds = array(); $tables = array( 'image' ); @@ -88,7 +108,7 @@ class NewFilesPager extends ReverseChronologicalPager { } } - if ( !$wgMiserMode && $this->like !== null ) { + if ( !$this->getConfig()->get( 'MiserMode' ) && $this->like !== null ) { $dbr = wfGetDB( DB_SLAVE ); $likeObj = Title::newFromURL( $this->like ); if ( $likeObj instanceof Title ) { @@ -120,12 +140,11 @@ class NewFilesPager extends ReverseChronologicalPager { // Note that null for mode is taken to mean use default. $mode = $this->getRequest()->getVal( 'gallerymode', null ); try { - $this->gallery = ImageGalleryBase::factory( $mode ); + $this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() ); } catch ( MWException $e ) { // User specified something invalid, fallback to default. - $this->gallery = ImageGalleryBase::factory(); + $this->gallery = ImageGalleryBase::factory( false, $this->getContext() ); } - $this->gallery->setContext( $this->getContext() ); } return ''; @@ -152,8 +171,6 @@ class NewFilesPager extends ReverseChronologicalPager { } function getForm() { - global $wgMiserMode; - $fields = array( 'like' => array( 'type' => 'text', @@ -162,7 +179,7 @@ class NewFilesPager extends ReverseChronologicalPager { ), 'showbots' => array( 'type' => 'check', - 'label' => $this->msg( 'showhidebots', $this->msg( 'show' )->plain() )->escaped(), + 'label-message' => 'newimages-showbots', 'name' => 'showbots', ), 'limit' => array( @@ -177,7 +194,7 @@ class NewFilesPager extends ReverseChronologicalPager { ), ); - if ( $wgMiserMode ) { + if ( $this->getConfig()->get( 'MiserMode' ) ) { unset( $fields['like'] ); } diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 43d48558..0b70bb7e 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -27,15 +27,12 @@ * @ingroup SpecialPage */ class SpecialNewpages extends IncludableSpecialPage { - // Stored objects - /** * @var FormOptions */ protected $opts; protected $customFilters; - // Some internal settings protected $showNavigation = false; public function __construct() { @@ -43,8 +40,6 @@ class SpecialNewpages extends IncludableSpecialPage { } protected function setup( $par ) { - global $wgEnableNewpagesUserFilter; - // Options $opts = new FormOptions(); $this->opts = $opts; // bind @@ -74,9 +69,6 @@ class SpecialNewpages extends IncludableSpecialPage { // Validate $opts->validateIntBounds( 'limit', 0, 5000 ); - if ( !$wgEnableNewpagesUserFilter ) { - $opts->setValue( 'username', '' ); - } } protected function parseParams( $par ) { @@ -124,8 +116,7 @@ class SpecialNewpages extends IncludableSpecialPage { /** * Show a form for filtering namespace and username * - * @param $par String - * @return String + * @param string $par */ public function execute( $par ) { $out = $this->getOutput(); @@ -194,7 +185,7 @@ class SpecialNewpages extends IncludableSpecialPage { $changed = $this->opts->getChangedValues(); unset( $changed['offset'] ); // Reset offset if query type changes - $self = $this->getTitle(); + $self = $this->getPageTitle(); foreach ( $filters as $key => $msg ) { $onoff = 1 - $this->opts->getValue( $key ); $link = Linker::link( $self, $showhide[$onoff], array(), @@ -207,8 +198,6 @@ class SpecialNewpages extends IncludableSpecialPage { } protected function form() { - global $wgEnableNewpagesUserFilter, $wgScript; - // Consume values $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW $namespace = $this->opts->consumeValue( 'namespace' ); @@ -232,8 +221,8 @@ class SpecialNewpages extends IncludableSpecialPage { list( $tagFilterLabel, $tagFilterSelector ) = $tagFilter; } - $form = Xml::openElement( 'form', array( 'action' => $wgScript ) ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . + $form = Xml::openElement( 'form', array( 'action' => wfScript() ) ) . + Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . Xml::fieldset( $this->msg( 'newpages' )->text() ) . Xml::openElement( 'table', array( 'id' => 'mw-newpages-table' ) ) . ' @@ -268,15 +257,14 @@ class SpecialNewpages extends IncludableSpecialPage { $tagFilterSelector . '' ) : '' ) . - ( $wgEnableNewpagesUserFilter ? - ' + ' - ' : '' ) . + ' . ''; } $out .= ""; $n++; - if ( $n % 3 == 0 ) { + if ( $n % $this->columns == 0 ) { $out .= ''; } } - if ( $n % 3 != 0 ) { + if ( $n % $this->columns != 0 ) { $out .= ''; } @@ -249,7 +252,7 @@ class SpecialPrefixindex extends SpecialAllpages { $out2 = ''; } else { $nsForm = $this->namespacePrefixForm( $namespace, $prefix ); - $self = $this->getTitle(); + $self = $this->getPageTitle(); $out2 = Xml::openElement( 'table', array( 'id' => 'mw-prefixindex-nav-table' ) ) . '' . "\n" . @@ -375,7 +433,12 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { Xml::label( $this->msg( 'revdelete-otherreason' )->text(), 'wpReason' ) . '' . '' . "\n" . '' . @@ -389,12 +452,8 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { Html::hidden( 'target', $this->targetObj->getPrefixedText() ) . Html::hidden( 'type', $this->typeName ) . Html::hidden( 'ids', implode( ',', $this->ids ) ) . - Xml::closeElement( 'fieldset' ) . "\n"; - } else { - $out = ''; - } - if ( $this->mIsAllowed ) { - $out .= Xml::closeElement( 'form' ) . "\n"; + Xml::closeElement( 'fieldset' ) . "\n" . + Xml::closeElement( 'form' ) . "\n"; // Show link to edit the dropdown reasons if ( $this->getUser()->isAllowed( 'editinterface' ) ) { $title = Title::makeTitle( NS_MEDIAWIKI, 'Revdelete-reason-dropdown' ); @@ -406,6 +465,8 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { ); $out .= Xml::tags( 'p', array( 'class' => 'mw-revdel-editreasons' ), $link ) . "\n"; } + } else { + $out = ''; } $this->getOutput()->addHTML( $out ); } @@ -415,17 +476,23 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { * @todo FIXME: Wikimedia-specific policy text */ protected function addUsageText() { - $this->getOutput()->addWikiMsg( 'revdelete-text' ); + // Messages: revdelete-text-text, revdelete-text-file, logdelete-text + $this->getOutput()->wrapWikiMsg( + "$1\n$2", $this->typeLabels['text'], + 'revdelete-text-others' + ); + if ( $this->getUser()->isAllowed( 'suppressrevision' ) ) { $this->getOutput()->addWikiMsg( 'revdelete-suppress-text' ); } + if ( $this->mIsAllowed ) { $this->getOutput()->addWikiMsg( 'revdelete-confirm' ); } } /** - * @return String: HTML + * @return string HTML */ protected function buildCheckBoxes() { $html = '
      ' . - Xml::submitButton( $this->msg( 'mergehistory-submit' )->text(), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) . + Xml::submitButton( + $this->msg( 'mergehistory-submit' )->text(), + array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) + ) . '
    ' . Xml::label( $this->msg( 'newpages-username' )->text(), 'mw-np-username' ) . ' ' . Xml::input( 'username', 30, $userText, array( 'id' => 'mw-np-username' ) ) . '
    ' . Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . @@ -301,7 +289,7 @@ class SpecialNewpages extends IncludableSpecialPage { * size, user links, and a comment * * @param object $result Result row - * @return String + * @return string */ public function formatRow( $result ) { $title = Title::newFromRow( $result ); @@ -394,14 +382,15 @@ class SpecialNewpages extends IncludableSpecialPage { $oldTitleText = $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped(); } - return "{$time} {$dm}{$plink} {$hist} {$dm}{$length} {$dm}{$ulink} {$comment} {$tagDisplay} {$oldTitleText}\n"; + return "{$time} {$dm}{$plink} {$hist} {$dm}{$length} " + . "{$dm}{$ulink} {$comment} {$tagDisplay} {$oldTitleText}\n"; } /** * Should a specific result row provide "patrollable" links? * * @param object $result Result row - * @return Boolean + * @return bool */ protected function patrollable( $result ) { return ( $this->getUser()->useNPPatrol() && !$result->rc_patrolled ); @@ -410,32 +399,31 @@ class SpecialNewpages extends IncludableSpecialPage { /** * Output a subscription feed listing recent edits to this page. * - * @param $type String + * @param string $type */ protected function feed( $type ) { - global $wgFeed, $wgFeedClasses, $wgFeedLimit; - - if ( !$wgFeed ) { + if ( !$this->getConfig()->get( 'Feed' ) ) { $this->getOutput()->addWikiMsg( 'feed-unavailable' ); return; } - if ( !isset( $wgFeedClasses[$type] ) ) { + $feedClasses = $this->getConfig()->get( 'FeedClasses' ); + if ( !isset( $feedClasses[$type] ) ) { $this->getOutput()->addWikiMsg( 'feed-invalid' ); return; } - $feed = new $wgFeedClasses[$type]( + $feed = new $feedClasses[$type]( $this->feedTitle(), $this->msg( 'tagline' )->text(), - $this->getTitle()->getFullURL() + $this->getPageTitle()->getFullURL() ); $pager = new NewPagesPager( $this, $this->opts ); $limit = $this->opts->getValue( 'limit' ); - $pager->mLimit = min( $limit, $wgFeedLimit ); + $pager->mLimit = min( $limit, $this->getConfig()->get( 'FeedLimit' ) ); $feed->outHeader(); if ( $pager->getNumRows() > 0 ) { @@ -447,10 +435,11 @@ class SpecialNewpages extends IncludableSpecialPage { } protected function feedTitle() { - global $wgLanguageCode, $wgSitename; $desc = $this->getDescription(); + $code = $this->getConfig()->get( 'LanguageCode' ); + $sitename = $this->getConfig()->get( 'Sitename' ); - return "$wgSitename - $desc [$wgLanguageCode]"; + return "$sitename - $desc [$code]"; } protected function feedItem( $row ) { @@ -514,7 +503,6 @@ class NewPagesPager extends ReverseChronologicalPager { } function getQueryInfo() { - global $wgEnableNewpagesUserFilter; $conds = array(); $conds['rc_new'] = 1; @@ -524,19 +512,17 @@ class NewPagesPager extends ReverseChronologicalPager { $username = $this->opts->getValue( 'username' ); $user = Title::makeTitleSafe( NS_USER, $username ); + $rcIndexes = array(); + if ( $namespace !== false ) { if ( $this->opts->getValue( 'invert' ) ) { $conds[] = 'rc_namespace != ' . $this->mDb->addQuotes( $namespace ); } else { $conds['rc_namespace'] = $namespace; } - $rcIndexes = array( 'new_name_timestamp' ); - } else { - $rcIndexes = array( 'rc_timestamp' ); } - # $wgEnableNewpagesUserFilter - temp WMF hack - if ( $wgEnableNewpagesUserFilter && $user ) { + if ( $user ) { $conds['rc_user_text'] = $user->getText(); $rcIndexes = 'rc_user_text'; } elseif ( User::groupHasPermission( '*', 'createpage' ) && @@ -572,11 +558,17 @@ class NewPagesPager extends ReverseChronologicalPager { wfRunHooks( 'SpecialNewpagesConditions', array( &$this, $this->opts, &$conds, &$tables, &$fields, &$join_conds ) ); + $options = array(); + + if ( $rcIndexes ) { + $options = array( 'USE INDEX' => array( 'recentchanges' => $rcIndexes ) ); + } + $info = array( 'tables' => $tables, 'fields' => $fields, 'conds' => $conds, - 'options' => array( 'USE INDEX' => array( 'recentchanges' => $rcIndexes ) ), + 'options' => $options, 'join_conds' => $join_conds ); diff --git a/includes/specials/SpecialPageLanguage.php b/includes/specials/SpecialPageLanguage.php new file mode 100644 index 00000000..2acf23cd --- /dev/null +++ b/includes/specials/SpecialPageLanguage.php @@ -0,0 +1,195 @@ +getOutput()->addModules( 'mediawiki.special.pageLanguage' ); + } + + protected function getFormFields() { + // Get default from the subpage of Special page + $defaultName = $this->par; + + $page = array(); + $page['pagename'] = array( + 'type' => 'text', + 'label-message' => 'pagelang-name', + 'default' => $defaultName, + ); + + // Options for whether to use the default language or select language + $selectoptions = array( + (string)$this->msg( 'pagelang-use-default' )->escaped() => 1, + (string)$this->msg( 'pagelang-select-lang' )->escaped() => 2, + ); + $page['selectoptions'] = array( + 'id' => 'mw-pl-options', + 'type' => 'radio', + 'options' => $selectoptions, + 'default' => 1 + ); + + // Building a language selector + $userLang = $this->getLanguage()->getCode(); + $languages = Language::fetchLanguageNames( $userLang, 'mwfile' ); + ksort( $languages ); + $options = array(); + foreach ( $languages as $code => $name ) { + $options["$code - $name"] = $code; + } + + $page['language'] = array( + 'id' => 'mw-pl-languageselector', + 'cssclass' => 'mw-languageselector', + 'type' => 'select', + 'options' => $options, + 'label-message' => 'pagelang-language', + 'default' => $this->getConfig()->get( 'LanguageCode' ), + ); + + return $page; + } + + protected function postText() { + return $this->showLogFragment( $this->par ); + } + + public function alterForm( HTMLForm $form ) { + $form->setDisplayFormat( 'vform' ); + $form->setWrapperLegend( false ); + wfRunHooks( 'LanguageSelector', array( $this->getOutput(), 'mw-languageselector' ) ); + } + + /** + * + * @param array $data + * @return bool + */ + public function onSubmit( array $data ) { + $title = Title::newFromText( $data['pagename'] ); + + // Check if title is valid + if ( !$title ) { + return false; + } + + // Get the default language for the wiki + // Returns the default since the page is not loaded from DB + $defLang = $title->getPageLanguage()->getCode(); + + $pageId = $title->getArticleID(); + + // Check if article exists + if ( !$pageId ) { + return false; + } + + // Load the page language from DB + $dbw = wfGetDB( DB_MASTER ); + $langOld = $dbw->selectField( + 'page', + 'page_lang', + array( 'page_id' => $pageId ), + __METHOD__ + ); + + // Url to redirect to after the operation + $this->goToUrl = $title->getFullURL(); + + // Check if user wants to use default language + if ( $data['selectoptions'] == 1 ) { + $langNew = null; + } else { + $langNew = $data['language']; + } + + // No change in language + if ( $langNew === $langOld ) { + return false; + } + + // Hardcoded [def] if the language is set to null + $logOld = $langOld ? $langOld : $defLang . '[def]'; + $logNew = $langNew ? $langNew : $defLang . '[def]'; + + // Writing new page language to database + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( + 'page', + array( 'page_lang' => $langNew ), + array( + 'page_id' => $pageId, + 'page_lang' => $langOld + ), + __METHOD__ + ); + + if ( !$dbw->affectedRows() ) { + return false; + } + + // Logging change of language + $logParams = array( + '4::oldlanguage' => $logOld, + '5::newlanguage' => $logNew + ); + $entry = new ManualLogEntry( 'pagelang', 'pagelang' ); + $entry->setPerformer( $this->getUser() ); + $entry->setTarget( $title ); + $entry->setParameters( $logParams ); + + $logid = $entry->insert(); + $entry->publish( $logid ); + + return true; + } + + public function onSuccess() { + // Success causes a redirect + $this->getOutput()->redirect( $this->goToUrl ); + } + + function showLogFragment( $title ) { + $moveLogPage = new LogPage( 'pagelang' ); + $out1 = Xml::element( 'h2', null, $moveLogPage->getName()->text() ); + $out2 = ''; + LogEventsList::showLogExtract( $out2, 'pagelang', $title ); + return $out1 . $out2; + } +} diff --git a/includes/specials/SpecialPagesWithProp.php b/includes/specials/SpecialPagesWithProp.php index e22b42a3..f5b19cc6 100644 --- a/includes/specials/SpecialPagesWithProp.php +++ b/includes/specials/SpecialPagesWithProp.php @@ -30,6 +30,7 @@ */ class SpecialPagesWithProp extends QueryPage { private $propName = null; + private $existingPropNames = null; function __construct( $name = 'PagesWithProp' ) { parent::__construct( $name ); @@ -47,18 +48,7 @@ class SpecialPagesWithProp extends QueryPage { $request = $this->getRequest(); $propname = $request->getVal( 'propname', $par ); - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( - 'page_props', - 'pp_propname', - '', - __METHOD__, - array( 'DISTINCT', 'ORDER BY' => 'pp_propname' ) - ); - $propnames = array(); - foreach ( $res as $row ) { - $propnames[$row->pp_propname] = $row->pp_propname; - } + $propnames = $this->getExistingPropNames(); $form = new HTMLForm( array( 'propname' => array( @@ -88,6 +78,18 @@ class SpecialPagesWithProp extends QueryPage { parent::execute( $data['propname'] ); } + /** + * Return an array of subpages beginning with $search that this special page will accept. + * + * @param string $search Prefix to search for + * @param int $limit Maximum number of results to return + * @return string[] Matching subpages + */ + public function prefixSearchSubpages( $search, $limit = 10 ) { + $subpages = array_keys( $this->getExistingPropNames() ); + return self::prefixSearchArray( $search, $limit, $subpages ); + } + /** * Disable RSS/Atom feeds * @return bool @@ -150,6 +152,25 @@ class SpecialPagesWithProp extends QueryPage { return $ret; } + public function getExistingPropNames() { + if ( $this->existingPropNames === null ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( + 'page_props', + 'pp_propname', + '', + __METHOD__, + array( 'DISTINCT', 'ORDER BY' => 'pp_propname' ) + ); + $propnames = array(); + foreach ( $res as $row ) { + $propnames[$row->pp_propname] = $row->pp_propname; + } + $this->existingPropNames = $propnames; + } + return $this->existingPropNames; + } + protected function getGroupName() { return 'pages'; } diff --git a/includes/specials/SpecialPasswordReset.php b/includes/specials/SpecialPasswordReset.php index d9faacca..3061c85b 100644 --- a/includes/specials/SpecialPasswordReset.php +++ b/includes/specials/SpecialPasswordReset.php @@ -62,9 +62,10 @@ class SpecialPasswordReset extends FormSpecialPage { } protected function getFormFields() { - global $wgPasswordResetRoutes, $wgAuth; + global $wgAuth; + $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' ); $a = array(); - if ( isset( $wgPasswordResetRoutes['username'] ) && $wgPasswordResetRoutes['username'] ) { + if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) { $a['Username'] = array( 'type' => 'text', 'label-message' => 'passwordreset-username', @@ -75,14 +76,14 @@ class SpecialPasswordReset extends FormSpecialPage { } } - if ( isset( $wgPasswordResetRoutes['email'] ) && $wgPasswordResetRoutes['email'] ) { + if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) { $a['Email'] = array( 'type' => 'email', 'label-message' => 'passwordreset-email', ); } - if ( isset( $wgPasswordResetRoutes['domain'] ) && $wgPasswordResetRoutes['domain'] ) { + if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) { $domains = $wgAuth->domainList(); $a['Domain'] = array( 'type' => 'select', @@ -103,7 +104,7 @@ class SpecialPasswordReset extends FormSpecialPage { } public function alterForm( HTMLForm $form ) { - global $wgPasswordResetRoutes; + $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' ); $form->setDisplayFormat( 'vform' ); // Turn the old-school line around the form off. @@ -112,14 +113,16 @@ class SpecialPasswordReset extends FormSpecialPage { // from a FormSpecialPage class. $form->setWrapperLegend( false ); + $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) ); + $i = 0; - if ( isset( $wgPasswordResetRoutes['username'] ) && $wgPasswordResetRoutes['username'] ) { + if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) { $i++; } - if ( isset( $wgPasswordResetRoutes['email'] ) && $wgPasswordResetRoutes['email'] ) { + if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) { $i++; } - if ( isset( $wgPasswordResetRoutes['domain'] ) && $wgPasswordResetRoutes['domain'] ) { + if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) { $i++; } @@ -133,10 +136,10 @@ class SpecialPasswordReset extends FormSpecialPage { * Process the form. At this point we know that the user passes all the criteria in * userCanExecute(), and if the data array contains 'Username', etc, then Username * resets are allowed. - * @param $data array + * @param array $data * @throws MWException * @throws ThrottledError|PermissionsError - * @return Bool|Array + * @return bool|array */ public function onSubmit( array $data ) { global $wgAuth; @@ -220,19 +223,16 @@ class SpecialPasswordReset extends FormSpecialPage { // Check against password throttle foreach ( $users as $user ) { if ( $user->isPasswordReminderThrottled() ) { - global $wgPasswordReminderResendTime; # Round the time in hours to 3 d.p., in case someone is specifying # minutes or seconds. return array( array( 'throttled-mailpassword', - round( $wgPasswordReminderResendTime, 3 ) + round( $this->getConfig()->get( 'PasswordReminderResendTime' ), 3 ) ) ); } } - global $wgNewPasswordExpiry; - // All the users will have the same email address if ( $firstUser->getEmail() == '' ) { // This won't be reachable from the email route, so safe to expose the username @@ -271,12 +271,12 @@ class SpecialPasswordReset extends FormSpecialPage { $passwordBlock, count( $passwords ), '<' . Title::newMainPage()->getCanonicalURL() . '>', - round( $wgNewPasswordExpiry / 86400 ) + round( $this->getConfig()->get( 'NewPasswordExpiry' ) / 86400 ) ); $title = $this->msg( 'passwordreset-emailtitle' ); - $this->result = $firstUser->sendMail( $title->escaped(), $this->email->text() ); + $this->result = $firstUser->sendMail( $title->text(), $this->email->text() ); if ( isset( $data['Capture'] ) && $data['Capture'] ) { // Save the user, will be used if an error occurs when sending the email @@ -318,11 +318,12 @@ class SpecialPasswordReset extends FormSpecialPage { } protected function canChangePassword( User $user ) { - global $wgPasswordResetRoutes, $wgEnableEmail, $wgAuth; + global $wgAuth; + $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' ); // Maybe password resets are disabled, or there are no allowable routes - if ( !is_array( $wgPasswordResetRoutes ) || - !in_array( true, array_values( $wgPasswordResetRoutes ) ) + if ( !is_array( $resetRoutes ) || + !in_array( true, array_values( $resetRoutes ) ) ) { return 'passwordreset-disabled'; } @@ -333,7 +334,7 @@ class SpecialPasswordReset extends FormSpecialPage { } // Maybe email features have been disabled - if ( !$wgEnableEmail ) { + if ( !$this->getConfig()->get( 'EnableEmail' ) ) { return 'passwordreset-emaildisabled'; } @@ -348,7 +349,7 @@ class SpecialPasswordReset extends FormSpecialPage { /** * Hide the password reset page if resets are disabled. - * @return Bool + * @return bool */ function isListed() { if ( $this->canChangePassword( $this->getUser() ) === true ) { diff --git a/includes/specials/SpecialPermanentLink.php b/includes/specials/SpecialPermanentLink.php new file mode 100644 index 00000000..17115e88 --- /dev/null +++ b/includes/specials/SpecialPermanentLink.php @@ -0,0 +1,45 @@ +mAllowedRedirectParams = array(); + } + + function getRedirect( $subpage ) { + $subpage = intval( $subpage ); + if ( $subpage === 0 ) { + # throw an error page when no subpage was given + throw new ErrorPageError( 'nopagetitle', 'nopagetext' ); + } + $this->mAddedRedirectParams['oldid'] = $subpage; + + return true; + } +} diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index ecee0bb7..cea00fa6 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -37,14 +37,7 @@ class SpecialPreferences extends SpecialPage { $out = $this->getOutput(); $out->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc. - $user = $this->getUser(); - if ( $user->isAnon() ) { - throw new ErrorPageError( - 'prefsnologin', - 'prefsnologintext', - array( $this->getTitle()->getPrefixedDBkey() ) - ); - } + $this->requireLogin( 'prefsnologintext2' ); $this->checkReadOnly(); if ( $par == 'reset' ) { @@ -62,7 +55,7 @@ class SpecialPreferences extends SpecialPage { ); } - $htmlForm = Preferences::getFormObject( $user, $this->getContext() ); + $htmlForm = Preferences::getFormObject( $this->getUser(), $this->getContext() ); $htmlForm->setSubmitCallback( array( 'Preferences', 'tryUISubmit' ) ); $htmlForm->show(); @@ -76,10 +69,11 @@ class SpecialPreferences extends SpecialPage { $this->getOutput()->addWikiMsg( 'prefs-reset-intro' ); $context = new DerivativeContext( $this->getContext() ); - $context->setTitle( $this->getTitle( 'reset' ) ); // Reset subpage + $context->setTitle( $this->getPageTitle( 'reset' ) ); // Reset subpage $htmlForm = new HTMLForm( array(), $context, 'prefs-restore' ); $htmlForm->setSubmitTextMsg( 'restoreprefs' ); + $htmlForm->setSubmitDestructive(); $htmlForm->setSubmitCallback( array( $this, 'submitReset' ) ); $htmlForm->suppressReset(); @@ -95,7 +89,7 @@ class SpecialPreferences extends SpecialPage { $user->resetOptions( 'all', $this->getContext() ); $user->saveSettings(); - $url = $this->getTitle()->getFullURL( 'success' ); + $url = $this->getPageTitle()->getFullURL( 'success' ); $this->getOutput()->redirect( $url ); diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php index 0d065b09..2e67e2b5 100644 --- a/includes/specials/SpecialPrefixindex.php +++ b/includes/specials/SpecialPrefixindex.php @@ -26,7 +26,7 @@ * * @ingroup SpecialPage */ -class SpecialPrefixindex extends SpecialAllpages { +class SpecialPrefixindex extends SpecialAllPages { /** * Whether to remove the searched prefix from the displayed link. Useful @@ -36,6 +36,9 @@ class SpecialPrefixindex extends SpecialAllpages { protected $hideRedirects = false; + // number of columns in output table + protected $columns = 3; + // Inherit $maxPerPage function __construct() { @@ -44,7 +47,7 @@ class SpecialPrefixindex extends SpecialAllpages { /** * Entry point : initialise variables and call subfunctions. - * @param string $par becomes "FOO" when called like Special:Prefixindex/FOO (default null) + * @param string $par Becomes "FOO" when called like Special:Prefixindex/FOO (default null) */ function execute( $par ) { global $wgContLang; @@ -63,16 +66,17 @@ class SpecialPrefixindex extends SpecialAllpages { $namespace = (int)$ns; // if no namespace given, use 0 (NS_MAIN). $this->hideRedirects = $request->getBool( 'hideredirects', $this->hideRedirects ); $this->stripPrefix = $request->getBool( 'stripprefix', $this->stripPrefix ); + $this->columns = $request->getInt( 'columns', $this->columns ); $namespaces = $wgContLang->getNamespaces(); $out->setPageTitle( - ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) ) + ( $namespace > 0 && array_key_exists( $namespace, $namespaces ) ) ? $this->msg( 'prefixindex-namespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : $this->msg( 'prefixindex' ) ); $showme = ''; - if ( isset( $par ) ) { + if ( $par !== null ) { $showme = $par; } elseif ( $prefix != '' ) { $showme = $prefix; @@ -92,16 +96,14 @@ class SpecialPrefixindex extends SpecialAllpages { /** * HTML for the top form - * @param $namespace Integer: a namespace constant (default NS_MAIN). - * @param string $from dbKey we are starting listing at. + * @param int $namespace A namespace constant (default NS_MAIN). + * @param string $from DbKey we are starting listing at. * @return string */ protected function namespacePrefixForm( $namespace = NS_MAIN, $from = '' ) { - global $wgScript; - $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); - $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); - $out .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ); + $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getConfig()->get( 'Script' ) ) ); + $out .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ); $out .= Xml::openElement( 'fieldset' ); $out .= Xml::element( 'legend', null, $this->msg( 'allpages' )->text() ); $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); @@ -149,9 +151,9 @@ class SpecialPrefixindex extends SpecialAllpages { } /** - * @param $namespace Integer, default NS_MAIN - * @param $prefix String - * @param string $from list all pages from this name (default FALSE) + * @param int $namespace Default NS_MAIN + * @param string $prefix + * @param string $from List all pages from this name (default false) */ protected function showPrefixChunk( $namespace = NS_MAIN, $prefix, $from = null ) { global $wgContLang; @@ -163,10 +165,11 @@ class SpecialPrefixindex extends SpecialAllpages { $fromList = $this->getNamespaceKeyAndText( $namespace, $from ); $prefixList = $this->getNamespaceKeyAndText( $namespace, $prefix ); $namespaces = $wgContLang->getNamespaces(); + $res = null; if ( !$prefixList || !$fromList ) { $out = $this->msg( 'allpagesbadtitle' )->parseAsBlock(); - } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) { + } elseif ( !array_key_exists( $namespace, $namespaces ) ) { // Show errormessage and reset to NS_MAIN $out = $this->msg( 'allpages-bad-ns', $namespace )->parse(); $namespace = NS_MAIN; @@ -203,7 +206,7 @@ class SpecialPrefixindex extends SpecialAllpages { $n = 0; if ( $res->numRows() > 0 ) { - $out = Xml::openElement( 'table', array( 'id' => 'mw-prefixindex-list-table' ) ); + $out = Xml::openElement( 'table', array( 'class' => 'mw-prefixindex-list-table' ) ); $prefixLength = strlen( $prefix ); while ( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { @@ -224,17 +227,17 @@ class SpecialPrefixindex extends SpecialAllpages { } else { $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; } - if ( $n % 3 == 0 ) { + if ( $n % $this->columns == 0 ) { $out .= '
    $link
    ' . @@ -257,14 +260,13 @@ class SpecialPrefixindex extends SpecialAllpages { ' '; - if ( isset( $res ) && $res && ( $n == $this->maxPerPage ) && - ( $s = $res->fetchObject() ) - ) { + if ( $res && ( $n == $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { $query = array( 'from' => $s->page_title, 'prefix' => $prefix, 'hideredirects' => $this->hideRedirects, 'stripprefix' => $this->stripPrefix, + 'columns' => $this->columns, ); if ( $namespace || $prefix == '' ) { diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php index 3de6ea24..0ba73857 100644 --- a/includes/specials/SpecialProtectedpages.php +++ b/includes/specials/SpecialProtectedpages.php @@ -27,7 +27,6 @@ * @ingroup SpecialPage */ class SpecialProtectedpages extends SpecialPage { - protected $IdLevel = 'level'; protected $IdType = 'type'; @@ -38,6 +37,7 @@ class SpecialProtectedpages extends SpecialPage { public function execute( $par ) { $this->setHeaders(); $this->outputHeader(); + $this->getOutput()->addModuleStyles( 'mediawiki.special' ); // Purge expired entries on one in every 10 queries if ( !mt_rand( 0, 10 ) ) { @@ -52,6 +52,7 @@ class SpecialProtectedpages extends SpecialPage { $ns = $request->getIntOrNull( 'namespace' ); $indefOnly = $request->getBool( 'indefonly' ) ? 1 : 0; $cascadeOnly = $request->getBool( 'cascadeonly' ) ? 1 : 0; + $noRedirect = $request->getBool( 'noredirect' ) ? 1 : 0; $pager = new ProtectedPagesPager( $this, @@ -62,7 +63,8 @@ class SpecialProtectedpages extends SpecialPage { $sizetype, $size, $indefOnly, - $cascadeOnly + $cascadeOnly, + $noRedirect ); $this->getOutput()->addHTML( $this->showOptions( @@ -72,137 +74,34 @@ class SpecialProtectedpages extends SpecialPage { $sizetype, $size, $indefOnly, - $cascadeOnly + $cascadeOnly, + $noRedirect ) ); if ( $pager->getNumRows() ) { - $this->getOutput()->addHTML( - $pager->getNavigationBar() . - '
      ' . $pager->getBody() . '
    ' . - $pager->getNavigationBar() - ); + $this->getOutput()->addParserOutputContent( $pager->getFullOutput() ); } else { $this->getOutput()->addWikiMsg( 'protectedpagesempty' ); } } /** - * Callback function to output a restriction - * @param Title $row Protected title - * @return string Formatted "
  • " element - */ - public function formatRow( $row ) { - wfProfileIn( __METHOD__ ); - - static $infinity = null; - - if ( is_null( $infinity ) ) { - $infinity = wfGetDB( DB_SLAVE )->getInfinity(); - } - - $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); - if ( !$title ) { - wfProfileOut( __METHOD__ ); - - return Html::rawElement( - 'li', - array(), - Html::element( - 'span', - array( 'class' => 'mw-invalidtitle' ), - Linker::getInvalidTitleDescription( - $this->getContext(), - $row->page_namespace, - $row->page_title - ) - ) - ) . "\n"; - } - - $link = Linker::link( $title ); - - $description_items = array(); - - // Messages: restriction-level-sysop, restriction-level-autoconfirmed - $protType = $this->msg( 'restriction-level-' . $row->pr_level )->escaped(); - - $description_items[] = $protType; - - if ( $row->pr_cascade ) { - $description_items[] = $this->msg( 'protect-summary-cascade' )->text(); - } - - $stxt = ''; - $lang = $this->getLanguage(); - - $expiry = $lang->formatExpiry( $row->pr_expiry, TS_MW ); - if ( $expiry != $infinity ) { - $user = $this->getUser(); - $description_items[] = $this->msg( - 'protect-expiring-local', - $lang->userTimeAndDate( $expiry, $user ), - $lang->userDate( $expiry, $user ), - $lang->userTime( $expiry, $user ) - )->escaped(); - } - - if ( !is_null( $size = $row->page_len ) ) { - $stxt = $lang->getDirMark() . ' ' . Linker::formatRevisionSize( $size ); - } - - // Show a link to the change protection form for allowed users otherwise - // a link to the protection log - if ( $this->getUser()->isAllowed( 'protect' ) ) { - $changeProtection = Linker::linkKnown( - $title, - $this->msg( 'protect_change' )->escaped(), - array(), - array( 'action' => 'unprotect' ) - ); - } else { - $ltitle = SpecialPage::getTitleFor( 'Log' ); - $changeProtection = Linker::linkKnown( - $ltitle, - $this->msg( 'protectlogpage' )->escaped(), - array(), - array( - 'type' => 'protect', - 'page' => $title->getPrefixedText() - ) - ); - } - - $changeProtection = ' ' . $this->msg( 'parentheses' )->rawParams( $changeProtection ) - ->escaped(); - - wfProfileOut( __METHOD__ ); - - return Html::rawElement( - 'li', - array(), - $lang->specialList( $link . $stxt, $lang->commaList( $description_items ), false ) . - $changeProtection - ) . "\n"; - } - - /** - * @param $namespace Integer - * @param string $type restriction type - * @param string $level restriction level + * @param int $namespace + * @param string $type Restriction type + * @param string $level Restriction level * @param string $sizetype "min" or "max" - * @param $size Integer - * @param $indefOnly Boolean: only indefinie protection - * @param $cascadeOnly Boolean: only cascading protection - * @return String: input form + * @param int $size + * @param bool $indefOnly Only indefinite protection + * @param bool $cascadeOnly Only cascading protection + * @param bool $noRedirect Don't show redirects + * @return string Input form */ protected function showOptions( $namespace, $type = 'edit', $level, $sizetype, - $size, $indefOnly, $cascadeOnly + $size, $indefOnly, $cascadeOnly, $noRedirect ) { - global $wgScript; + $title = $this->getPageTitle(); - $title = $this->getTitle(); - - return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . + return Xml::openElement( 'form', array( 'method' => 'get', 'action' => wfScript() ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', array(), $this->msg( 'protectedpages' )->text() ) . Html::hidden( 'title', $title->getPrefixedDBkey() ) . "\n" . @@ -212,6 +111,7 @@ class SpecialProtectedpages extends SpecialPage { "
    " . $this->getExpiryCheck( $indefOnly ) . " \n" . $this->getCascadeCheck( $cascadeOnly ) . " \n" . + $this->getRedirectCheck( $noRedirect ) . " \n" . "
    " . $this->getSizeLimit( $sizetype, $size ) . " \n" . "" . @@ -224,8 +124,8 @@ class SpecialProtectedpages extends SpecialPage { * Prepare the namespace filter drop-down; standard namespace * selector, sans the MediaWiki namespace * - * @param $namespace Mixed: pre-select namespace - * @return String + * @param string|null $namespace Pre-select namespace + * @return string */ protected function getNamespaceMenu( $namespace = null ) { return Html::rawElement( 'span', array( 'style' => 'white-space: nowrap;' ), @@ -269,6 +169,19 @@ class SpecialProtectedpages extends SpecialPage { ) . "\n"; } + /** + * @param bool $noRedirect + * @return string Formatted HTML + */ + protected function getRedirectCheck( $noRedirect ) { + return Xml::checkLabel( + $this->msg( 'protectedpages-noredirect' )->text(), + 'noredirect', + 'noredirect', + $noRedirect + ) . "\n"; + } + /** * @param string $sizetype "min" or "max" * @param mixed $size @@ -300,7 +213,7 @@ class SpecialProtectedpages extends SpecialPage { /** * Creates the input label of the restriction type - * @param $pr_type string Protection type + * @param string $pr_type Protection type * @return string Formatted HTML */ protected function getTypeMenu( $pr_type ) { @@ -329,18 +242,16 @@ class SpecialProtectedpages extends SpecialPage { /** * Creates the input label of the restriction level - * @param $pr_level string Protection level + * @param string $pr_level Protection level * @return string Formatted HTML */ protected function getLevelMenu( $pr_level ) { - global $wgRestrictionLevels; - // Temporary array $m = array( $this->msg( 'restriction-level-all' )->text() => 0 ); $options = array(); // First pass to load the log names - foreach ( $wgRestrictionLevels as $type ) { + foreach ( $this->getConfig()->get( 'RestrictionLevels' ) as $type ) { // Messages used can be 'restriction-level-sysop' and 'restriction-level-autoconfirmed' if ( $type != '' && $type != '*' ) { $text = $this->msg( "restriction-level-$type" )->text(); @@ -370,12 +281,12 @@ class SpecialProtectedpages extends SpecialPage { * @todo document * @ingroup Pager */ -class ProtectedPagesPager extends AlphabeticPager { +class ProtectedPagesPager extends TablePager { public $mForm, $mConds; - private $type, $level, $namespace, $sizetype, $size, $indefonly; + private $type, $level, $namespace, $sizetype, $size, $indefonly, $cascadeonly, $noredirect; function __construct( $form, $conds = array(), $type, $level, $namespace, - $sizetype = '', $size = 0, $indefonly = false, $cascadeonly = false + $sizetype = '', $size = 0, $indefonly = false, $cascadeonly = false, $noredirect = false ) { $this->mForm = $form; $this->mConds = $conds; @@ -386,28 +297,203 @@ class ProtectedPagesPager extends AlphabeticPager { $this->size = intval( $size ); $this->indefonly = (bool)$indefonly; $this->cascadeonly = (bool)$cascadeonly; + $this->noredirect = (bool)$noredirect; parent::__construct( $form->getContext() ); } - function getStartBody() { + function preprocessResults( $result ) { # Do a link batch query $lb = new LinkBatch; - foreach ( $this->mResult as $row ) { + $userids = array(); + + foreach ( $result as $row ) { $lb->add( $row->page_namespace, $row->page_title ); + // field is nullable, maybe null on old protections + if ( $row->log_user !== null ) { + $userids[] = $row->log_user; + } + } + + // fill LinkBatch with user page and user talk + if ( count( $userids ) ) { + $userCache = UserCache::singleton(); + $userCache->doQuery( $userids, array(), __METHOD__ ); + foreach ( $userids as $userid ) { + $name = $userCache->getProp( $userid, 'name' ); + if ( $name !== false ) { + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + } + } } + $lb->execute(); + } + + function getFieldNames() { + static $headers = null; + + if ( $headers == array() ) { + $headers = array( + 'log_timestamp' => 'protectedpages-timestamp', + 'pr_page' => 'protectedpages-page', + 'pr_expiry' => 'protectedpages-expiry', + 'log_user' => 'protectedpages-performer', + 'pr_params' => 'protectedpages-params', + 'log_comment' => 'protectedpages-reason', + ); + foreach ( $headers as $key => $val ) { + $headers[$key] = $this->msg( $val )->text(); + } + } - return ''; + return $headers; } - function formatRow( $row ) { - return $this->mForm->formatRow( $row ); + /** + * @param string $field + * @param string $value + * @return string + * @throws MWException + */ + function formatValue( $field, $value ) { + /** @var $row object */ + $row = $this->mCurrentRow; + + $formatted = ''; + + switch ( $field ) { + case 'log_timestamp': + // when timestamp is null, this is a old protection row + if ( $value === null ) { + $formatted = Html::rawElement( + 'span', + array( 'class' => 'mw-protectedpages-unknown' ), + $this->msg( 'protectedpages-unknown-timestamp' )->escaped() + ); + } else { + $formatted = $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ); + } + break; + + case 'pr_page': + $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); + if ( !$title ) { + $formatted = Html::element( + 'span', + array( 'class' => 'mw-invalidtitle' ), + Linker::getInvalidTitleDescription( + $this->getContext(), + $row->page_namespace, + $row->page_title + ) + ); + } else { + $formatted = Linker::link( $title ); + } + if ( !is_null( $row->page_len ) ) { + $formatted .= $this->getLanguage()->getDirMark() . + ' ' . Html::rawElement( + 'span', + array( 'class' => 'mw-protectedpages-length' ), + Linker::formatRevisionSize( $row->page_len ) + ); + } + break; + + case 'pr_expiry': + $formatted = $this->getLanguage()->formatExpiry( $value, /* User preference timezone */true ); + $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); + if ( $this->getUser()->isAllowed( 'protect' ) && $title ) { + $changeProtection = Linker::linkKnown( + $title, + $this->msg( 'protect_change' )->escaped(), + array(), + array( 'action' => 'unprotect' ) + ); + $formatted .= ' ' . Html::rawElement( + 'span', + array( 'class' => 'mw-protectedpages-actions' ), + $this->msg( 'parentheses' )->rawParams( $changeProtection )->escaped() + ); + } + break; + + case 'log_user': + // when timestamp is null, this is a old protection row + if ( $row->log_timestamp === null ) { + $formatted = Html::rawElement( + 'span', + array( 'class' => 'mw-protectedpages-unknown' ), + $this->msg( 'protectedpages-unknown-performer' )->escaped() + ); + } else { + $username = UserCache::singleton()->getProp( $value, 'name' ); + if ( LogEventsList::userCanBitfield( + $row->log_deleted, + LogPage::DELETED_USER, + $this->getUser() + ) ) { + if ( $username === false ) { + $formatted = htmlspecialchars( $value ); + } else { + $formatted = Linker::userLink( $value, $username ) + . Linker::userToolLinks( $value, $username ); + } + } else { + $formatted = $this->msg( 'rev-deleted-user' )->escaped(); + } + if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) { + $formatted = '' . $formatted . ''; + } + } + break; + + case 'pr_params': + $params = array(); + // Messages: restriction-level-sysop, restriction-level-autoconfirmed + $params[] = $this->msg( 'restriction-level-' . $row->pr_level )->escaped(); + if ( $row->pr_cascade ) { + $params[] = $this->msg( 'protect-summary-cascade' )->text(); + } + $formatted = $this->getLanguage()->commaList( $params ); + break; + + case 'log_comment': + // when timestamp is null, this is an old protection row + if ( $row->log_timestamp === null ) { + $formatted = Html::rawElement( + 'span', + array( 'class' => 'mw-protectedpages-unknown' ), + $this->msg( 'protectedpages-unknown-reason' )->escaped() + ); + } else { + if ( LogEventsList::userCanBitfield( + $row->log_deleted, + LogPage::DELETED_COMMENT, + $this->getUser() + ) ) { + $formatted = Linker::formatComment( $value !== null ? $value : '' ); + } else { + $formatted = $this->msg( 'rev-deleted-comment' )->escaped(); + } + if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) { + $formatted = '' . $formatted . ''; + } + } + break; + + default: + throw new MWException( "Unknown field '$field'" ); + } + + return $formatted; } function getQueryInfo() { $conds = $this->mConds; - $conds[] = '(pr_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() ) . - 'OR pr_expiry IS NULL)'; + $conds[] = 'pr_expiry > ' . $this->mDb->addQuotes( $this->mDb->timestamp() ) . + 'OR pr_expiry IS NULL'; $conds[] = 'page_id=pr_page'; $conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type ); @@ -424,6 +510,9 @@ class ProtectedPagesPager extends AlphabeticPager { if ( $this->cascadeonly ) { $conds[] = 'pr_cascade = 1'; } + if ( $this->noredirect ) { + $conds[] = 'page_is_redirect = 0'; + } if ( $this->level ) { $conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level ); @@ -433,14 +522,51 @@ class ProtectedPagesPager extends AlphabeticPager { } return array( - 'tables' => array( 'page_restrictions', 'page' ), - 'fields' => array( 'pr_id', 'page_namespace', 'page_title', 'page_len', - 'pr_type', 'pr_level', 'pr_expiry', 'pr_cascade' ), - 'conds' => $conds + 'tables' => array( 'page', 'page_restrictions', 'log_search', 'logging' ), + 'fields' => array( + 'pr_id', + 'page_namespace', + 'page_title', + 'page_len', + 'pr_type', + 'pr_level', + 'pr_expiry', + 'pr_cascade', + 'log_timestamp', + 'log_user', + 'log_comment', + 'log_deleted', + ), + 'conds' => $conds, + 'join_conds' => array( + 'log_search' => array( + 'LEFT JOIN', array( + 'ls_field' => 'pr_id', 'ls_value = pr_id' + ) + ), + 'logging' => array( + 'LEFT JOIN', array( + 'ls_log_id = log_id' + ) + ) + ) ); } + public function getTableClass() { + return parent::getTableClass() . ' mw-protectedpages'; + } + function getIndexField() { return 'pr_id'; } + + function getDefaultSort() { + return 'pr_id'; + } + + function isFieldSortable( $field ) { + // no index for sorting exists + return false; + } } diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php index 078e7b12..a40da87d 100644 --- a/includes/specials/SpecialProtectedtitles.php +++ b/includes/specials/SpecialProtectedtitles.php @@ -126,16 +126,15 @@ class SpecialProtectedtitles extends SpecialPage { } /** - * @param $namespace Integer: - * @param $type string - * @param $level string + * @param int $namespace + * @param string $type + * @param string $level * @return string * @private */ function showOptions( $namespace, $type = 'edit', $level ) { - global $wgScript; - $action = htmlspecialchars( $wgScript ); - $title = $this->getTitle(); + $action = htmlspecialchars( wfScript() ); + $title = $this->getPageTitle(); $special = htmlspecialchars( $title->getPrefixedDBkey() ); return "
    \n" . @@ -152,7 +151,7 @@ class SpecialProtectedtitles extends SpecialPage { * Prepare the namespace filter drop-down; standard namespace * selector, sans the MediaWiki namespace * - * @param $namespace Mixed: pre-select namespace + * @param string|null $namespace Pre-select namespace * @return string */ function getNamespaceMenu( $namespace = null ) { @@ -175,14 +174,12 @@ class SpecialProtectedtitles extends SpecialPage { * @private */ function getLevelMenu( $pr_level ) { - global $wgRestrictionLevels; - // Temporary array $m = array( $this->msg( 'restriction-level-all' )->text() => 0 ); $options = array(); // First pass to load the log names - foreach ( $wgRestrictionLevels as $type ) { + foreach ( $this->getConfig()->get( 'RestrictionLevels' ) as $type ) { if ( $type != '' && $type != '*' ) { // Messages: restriction-level-sysop, restriction-level-autoconfirmed $text = $this->msg( "restriction-level-$type" )->text(); diff --git a/includes/specials/SpecialRandomInCategory.php b/includes/specials/SpecialRandomInCategory.php index 0e022bfa..570ab3bf 100644 --- a/includes/specials/SpecialRandomInCategory.php +++ b/includes/specials/SpecialRandomInCategory.php @@ -46,7 +46,7 @@ * * @ingroup SpecialPage */ -class SpecialRandomInCategory extends SpecialPage { +class SpecialRandomInCategory extends FormSpecialPage { protected $extra = array(); // Extra SQL statements protected $category = false; // Title object of category protected $maxOffset = 30; // Max amount to fudge randomness by. @@ -67,12 +67,35 @@ class SpecialRandomInCategory extends SpecialPage { $this->minTimestamp = null; } - public function execute( $par ) { - global $wgScript; + protected function getFormFields() { + $form = array( + 'category' => array( + 'type' => 'text', + 'label-message' => 'randomincategory-category', + 'required' => true, + ) + ); + + return $form; + } + + public function requiresWrite() { + return false; + } + + public function requiresUnblock() { + return false; + } + protected function setParameter( $par ) { + // if subpage present, fake form submission + $this->onSubmit( array( 'category' => $par ) ); + } + + public function onSubmit( array $data ) { $cat = false; - $categoryStr = $this->getRequest()->getText( 'category', $par ); + $categoryStr = $data['category']; if ( $categoryStr ) { $cat = Title::newFromText( $categoryStr, NS_CATEGORY ); @@ -87,48 +110,31 @@ class SpecialRandomInCategory extends SpecialPage { $this->setCategory( $cat ); } - if ( !$this->category && $categoryStr ) { - $this->setHeaders(); - $this->getOutput()->addWikiMsg( 'randomincategory-invalidcategory', + $msg = $this->msg( 'randomincategory-invalidcategory', wfEscapeWikiText( $categoryStr ) ); - return; + return Status::newFatal( $msg ); + } elseif ( !$this->category ) { - $this->setHeaders(); - $input = Html::input( 'category' ); - $submitText = $this->msg( 'randomincategory-selectcategory-submit' )->text(); - $submit = Html::input( '', $submitText, 'submit' ); - - $msg = $this->msg( 'randomincategory-selectcategory' ); - $form = Html::rawElement( 'form', array( 'action' => $wgScript ), - Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - $msg->rawParams( $input, $submit )->parse() - ); - $this->getOutput()->addHtml( $form ); - - return; + return; // no data sent } $title = $this->getRandomTitle(); if ( is_null( $title ) ) { - $this->setHeaders(); - $this->getOutput()->addWikiMsg( 'randomincategory-nopages', + $msg = $this->msg( 'randomincategory-nopages', $this->category->getText() ); - return; + return Status::newFatal( $msg ); } - $query = $this->getRequest()->getValues(); - unset( $query['title'] ); - unset( $query['category'] ); - $this->getOutput()->redirect( $title->getFullURL( $query ) ); + $this->getOutput()->redirect( $title->getFullURL() ); } /** * Choose a random title. - * @return Title object (or null if nothing to choose from) + * @return Title|null Title object (or null if nothing to choose from) */ public function getRandomTitle() { // Convert to float, since we do math with the random number. @@ -178,7 +184,7 @@ class SpecialRandomInCategory extends SpecialPage { * was a large gap in the distribution of cl_timestamp values. This way instead * of things to the right of the gap being favoured, both sides of the gap * are favoured. - * @return Array Query information. + * @return array Query information. */ protected function getQueryInfo( $rand, $offset, $up ) { $op = $up ? '>=' : '<='; @@ -208,6 +214,7 @@ class SpecialRandomInCategory extends SpecialPage { $qi['conds'][] = 'cl_timestamp ' . $op . ' ' . $dbr->addQuotes( $dbr->timestamp( $minClTime ) ); } + return $qi; } @@ -230,6 +237,7 @@ class SpecialRandomInCategory extends SpecialPage { } $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp; + return intval( $ts ); } @@ -237,8 +245,8 @@ class SpecialRandomInCategory extends SpecialPage { * Get the lowest and highest timestamp for a category. * * @param Title $category - * @return Array The lowest and highest timestamp - * @throws MWException if category has no entries. + * @return array The lowest and highest timestamp + * @throws MWException If category has no entries. */ protected function getMinAndMaxForCat( Title $category ) { $dbr = wfGetDB( DB_SLAVE ); @@ -259,6 +267,7 @@ class SpecialRandomInCategory extends SpecialPage { if ( !$res ) { throw new MWException( 'No entries in category' ); } + return array( wfTimestamp( TS_UNIX, $res->low ), wfTimestamp( TS_UNIX, $res->high ) ); } @@ -266,8 +275,8 @@ class SpecialRandomInCategory extends SpecialPage { * @param float $rand A random number that is converted to a random timestamp * @param int $offset A small offset to make the result seem more "random" * @param bool $up Get the result above the random value - * @param String $fname The name of the calling method - * @return Array Info for the title selected. + * @param string $fname The name of the calling method + * @return array Info for the title selected. */ private function selectRandomPageFromDB( $rand, $offset, $up, $fname = __METHOD__ ) { $dbr = wfGetDB( DB_SLAVE ); diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php index c94d2b35..6d8f59b5 100644 --- a/includes/specials/SpecialRandompage.php +++ b/includes/specials/SpecialRandompage.php @@ -56,7 +56,9 @@ class RandomPage extends SpecialPage { public function execute( $par ) { global $wgContLang; - if ( $par ) { + if ( is_string( $par ) ) { + // Testing for stringiness since we want to catch + // the empty string to mean main namespace only. $this->setNamespace( $wgContLang->getNsIndex( $par ) ); } @@ -80,7 +82,7 @@ class RandomPage extends SpecialPage { /** * Get a comma-delimited list of namespaces we don't have * any pages in - * @return String + * @return string */ private function getNsList() { global $wgContLang; @@ -98,7 +100,7 @@ class RandomPage extends SpecialPage { /** * Choose a random title. - * @return Title object (or null if nothing to choose from) + * @return Title|null Title object (or null if nothing to choose from) */ public function getRandomTitle() { $randstr = wfRandom(); @@ -144,7 +146,6 @@ class RandomPage extends SpecialPage { ), $this->extra ), 'options' => array( 'ORDER BY' => 'page_random', - 'USE INDEX' => 'page_random', 'LIMIT' => 1, ), 'join_conds' => array() diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index a42a2171..e6d8f1c3 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -26,12 +26,38 @@ * * @ingroup SpecialPage */ -class SpecialRecentChanges extends IncludableSpecialPage { - var $rcOptions, $rcSubpage; - protected $customFilters; +class SpecialRecentChanges extends ChangesListSpecialPage { + // @codingStandardsIgnoreStart Needed "useless" override to change parameters. + public function __construct( $name = 'Recentchanges', $restriction = '' ) { + parent::__construct( $name, $restriction ); + } + // @codingStandardsIgnoreEnd + + /** + * Main execution point + * + * @param string $subpage + */ + public function execute( $subpage ) { + // Backwards-compatibility: redirect to new feed URLs + $feedFormat = $this->getRequest()->getVal( 'feed' ); + if ( !$this->including() && $feedFormat ) { + $query = $this->getFeedQuery(); + $query['feedformat'] = $feedFormat === 'atom' ? 'atom' : 'rss'; + $this->getOutput()->redirect( wfAppendQuery( wfScript( 'api' ), $query ) ); + + return; + } + + // 10 seconds server-side caching max + $this->getOutput()->setSquidMaxage( 10 ); + // Check if the client has a cached version + $lastmod = $this->checkLastModified(); + if ( $lastmod === false ) { + return; + } - public function __construct( $name = 'Recentchanges' ) { - parent::__construct( $name ); + parent::execute( $subpage ); } /** @@ -40,7 +66,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { * @return FormOptions */ public function getDefaultOptions() { - $opts = new FormOptions(); + $opts = parent::getDefaultOptions(); $user = $this->getUser(); $opts->add( 'days', $user->getIntOption( 'rcdays' ) ); @@ -54,10 +80,6 @@ class SpecialRecentChanges extends IncludableSpecialPage { $opts->add( 'hidepatrolled', $user->getBoolOption( 'hidepatrolled' ) ); $opts->add( 'hidemyself', false ); - $opts->add( 'namespace', '', FormOptions::INTNULL ); - $opts->add( 'invert', false ); - $opts->add( 'associated', false ); - $opts->add( 'categories', '' ); $opts->add( 'categories_any', false ); $opts->add( 'tagfilter', '' ); @@ -65,32 +87,6 @@ class SpecialRecentChanges extends IncludableSpecialPage { return $opts; } - /** - * Create a FormOptions object with options as specified by the user - * - * @param array $parameters - * - * @return FormOptions - */ - public function setup( $parameters ) { - $opts = $this->getDefaultOptions(); - - foreach ( $this->getCustomFilters() as $key => $params ) { - $opts->add( $key, $params['default'] ); - } - - $opts->fetchValuesFromRequest( $this->getRequest() ); - - // Give precedence to subpage syntax - if ( $parameters !== null ) { - $this->parseParameters( $parameters, $opts ); - } - - $opts->validateIntBounds( 'limit', 0, 5000 ); - - return $opts; - } - /** * Get custom show/hide filters * @@ -98,116 +94,15 @@ class SpecialRecentChanges extends IncludableSpecialPage { */ protected function getCustomFilters() { if ( $this->customFilters === null ) { - $this->customFilters = array(); - wfRunHooks( 'SpecialRecentChangesFilters', array( $this, &$this->customFilters ) ); + $this->customFilters = parent::getCustomFilters(); + wfRunHooks( 'SpecialRecentChangesFilters', array( $this, &$this->customFilters ), '1.23' ); } return $this->customFilters; } /** - * Create a FormOptions object specific for feed requests and return it - * - * @return FormOptions - */ - public function feedSetup() { - global $wgFeedLimit; - $opts = $this->getDefaultOptions(); - $opts->fetchValuesFromRequest( $this->getRequest() ); - $opts->validateIntBounds( 'limit', 0, $wgFeedLimit ); - - return $opts; - } - - /** - * Get the current FormOptions for this request - */ - public function getOptions() { - if ( $this->rcOptions === null ) { - if ( $this->including() ) { - $isFeed = false; - } else { - $isFeed = (bool)$this->getRequest()->getVal( 'feed' ); - } - $this->rcOptions = $isFeed ? $this->feedSetup() : $this->setup( $this->rcSubpage ); - } - - return $this->rcOptions; - } - - /** - * Main execution point - * - * @param string $subpage - */ - public function execute( $subpage ) { - $this->rcSubpage = $subpage; - $feedFormat = $this->including() ? null : $this->getRequest()->getVal( 'feed' ); - - # 10 seconds server-side caching max - $this->getOutput()->setSquidMaxage( 10 ); - # Check if the client has a cached version - $lastmod = $this->checkLastModified( $feedFormat ); - if ( $lastmod === false ) { - return; - } - - $opts = $this->getOptions(); - $this->setHeaders(); - $this->outputHeader(); - $this->addModules(); - - // Fetch results, prepare a batch link existence check query - $conds = $this->buildMainQueryConds( $opts ); - $rows = $this->doMainQuery( $conds, $opts ); - if ( $rows === false ) { - if ( !$this->including() ) { - $this->doHeader( $opts ); - } - - return; - } - - if ( !$feedFormat ) { - $batch = new LinkBatch; - foreach ( $rows as $row ) { - $batch->add( NS_USER, $row->rc_user_text ); - $batch->add( NS_USER_TALK, $row->rc_user_text ); - $batch->add( $row->rc_namespace, $row->rc_title ); - } - $batch->execute(); - } - if ( $feedFormat ) { - list( $changesFeed, $formatter ) = $this->getFeedObject( $feedFormat ); - /** @var ChangesFeed $changesFeed */ - $changesFeed->execute( $formatter, $rows, $lastmod, $opts ); - } else { - $this->webOutput( $rows, $opts ); - } - - $rows->free(); - } - - /** - * Return an array with a ChangesFeed object and ChannelFeed object - * - * @param string $feedFormat Feed's format (either 'rss' or 'atom') - * @return array - */ - public function getFeedObject( $feedFormat ) { - $changesFeed = new ChangesFeed( $feedFormat, 'rcfeed' ); - $formatter = $changesFeed->getFeedObject( - $this->msg( 'recentchanges' )->inContentLanguage()->text(), - $this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(), - $this->getTitle()->getFullURL() - ); - - return array( $changesFeed, $formatter ); - } - - /** - * Process $par and put options found if $opts - * Mainly used when including the page + * Process $par and put options found in $opts. Used when including the page. * * @param string $par * @param FormOptions $opts @@ -257,25 +152,9 @@ class SpecialRecentChanges extends IncludableSpecialPage { } } - /** - * Get last modified date, for client caching - * Don't use this if we are using the patrol feature, patrol changes don't - * update the timestamp - * - * @param string $feedFormat - * @return string|bool - */ - public function checkLastModified( $feedFormat ) { - $dbr = wfGetDB( DB_SLAVE ); - $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ ); - if ( $feedFormat || !$this->getUser()->useRCPatrol() ) { - if ( $lastmod && $this->getOutput()->checkLastModified( $lastmod ) ) { - # Client cache fresh and headers sent, nothing more to do. - return false; - } - } - - return $lastmod; + public function validateOptions( FormOptions $opts ) { + $opts->validateIntBounds( 'limit', 0, 5000 ); + parent::validateOptions( $opts ); } /** @@ -285,21 +164,8 @@ class SpecialRecentChanges extends IncludableSpecialPage { * @return array */ public function buildMainQueryConds( FormOptions $opts ) { - $dbr = wfGetDB( DB_SLAVE ); - $conds = array(); - - # It makes no sense to hide both anons and logged-in users - # Where this occurs, force anons to be shown - $forcebot = false; - if ( $opts['hideanons'] && $opts['hideliu'] ) { - # Check if the user wants to show bots only - if ( $opts['hidebots'] ) { - $opts['hideanons'] = false; - } else { - $forcebot = true; - $opts['hidebots'] = false; - } - } + $dbr = $this->getDB(); + $conds = parent::buildMainQueryConds( $opts ); // Calculate cutoff $cutoff_unixtime = time() - ( $opts['days'] * 86400 ); @@ -315,59 +181,6 @@ class SpecialRecentChanges extends IncludableSpecialPage { $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff ); - $hidePatrol = $this->getUser()->useRCPatrol() && $opts['hidepatrolled']; - $hideLoggedInUsers = $opts['hideliu'] && !$forcebot; - $hideAnonymousUsers = $opts['hideanons'] && !$forcebot; - - if ( $opts['hideminor'] ) { - $conds['rc_minor'] = 0; - } - if ( $opts['hidebots'] ) { - $conds['rc_bot'] = 0; - } - if ( $hidePatrol ) { - $conds['rc_patrolled'] = 0; - } - if ( $forcebot ) { - $conds['rc_bot'] = 1; - } - if ( $hideLoggedInUsers ) { - $conds[] = 'rc_user = 0'; - } - if ( $hideAnonymousUsers ) { - $conds[] = 'rc_user != 0'; - } - - if ( $opts['hidemyself'] ) { - if ( $this->getUser()->getId() ) { - $conds[] = 'rc_user != ' . $dbr->addQuotes( $this->getUser()->getId() ); - } else { - $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $this->getUser()->getName() ); - } - } - - # Namespace filtering - if ( $opts['namespace'] !== '' ) { - $selectedNS = $dbr->addQuotes( $opts['namespace'] ); - $operator = $opts['invert'] ? '!=' : '='; - $boolean = $opts['invert'] ? 'AND' : 'OR'; - - # namespace association (bug 2429) - if ( !$opts['associated'] ) { - $condition = "rc_namespace $operator $selectedNS"; - } else { - # Also add the associated namespace - $associatedNS = $dbr->addQuotes( - MWNamespace::getAssociated( $opts['namespace'] ) - ); - $condition = "(rc_namespace $operator $selectedNS " - . $boolean - . " rc_namespace $operator $associatedNS)"; - } - - $conds[] = $condition; - } - return $conds; } @@ -379,37 +192,32 @@ class SpecialRecentChanges extends IncludableSpecialPage { * @return bool|ResultWrapper Result or false (for Recentchangeslinked only) */ public function doMainQuery( $conds, $opts ) { + $dbr = $this->getDB(); + $user = $this->getUser(); + $tables = array( 'recentchanges' ); + $fields = RecentChange::selectFields(); + $query_options = array(); $join_conds = array(); - $query_options = array( - 'USE INDEX' => array( 'recentchanges' => 'rc_timestamp' ) - ); - - $uid = $this->getUser()->getId(); - $dbr = wfGetDB( DB_SLAVE ); - $limit = $opts['limit']; - $namespace = $opts['namespace']; - $invert = $opts['invert']; - $associated = $opts['associated']; - $fields = RecentChange::selectFields(); // JOIN on watchlist for users - if ( $uid && $this->getUser()->isAllowed( 'viewmywatchlist' ) ) { + if ( $user->getId() && $user->isAllowed( 'viewmywatchlist' ) ) { $tables[] = 'watchlist'; $fields[] = 'wl_user'; $fields[] = 'wl_notificationtimestamp'; $join_conds['watchlist'] = array( 'LEFT JOIN', array( - 'wl_user' => $uid, + 'wl_user' => $user->getId(), 'wl_title=rc_title', 'wl_namespace=rc_namespace' ) ); } - if ( $this->getUser()->isAllowed( 'rollback' ) ) { + + if ( $user->isAllowed( 'rollback' ) ) { $tables[] = 'page'; $fields[] = 'page_latest'; $join_conds['page'] = array( 'LEFT JOIN', 'rc_cur_id=page_id' ); } - // Tag stuff. + ChangeTags::modifyDisplayQuery( $tables, $fields, @@ -419,48 +227,81 @@ class SpecialRecentChanges extends IncludableSpecialPage { $opts['tagfilter'] ); - if ( !wfRunHooks( 'SpecialRecentChangesQuery', - array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ) ) + if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, + $opts ) ) { 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). - return $dbr->select( + $rows = $dbr->select( $tables, $fields, $conds + array( 'rc_new' => array( 0, 1 ) ), __METHOD__, - array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ) + $query_options, + array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $opts['limit'] ) + $query_options, $join_conds ); + + // Build the final data + if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) { + $this->filterByCategories( $rows, $opts ); + } + + return $rows; + } + + protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ) { + return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ) + && wfRunHooks( + 'SpecialRecentChangesQuery', + array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ), + '1.23' + ); + } + + public function outputFeedLinks() { + $this->addFeedLinks( $this->getFeedQuery() ); } /** - * Send output to the OutputPage object, only called if not used feeds + * Get URL query parameters for action=feedrecentchanges API feed of current recent changes view. + * + * @return array + */ + private function getFeedQuery() { + $query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) { + // API handles empty parameters in a different way + return $value !== ''; + } ); + $query['action'] = 'feedrecentchanges'; + $feedLimit = $this->getConfig()->get( 'FeedLimit' ); + if ( $query['limit'] > $feedLimit ) { + $query['limit'] = $feedLimit; + } + + return $query; + } + + /** + * Build and output the actual changes list. * * @param array $rows Database rows * @param FormOptions $opts */ - public function webOutput( $rows, $opts ) { - global $wgRCShowWatchingUsers, $wgShowUpdatedMarker, $wgAllowCategorizedRecentChanges; - - // Build the final data - - if ( $wgAllowCategorizedRecentChanges ) { - $this->filterByCategories( $rows, $opts ); - } - + public function outputChangesList( $rows, $opts ) { $limit = $opts['limit']; - $showWatcherCount = $wgRCShowWatchingUsers && $this->getUser()->getOption( 'shownumberswatching' ); + $showWatcherCount = $this->getConfig()->get( 'RCShowWatchingUsers' ) + && $this->getUser()->getOption( 'shownumberswatching' ); $watcherCache = array(); - $dbr = wfGetDB( DB_SLAVE ); + $dbr = $this->getDB(); $counter = 1; $list = ChangesList::newFromContext( $this->getContext() ); + $list->initChangesListRows( $rows ); $rclistOutput = $list->beginRecentChangesList(); foreach ( $rows as $obj ) { @@ -470,7 +311,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { $rc = RecentChange::newFromRow( $obj ); $rc->counter = $counter++; # Check if the page has been updated since the last visit - if ( $wgShowUpdatedMarker && !empty( $obj->wl_notificationtimestamp ) ) { + if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) && !empty( $obj->wl_notificationtimestamp ) ) { $rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp ); } else { $rc->notificationtimestamp = false; // Default @@ -501,68 +342,35 @@ class SpecialRecentChanges extends IncludableSpecialPage { } $rclistOutput .= $list->endRecentChangesList(); - // Print things out - - if ( !$this->including() ) { - // Output options box - $this->doHeader( $opts ); - } - - // And now for the content - $feedQuery = $this->getFeedQuery(); - if ( $feedQuery !== '' ) { - $this->getOutput()->setFeedAppendQuery( $feedQuery ); - } else { - $this->getOutput()->setFeedAppendQuery( false ); - } - if ( $rows->numRows() === 0 ) { - $this->getOutput()->wrapWikiMsg( - "
    \n$1\n
    ", 'recentchanges-noresult' + $this->getOutput()->addHtml( + '
    ' . + $this->msg( 'recentchanges-noresult' )->parse() . + '
    ' ); + if ( !$this->including() ) { + $this->getOutput()->setStatusCode( 404 ); + } } else { $this->getOutput()->addHTML( $rclistOutput ); } } /** - * Get the query string to append to feed link URLs. - * - * @return string - */ - public function getFeedQuery() { - global $wgFeedLimit; - - $this->getOptions()->validateIntBounds( 'limit', 0, $wgFeedLimit ); - $options = $this->getOptions()->getChangedValues(); - - // wfArrayToCgi() omits options set to null or false - foreach ( $options as &$value ) { - if ( $value === false ) { - $value = '0'; - } - } - unset( $value ); - - return wfArrayToCgi( $options ); - } - - /** - * Return the text to be displayed above the changes + * Set the text to be displayed above the changes * * @param FormOptions $opts - * @return string XHTML + * @param int $numRows Number of rows in the result to show after this header */ - public function doHeader( $opts ) { - global $wgScript; - + public function doHeader( $opts, $numRows ) { $this->setTopText( $opts ); $defaults = $opts->getAllValues(); $nondefaults = $opts->getChangedValues(); $panel = array(); - $panel[] = $this->optionsPanel( $defaults, $nondefaults ); + $panel[] = self::makeLegend( $this->getContext() ); + $panel[] = $this->optionsPanel( $defaults, $nondefaults, $numRows ); $panel[] = '
    '; $extraOpts = $this->getExtraOptions( $opts ); @@ -604,9 +412,9 @@ class SpecialRecentChanges extends IncludableSpecialPage { $out .= Html::hidden( $key, $value ); } - $t = $this->getTitle(); + $t = $this->getPageTitle(); $out .= Html::hidden( 'title', $t->getPrefixedText() ); - $form = Xml::tags( 'form', array( 'action' => $wgScript ), $out ); + $form = Xml::tags( 'form', array( 'action' => wfScript() ), $out ); $panel[] = $form; $panelString = implode( "\n", $panel ); @@ -621,6 +429,27 @@ class SpecialRecentChanges extends IncludableSpecialPage { $this->setBottomText( $opts ); } + /** + * Send the text to be displayed above the options + * + * @param FormOptions $opts Unused + */ + function setTopText( FormOptions $opts ) { + global $wgContLang; + + $message = $this->msg( 'recentchangestext' )->inContentLanguage(); + if ( !$message->isDisabled() ) { + $this->getOutput()->addWikiText( + Html::rawElement( 'p', + array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ), + "\n" . $message->plain() . "\n" + ), + /* $lineStart */ false, + /* $interface */ false + ); + } + } + /** * Get options to be displayed in a form * @@ -635,8 +464,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { $extraOpts = array(); $extraOpts['namespace'] = $this->namespaceFilterForm( $opts ); - global $wgAllowCategorizedRecentChanges; - if ( $wgAllowCategorizedRecentChanges ) { + if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) { $extraOpts['category'] = $this->categoryFilterForm( $opts ); } @@ -654,38 +482,31 @@ class SpecialRecentChanges extends IncludableSpecialPage { } /** - * Send the text to be displayed above the options - * - * @param FormOptions $opts Unused + * Add page-specific modules. */ - function setTopText( FormOptions $opts ) { - global $wgContLang; - - $message = $this->msg( 'recentchangestext' )->inContentLanguage(); - if ( !$message->isDisabled() ) { - $this->getOutput()->addWikiText( - Html::rawElement( 'p', - array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ), - "\n" . $message->plain() . "\n" - ), - /* $lineStart */ false, - /* $interface */ false - ); - } + protected function addModules() { + parent::addModules(); + $out = $this->getOutput(); + $out->addModules( 'mediawiki.special.recentchanges' ); } /** - * Send the text to be displayed after the options, for use in subclasses. + * Get last modified date, for client caching + * Don't use this if we are using the patrol feature, patrol changes don't + * update the timestamp * - * @param FormOptions $opts + * @return string|bool */ - function setBottomText( FormOptions $opts ) { + public function checkLastModified() { + $dbr = $this->getDB(); + $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ ); + + return $lastmod; } /** * Creates the choose namespace selection * - * @todo Uses radio buttons (HASHAR) * @param FormOptions $opts * @return string */ @@ -710,7 +531,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { } /** - * Create a input to filter changes by categories + * Create an input to filter changes by categories * * @param FormOptions $opts * @return array @@ -728,7 +549,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { /** * Filter $rows by categories set in $opts * - * @param array $rows Database rows + * @param ResultWrapper $rows Database rows * @param FormOptions $opts */ function filterByCategories( &$rows, FormOptions $opts ) { @@ -774,9 +595,9 @@ class SpecialRecentChanges extends IncludableSpecialPage { } # Look up - $c = new Categoryfinder; - $c->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' ); - $match = $c->run(); + $catFind = new CategoryFinder; + $catFind->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' ); + $match = $catFind->run(); # Filter $newrows = array(); @@ -815,7 +636,7 @@ class SpecialRecentChanges extends IncludableSpecialPage { $text = '' . $text . ''; } - return Linker::linkKnown( $this->getTitle(), $text, array(), $params ); + return Linker::linkKnown( $this->getPageTitle(), $text, array(), $params ); } /** @@ -823,11 +644,10 @@ class SpecialRecentChanges extends IncludableSpecialPage { * * @param array $defaults * @param array $nondefaults + * @param int $numRows Number of rows in the result to show after this header * @return string */ - function optionsPanel( $defaults, $nondefaults ) { - global $wgRCLinkLimits, $wgRCLinkDays; - + function optionsPanel( $defaults, $nondefaults, $numRows ) { $options = $nondefaults + $defaults; $note = ''; @@ -839,19 +659,24 @@ class SpecialRecentChanges extends IncludableSpecialPage { $lang = $this->getLanguage(); $user = $this->getUser(); if ( $options['from'] ) { - $note .= $this->msg( 'rcnotefrom' )->numParams( $options['limit'] )->params( - $lang->userTimeAndDate( $options['from'], $user ), - $lang->userDate( $options['from'], $user ), - $lang->userTime( $options['from'], $user ) )->parse() . '
    '; + $note .= $this->msg( 'rcnotefrom' ) + ->numParams( $options['limit'] ) + ->params( + $lang->userTimeAndDate( $options['from'], $user ), + $lang->userDate( $options['from'], $user ), + $lang->userTime( $options['from'], $user ) + ) + ->numParams( $numRows ) + ->parse() . '
    '; } # Sort data for display and make sure it's unique after we've added user data. - $linkLimits = $wgRCLinkLimits; + $linkLimits = $this->getConfig()->get( 'RCLinkLimits' ); $linkLimits[] = $options['limit']; sort( $linkLimits ); $linkLimits = array_unique( $linkLimits ); - $linkDays = $wgRCLinkDays; + $linkDays = $this->getConfig()->get( 'RCLinkDays' ); $linkDays[] = $options['days']; sort( $linkDays ); $linkDays = array_unique( $linkDays ); @@ -873,7 +698,6 @@ class SpecialRecentChanges extends IncludableSpecialPage { $dl = $lang->pipeList( $dl ); // show/hide links - $showhide = array( $this->msg( 'show' )->text(), $this->msg( 'hide' )->text() ); $filters = array( 'hideminor' => 'rcshowhideminor', 'hidebots' => 'rcshowhidebots', @@ -882,6 +706,9 @@ class SpecialRecentChanges extends IncludableSpecialPage { 'hidepatrolled' => 'rcshowhidepatr', 'hidemyself' => 'rcshowhidemine' ); + + $showhide = array( 'show', 'hide' ); + foreach ( $this->getCustomFilters() as $key => $params ) { $filters[$key] = $params['msg']; } @@ -892,35 +719,42 @@ class SpecialRecentChanges extends IncludableSpecialPage { $links = array(); foreach ( $filters as $key => $msg ) { - $link = $this->makeOptionsLink( $showhide[1 - $options[$key]], + // The following messages are used here: + // rcshowhideminor-show, rcshowhideminor-hide, rcshowhidebots-show, rcshowhidebots-hide, + // rcshowhideanons-show, rcshowhideanons-hide, rcshowhideliu-show, rcshowhideliu-hide, + // rcshowhidepatr-show, rcshowhidepatr-hide, rcshowhidemine-show, rcshowhidemine-hide. + $linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] ); + // Extensions can define additional filters, but don't need to define the corresponding + // messages. If they don't exist, just fall back to 'show' and 'hide'. + if ( !$linkMessage->exists() ) { + $linkMessage = $this->msg( $showhide[1 - $options[$key]] ); + } + + $link = $this->makeOptionsLink( $linkMessage->text(), array( $key => 1 - $options[$key] ), $nondefaults ); - $links[] = $this->msg( $msg )->rawParams( $link )->escaped(); + $links[] = "" . $this->msg( $msg )->rawParams( $link )->escaped() . ''; } // show from this onward link $timestamp = wfTimestampNow(); $now = $lang->userTimeAndDate( $timestamp, $user ); - $tl = $this->makeOptionsLink( - $now, array( 'from' => $timestamp ), $nondefaults - ); + $timenow = $lang->userTime( $timestamp, $user ); + $datenow = $lang->userDate( $timestamp, $user ); + $pipedLinks = '' . $lang->pipeList( $links ) . ''; - $rclinks = $this->msg( 'rclinks' )->rawParams( $cl, $dl, $lang->pipeList( $links ) ) - ->parse(); - $rclistfrom = $this->msg( 'rclistfrom' )->rawParams( $tl )->parse(); + $rclinks = '' . $this->msg( 'rclinks' )->rawParams( $cl, $dl, $pipedLinks ) + ->parse() . ''; - return "{$note}$rclinks
    $rclistfrom"; - } + $rclistfrom = '' . $this->makeOptionsLink( + $this->msg( 'rclistfrom' )->rawParams( $now, $timenow, $datenow )->parse(), + array( 'from' => $timestamp ), + $nondefaults + ) . ''; - /** - * Add page-specific modules. - */ - protected function addModules() { - $this->getOutput()->addModules( array( - 'mediawiki.special.recentchanges', - ) ); + return "{$note}$rclinks
    $rclistfrom"; } - protected function getGroupName() { - return 'changes'; + public function isIncludable() { + return true; } } diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php index a8447046..3ad9f0f4 100644 --- a/includes/specials/SpecialRecentchangeslinked.php +++ b/includes/specials/SpecialRecentchangeslinked.php @@ -26,8 +26,9 @@ * * @ingroup SpecialPage */ -class SpecialRecentchangeslinked extends SpecialRecentChanges { - var $rclTargetTitle; +class SpecialRecentChangesLinked extends SpecialRecentChanges { + /** @var bool|Title */ + protected $rclTargetTitle; function __construct() { parent::__construct( 'Recentchangeslinked' ); @@ -37,6 +38,7 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { $opts = parent::getDefaultOptions(); $opts->add( 'target', '' ); $opts->add( 'showlinkedto', false ); + return $opts; } @@ -44,23 +46,6 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { $opts['target'] = $par; } - public function feedSetup() { - $opts = parent::feedSetup(); - $opts['target'] = $this->getRequest()->getVal( 'target' ); - return $opts; - } - - public function getFeedObject( $feedFormat ) { - $feed = new ChangesFeed( $feedFormat, false ); - $feedObj = $feed->getFeedObject( - $this->msg( 'recentchangeslinked-title', $this->getTargetTitle()->getPrefixedText() ) - ->inContentLanguage()->text(), - $this->msg( 'recentchangeslinked-feed' )->inContentLanguage()->text(), - $this->getTitle()->getFullURL() - ); - return array( $feed, $feedObj ); - } - public function doMainQuery( $conds, $opts ) { $target = $opts['target']; $showlinkedto = $opts['showlinkedto']; @@ -71,8 +56,10 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { } $outputPage = $this->getOutput(); $title = Title::newFromURL( $target ); - if ( !$title || $title->getInterwiki() != '' ) { - $outputPage->wrapWikiMsg( "
    \n$1\n
    ", 'allpagesbadtitle' ); + if ( !$title || $title->isExternal() ) { + $outputPage->addHtml( '
    ' . $this->msg( 'allpagesbadtitle' ) + ->parse() . '
    ' ); + return false; } @@ -106,7 +93,7 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { 'wl_user' => $uid, 'wl_title=rc_title', 'wl_namespace=rc_namespace' - )); + ) ); } if ( $this->getUser()->isAllowed( 'rollback' ) ) { $tables[] = 'page'; @@ -122,7 +109,9 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { $opts['tagfilter'] ); - if ( !wfRunHooks( 'SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$select ) ) ) { + if ( !$this->runMainQueryHook( $tables, $select, $conds, $query_options, $join_conds, + $opts ) + ) { return false; } @@ -145,14 +134,20 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { } // field name prefixes for all the various tables we might want to join with - $prefix = array( 'pagelinks' => 'pl', 'templatelinks' => 'tl', 'categorylinks' => 'cl', 'imagelinks' => 'il' ); + $prefix = array( + 'pagelinks' => 'pl', + 'templatelinks' => 'tl', + 'categorylinks' => 'cl', + 'imagelinks' => 'il' + ); $subsql = array(); // SELECT statements to combine with UNION foreach ( $link_tables as $link_table ) { $pfx = $prefix[$link_table]; - // imagelinks and categorylinks tables have no xx_namespace field, and have xx_to instead of xx_title + // imagelinks and categorylinks tables have no xx_namespace field, + // and have xx_to instead of xx_title if ( $link_table == 'imagelinks' ) { $link_ns = NS_FILE; } elseif ( $link_table == 'categorylinks' ) { @@ -225,6 +220,14 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { return $res; } + function setTopText( FormOptions $opts ) { + $target = $this->getTargetTitle(); + if ( $target ) { + $this->getOutput()->addBacklinkSubtitle( $target ); + $this->getSkin()->setRelevantTitle( $target ); + } + } + /** * Get options to be displayed in a form * @@ -256,13 +259,7 @@ class SpecialRecentchangeslinked extends SpecialRecentChanges { $this->rclTargetTitle = false; } } - return $this->rclTargetTitle; - } - function setTopText( FormOptions $opts ) { - $target = $this->getTargetTitle(); - if ( $target ) { - $this->getOutput()->addBacklinkSubtitle( $target ); - } + return $this->rclTargetTitle; } } diff --git a/includes/specials/SpecialRedirect.php b/includes/specials/SpecialRedirect.php index f05dacbc..2022d748 100644 --- a/includes/specials/SpecialRedirect.php +++ b/includes/specials/SpecialRedirect.php @@ -55,6 +55,7 @@ class SpecialRedirect extends FormSpecialPage { /** * Set $mType and $mValue based on parsed value of $subpage. + * @param string $subpage */ function setParameter( $subpage ) { // parse $subpage to pull out the parts @@ -66,7 +67,7 @@ class SpecialRedirect extends FormSpecialPage { /** * Handle Special:Redirect/user/xxxx (by redirecting to User:YYYY) * - * @return string|null url to redirect to, or null if $mValue is invalid. + * @return string|null Url to redirect to, or null if $mValue is invalid. */ function dispatchUser() { if ( !ctype_digit( $this->mValue ) ) { @@ -78,18 +79,19 @@ class SpecialRedirect extends FormSpecialPage { return null; } $userpage = Title::makeTitle( NS_USER, $username ); + return $userpage->getFullURL( '', false, PROTO_CURRENT ); } /** * Handle Special:Redirect/file/xxxx * - * @return string|null url to redirect to, or null if $mValue is not found. + * @return string|null Url to redirect to, or null if $mValue is not found. */ function dispatchFile() { $title = Title::makeTitleSafe( NS_FILE, $this->mValue ); - if ( ! $title instanceof Title ) { + if ( !$title instanceof Title ) { return null; } $file = wfFindFile( $title ); @@ -112,6 +114,7 @@ class SpecialRedirect extends FormSpecialPage { $url = $mto->getURL(); } } + return $url; } @@ -119,7 +122,7 @@ class SpecialRedirect extends FormSpecialPage { * Handle Special:Redirect/revision/xxx * (by redirecting to index.php?oldid=xxx) * - * @return string|null url to redirect to, or null if $mValue is invalid. + * @return string|null Url to redirect to, or null if $mValue is invalid. */ function dispatchRevision() { $oldid = $this->mValue; @@ -130,46 +133,73 @@ class SpecialRedirect extends FormSpecialPage { if ( $oldid === 0 ) { return null; } + return wfAppendQuery( wfScript( 'index' ), array( 'oldid' => $oldid ) ); } + /** + * Handle Special:Redirect/page/xxx (by redirecting to index.php?curid=xxx) + * + * @return string|null Url to redirect to, or null if $mValue is invalid. + */ + function dispatchPage() { + $curid = $this->mValue; + if ( !ctype_digit( $curid ) ) { + return null; + } + $curid = (int)$curid; + if ( $curid === 0 ) { + return null; + } + + return wfAppendQuery( wfScript( 'index' ), array( + 'curid' => $curid + ) ); + } + /** * Use appropriate dispatch* method to obtain a redirection URL, * and either: redirect, set a 404 error code and error message, * or do nothing (if $mValue wasn't set) allowing the form to be * displayed. * - * @return bool true if a redirect was successfully handled. + * @return bool True if a redirect was successfully handled. */ function dispatch() { // the various namespaces supported by Special:Redirect switch ( $this->mType ) { - case 'user': - $url = $this->dispatchUser(); - break; - case 'file': - $url = $this->dispatchFile(); - break; - case 'revision': - $url = $this->dispatchRevision(); - break; - default: - $this->getOutput()->setStatusCode( 404 ); - $url = null; - break; + case 'user': + $url = $this->dispatchUser(); + break; + case 'file': + $url = $this->dispatchFile(); + break; + case 'revision': + $url = $this->dispatchRevision(); + break; + case 'page': + $url = $this->dispatchPage(); + break; + default: + $this->getOutput()->setStatusCode( 404 ); + $url = null; + break; } if ( $url ) { $this->getOutput()->redirect( $url ); + return true; } if ( !is_null( $this->mValue ) ) { $this->getOutput()->setStatusCode( 404 ); // Message: redirect-not-exists $msg = $this->getMessagePrefix() . '-not-exists'; + return Status::newFatal( $msg ); } + return false; } @@ -177,8 +207,10 @@ class SpecialRedirect extends FormSpecialPage { $mp = $this->getMessagePrefix(); $ns = array( // subpage => message - // Messages: redirect-user, redirect-revision, redirect-file + // Messages: redirect-user, redirect-page, redirect-revision, + // redirect-file 'user' => $mp . '-user', + 'page' => $mp . '-page', 'revision' => $mp . '-revision', 'file' => $mp . '-file', ); @@ -204,6 +236,7 @@ class SpecialRedirect extends FormSpecialPage { if ( !empty( $this->mValue ) ) { $a['value']['default'] = $this->mValue; } + return $a; } @@ -211,6 +244,7 @@ class SpecialRedirect extends FormSpecialPage { if ( !empty( $data['type'] ) && !empty( $data['value'] ) ) { $this->setParameter( $data['type'] . '/' . $data['value'] ); } + /* if this returns false, will show the form */ return $this->dispatch(); } diff --git a/includes/specials/SpecialResetTokens.php b/includes/specials/SpecialResetTokens.php index ef2a45da..4add7421 100644 --- a/includes/specials/SpecialResetTokens.php +++ b/includes/specials/SpecialResetTokens.php @@ -40,16 +40,15 @@ class SpecialResetTokens extends FormSpecialPage { * @return array */ protected function getTokensList() { - global $wgHiddenPrefs; - if ( !isset( $this->tokensList ) ) { $tokens = array( array( 'preference' => 'watchlisttoken', 'label-message' => 'resettokens-watchlist-token' ), ); wfRunHooks( 'SpecialResetTokensTokens', array( &$tokens ) ); - $tokens = array_filter( $tokens, function ( $tok ) use ( $wgHiddenPrefs ) { - return !in_array( $tok['preference'], $wgHiddenPrefs ); + $hiddenPrefs = $this->getConfig()->get( 'HiddenPrefs' ); + $tokens = array_filter( $tokens, function ( $tok ) use ( $hiddenPrefs ) { + return !in_array( $tok['preference'], $hiddenPrefs ); } ); $this->tokensList = $tokens; @@ -61,6 +60,7 @@ class SpecialResetTokens extends FormSpecialPage { public function execute( $par ) { // This is a preferences page, so no user JS for y'all. $this->getOutput()->disallowUserJs(); + $this->requireLogin(); parent::execute( $par ); @@ -77,6 +77,7 @@ class SpecialResetTokens extends FormSpecialPage { /** * Display appropriate message if there's nothing to do. * The submit button is also suppressed in this case (see alterForm()). + * @return array */ protected function getFormFields() { $user = $this->getUser(); @@ -89,7 +90,7 @@ class SpecialResetTokens extends FormSpecialPage { ->rawParams( $this->msg( $tok['label-message'] )->parse() ) ->params( $user->getTokenFromOption( $tok['preference'] ) ) ->escaped(); - $tokensForForm[ $label ] = $tok['preference']; + $tokensForForm[$label] = $tok['preference']; } $desc = array( @@ -112,6 +113,7 @@ class SpecialResetTokens extends FormSpecialPage { /** * Suppress the submit button if there's nothing to do; * provide additional message on it otherwise. + * @param HTMLForm $form */ protected function alterForm( HTMLForm $form ) { if ( $this->getTokensList() ) { diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php index 825be6c4..7eea71da 100644 --- a/includes/specials/SpecialRevisiondelete.php +++ b/includes/specials/SpecialRevisiondelete.php @@ -28,61 +28,80 @@ * @ingroup SpecialPage */ class SpecialRevisionDelete extends UnlistedSpecialPage { - /** True if the submit button was clicked, and the form was posted */ - var $submitClicked; + /** @var bool Was the DB modified in this request */ + protected $wasSaved = false; - /** Target ID list */ - var $ids; + /** @var bool True if the submit button was clicked, and the form was posted */ + private $submitClicked; - /** Archive name, for reviewing deleted files */ - var $archiveName; + /** @var array Target ID list */ + private $ids; - /** Edit token for securing image views against XSS */ - var $token; + /** @var string Archive name, for reviewing deleted files */ + private $archiveName; - /** Title object for target parameter */ - var $targetObj; + /** @var string Edit token for securing image views against XSS */ + private $token; - /** Deletion type, may be revision, archive, oldimage, filearchive, logging. */ - var $typeName; + /** @var Title Title object for target parameter */ + private $targetObj; - /** Array of checkbox specs (message, name, deletion bits) */ - var $checks; + /** @var string Deletion type, may be revision, archive, oldimage, filearchive, logging. */ + private $typeName; - /** UI Labels about the current type */ - var $typeLabels; + /** @var array Array of checkbox specs (message, name, deletion bits) */ + private $checks; - /** The RevDel_List object, storing the list of items to be deleted/undeleted */ - var $list; + /** @var array UI Labels about the current type */ + private $typeLabels; + + /** @var RevDelList RevDelList object, storing the list of items to be deleted/undeleted */ + private $revDelList; + + /** @var bool Whether user is allowed to perform the action */ + private $mIsAllowed; + + /** @var string */ + private $otherReason; /** * UI labels for each type. */ - static $UILabels = array( + private static $UILabels = array( 'revision' => array( - 'check-label' => 'revdelete-hide-text', - 'success' => 'revdelete-success', - 'failure' => 'revdelete-failure', + 'check-label' => 'revdelete-hide-text', + 'success' => 'revdelete-success', + 'failure' => 'revdelete-failure', + 'text' => 'revdelete-text-text', + 'selected'=> 'revdelete-selected-text', ), 'archive' => array( - 'check-label' => 'revdelete-hide-text', - 'success' => 'revdelete-success', - 'failure' => 'revdelete-failure', + 'check-label' => 'revdelete-hide-text', + 'success' => 'revdelete-success', + 'failure' => 'revdelete-failure', + 'text' => 'revdelete-text-text', + 'selected'=> 'revdelete-selected-text', ), 'oldimage' => array( - 'check-label' => 'revdelete-hide-image', - 'success' => 'revdelete-success', - 'failure' => 'revdelete-failure', + 'check-label' => 'revdelete-hide-image', + 'success' => 'revdelete-success', + 'failure' => 'revdelete-failure', + 'text' => 'revdelete-text-file', + 'selected'=> 'revdelete-selected-file', ), 'filearchive' => array( - 'check-label' => 'revdelete-hide-image', - 'success' => 'revdelete-success', - 'failure' => 'revdelete-failure', + 'check-label' => 'revdelete-hide-image', + 'success' => 'revdelete-success', + 'failure' => 'revdelete-failure', + 'text' => 'revdelete-text-file', + 'selected'=> 'revdelete-selected-file', ), 'logging' => array( - 'check-label' => 'revdelete-hide-name', - 'success' => 'logdelete-success', - 'failure' => 'logdelete-failure', + 'check-label' => 'revdelete-hide-name', + 'success' => 'logdelete-success', + 'failure' => 'logdelete-failure', + 'text' => 'logdelete-text', + 'selected' => 'logdelete-selected', ), ); @@ -113,7 +132,9 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { // $this->ids = array_map( 'intval', $this->ids ); $this->ids = array_unique( array_filter( $this->ids ) ); - if ( $request->getVal( 'action' ) == 'historysubmit' || $request->getVal( 'action' ) == 'revisiondelete' ) { + if ( $request->getVal( 'action' ) == 'historysubmit' + || $request->getVal( 'action' ) == 'revisiondelete' + ) { // For show/hide form submission from history page // Since we are access through index.php?title=XXX&action=historysubmit // getFullTitle() will contain the target title and not our title @@ -129,6 +150,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $this->token = $request->getVal( 'token' ); if ( $this->archiveName && $this->targetObj ) { $this->tryShowFile( $this->archiveName ); + return; } @@ -138,16 +160,29 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { if ( !$this->typeName || count( $this->ids ) == 0 ) { throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); } - $this->typeLabels = self::$UILabels[$this->typeName]; - $this->mIsAllowed = $user->isAllowed( RevisionDeleter::getRestriction( $this->typeName ) ); # Allow the list type to adjust the passed target - $this->targetObj = RevisionDeleter::suggestTarget( $this->typeName, $this->targetObj, $this->ids ); + $this->targetObj = RevisionDeleter::suggestTarget( + $this->typeName, + $this->targetObj, + $this->ids + ); + + $this->typeLabels = self::$UILabels[$this->typeName]; + $list = $this->getList(); + $list->reset(); + $bitfield = $list->current()->getBits(); + $this->mIsAllowed = $user->isAllowed( RevisionDeleter::getRestriction( $this->typeName ) ); + $canViewSuppressedOnly = $this->getUser()->isAllowed( 'viewsuppressed' ) && + !$this->getUser()->isAllowed( 'suppressrevision' ); + $pageIsSuppressed = $bitfield & Revision::DELETED_RESTRICTED; + $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 @@ -155,6 +190,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { # Initialise checkboxes $this->checks = array( + # Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name array( $this->typeLabels['check-label'], 'wpHidePrimary', RevisionDeleter::getRevdelConstant( $this->typeName ) ), @@ -177,14 +213,24 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { # Show relevant lines from the deletion log $deleteLogPage = new LogPage( 'delete' ); $output->addHTML( "

    " . $deleteLogPage->getName()->escaped() . "

    \n" ); - LogEventsList::showLogExtract( $output, 'delete', - $this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) ); + LogEventsList::showLogExtract( + $output, + 'delete', + $this->targetObj, + '', /* user */ + array( 'lim' => 25, 'conds' => $qc, 'useMaster' => $this->wasSaved ) + ); # Show relevant lines from the suppression log if ( $user->isAllowed( 'suppressionlog' ) ) { $suppressLogPage = new LogPage( 'suppress' ); $output->addHTML( "

    " . $suppressLogPage->getName()->escaped() . "

    \n" ); - LogEventsList::showLogExtract( $output, 'suppress', - $this->targetObj, '', array( 'lim' => 25, 'conds' => $qc ) ); + LogEventsList::showLogExtract( + $output, + 'suppress', + $this->targetObj, + '', + array( 'lim' => 25, 'conds' => $qc, 'useMaster' => $this->wasSaved ) + ); } } @@ -194,6 +240,9 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { protected function showConvenienceLinks() { # Give a link to the logs/hist for this page if ( $this->targetObj ) { + // Also set header tabs to be for the target. + $this->getSkin()->setRelevantTitle( $this->targetObj ); + $links = array(); $links[] = Linker::linkKnown( SpecialPage::getTitleFor( 'Log' ), @@ -236,12 +285,14 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $conds['log_action'] = $this->getList()->getLogAction(); $conds['ls_field'] = RevisionDeleter::getRelationType( $this->typeName ); $conds['ls_value'] = $this->ids; + return $conds; } /** * Show a deleted file version requested by the visitor. - * TODO Mostly copied from Special:Undelete. Refactor. + * @todo Mostly copied from Special:Undelete. Refactor. + * @param string $archiveName */ protected function tryShowFile( $archiveName ) { $repo = RepoGroup::singleton()->getLocalRepo(); @@ -250,6 +301,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { // Check if user is allowed to see this file if ( !$oimage->exists() ) { $this->getOutput()->addWikiMsg( 'revdelete-no-file' ); + return; } $user = $this->getUser(); @@ -269,7 +321,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $this->getOutput()->addHTML( Xml::openElement( 'form', array( 'method' => 'POST', - 'action' => $this->getTitle()->getLocalURL( array( + 'action' => $this->getPageTitle()->getLocalURL( array( 'target' => $this->targetObj->getPrefixedDBkey(), 'file' => $archiveName, 'token' => $user->getEditToken( $archiveName ), @@ -279,6 +331,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { Xml::submitButton( $this->msg( 'revdelete-show-file-submit' )->text() ) . '
    ' ); + return; } $this->getOutput()->disable(); @@ -287,7 +340,9 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { # a user without appropriate permissions can toddle off and # nab the image, and Squid will serve it $this->getRequest()->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); - $this->getRequest()->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); + $this->getRequest()->response()->header( + 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' + ); $this->getRequest()->response()->header( 'Pragma: no-cache' ); $key = $oimage->getStorageKey(); @@ -297,14 +352,16 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { /** * Get the list object for this request + * @return RevDelList */ protected function getList() { - if ( is_null( $this->list ) ) { - $this->list = RevisionDeleter::createList( + if ( is_null( $this->revDelList ) ) { + $this->revDelList = RevisionDeleter::createList( $this->typeName, $this->getContext(), $this->targetObj, $this->ids ); } - return $this->list; + + return $this->revDelList; } /** @@ -312,28 +369,29 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { * which will allow the user to choose new visibility settings. */ protected function showForm() { - $UserAllowed = true; + $userAllowed = true; - if ( $this->typeName == 'logging' ) { - $this->getOutput()->addWikiMsg( 'logdelete-selected', $this->getLanguage()->formatNum( count( $this->ids ) ) ); - } else { - $this->getOutput()->addWikiMsg( 'revdelete-selected', - $this->targetObj->getPrefixedText(), count( $this->ids ) ); - } + // Messages: revdelete-selected-text, revdelete-selected-file, logdelete-selected + $this->getOutput()->wrapWikiMsg( "$1", array( $this->typeLabels['selected'], + $this->getLanguage()->formatNum( count( $this->ids ) ), $this->targetObj->getPrefixedText() ) ); $this->getOutput()->addHTML( "
      " ); $numRevisions = 0; // Live revisions... $list = $this->getList(); + // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed for ( $list->reset(); $list->current(); $list->next() ) { + // @codingStandardsIgnoreEnd $item = $list->current(); + if ( !$item->canView() ) { if ( !$this->submitClicked ) { throw new PermissionsError( 'suppressrevision' ); } - $UserAllowed = false; + $userAllowed = false; } + $numRevisions++; $this->getOutput()->addHTML( $item->getHTML() ); } @@ -347,14 +405,14 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $this->addUsageText(); // Normal sysops can always see what they did, but can't always change it - if ( !$UserAllowed ) { + if ( !$userAllowed ) { return; } // Show form if the user can submit if ( $this->mIsAllowed ) { $out = Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ), + 'action' => $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ), 'id' => 'mw-revdel-form-revisions' ) ) . Xml::fieldset( $this->msg( 'revdelete-legend' )->text() ) . $this->buildCheckBoxes() . @@ -367,7 +425,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { Xml::listDropDown( 'wpRevDeleteReasonList', $this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->text(), $this->msg( 'revdelete-reasonotherlist' )->inContentLanguage()->text(), - $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ), 'wpReasonDropDown', 1 + $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ), 'wpReasonDropDown' ) . '
  • ' . - Xml::input( 'wpReason', 60, $this->otherReason, array( 'id' => 'wpReason', 'maxlength' => 100 ) ) . + Xml::input( + 'wpReason', + 60, + $this->otherReason, + array( 'id' => 'wpReason', 'maxlength' => 100 ) + ) . '
    '; @@ -434,26 +501,42 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { if ( $list->length() == 1 ) { $list->reset(); $bitfield = $list->current()->getBits(); // existing field + if ( $this->submitClicked ) { - $bitfield = $this->extractBitfield( $this->extractBitParams(), $bitfield ); + $bitfield = RevisionDeleter::extractBitfield( $this->extractBitParams(), $bitfield ); } + foreach ( $this->checks as $item ) { + // Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name, + // revdelete-hide-comment, revdelete-hide-user, revdelete-hide-restricted list( $message, $name, $field ) = $item; - $innerHTML = Xml::checkLabel( $this->msg( $message )->text(), $name, $name, $bitfield & $field ); + $innerHTML = Xml::checkLabel( + $this->msg( $message )->text(), + $name, + $name, + $bitfield & $field + ); + if ( $field == Revision::DELETED_RESTRICTED ) { $innerHTML = "$innerHTML"; } + $line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML ); $html .= "$line\n"; } - // Otherwise, use tri-state radios } else { + // Otherwise, use tri-state radios $html .= ''; - $html .= ''; - $html .= ''; - $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; $html .= "\n"; foreach ( $this->checks as $item ) { + // Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name, + // revdelete-hide-comment, revdelete-hide-user, revdelete-hide-restricted list( $message, $name, $field ) = $item; // If there are several items, use third state by default... if ( $this->submitClicked ) { @@ -474,6 +557,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { } $html .= '
    ' . $this->msg( 'revdelete-radio-same' )->escaped() . '' . $this->msg( 'revdelete-radio-unset' )->escaped() . '' . $this->msg( 'revdelete-radio-set' )->escaped() . '' + . $this->msg( 'revdelete-radio-same' )->escaped() . '' + . $this->msg( 'revdelete-radio-unset' )->escaped() . '' + . $this->msg( 'revdelete-radio-set' )->escaped() . '
    '; + return $html; } @@ -487,30 +571,37 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $token = $this->getRequest()->getVal( 'wpEditToken' ); if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) { $this->getOutput()->addWikiMsg( 'sessionfailure' ); + return false; } $bitParams = $this->extractBitParams(); - $listReason = $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ); // from dropdown + // from dropdown + $listReason = $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ); $comment = $listReason; - if ( $comment != 'other' && $this->otherReason != '' ) { - // Entry from drop down menu + additional comment - $comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $this->otherReason; - } elseif ( $comment == 'other' ) { + if ( $comment === 'other' ) { $comment = $this->otherReason; + } elseif ( $this->otherReason !== '' ) { + // Entry from drop down menu + additional comment + $comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text() + . $this->otherReason; } # Can the user set this field? - if ( $bitParams[Revision::DELETED_RESTRICTED] == 1 && !$this->getUser()->isAllowed( 'suppressrevision' ) ) { + if ( $bitParams[Revision::DELETED_RESTRICTED] == 1 + && !$this->getUser()->isAllowed( 'suppressrevision' ) + ) { throw new PermissionsError( 'suppressrevision' ); } # If the save went through, go to success message... $status = $this->save( $bitParams, $comment, $this->targetObj ); if ( $status->isGood() ) { $this->success(); + return true; - # ...otherwise, bounce back to form... } else { + # ...otherwise, bounce back to form... $this->failure( $status ); } + return false; } @@ -518,16 +609,23 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { * Report that the submit operation succeeded */ protected function success() { + // Messages: revdelete-success, logdelete-success $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) ); - $this->getOutput()->wrapWikiMsg( "\n$1\n", $this->typeLabels['success'] ); - $this->list->reloadFromMaster(); + $this->getOutput()->wrapWikiMsg( + "\n$1\n", + $this->typeLabels['success'] + ); + $this->wasSaved = true; + $this->revDelList->reloadFromMaster(); $this->showForm(); } /** * Report that the submit operation failed + * @param Status $status */ protected function failure( $status ) { + // Messages: revdelete-failure, logdelete-failure $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) ); $this->getOutput()->addWikiText( $status->getWikiText( $this->typeLabels['failure'] ) ); $this->showForm(); @@ -551,26 +649,16 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { if ( !isset( $bitfield[Revision::DELETED_RESTRICTED] ) ) { $bitfield[Revision::DELETED_RESTRICTED] = 0; } - return $bitfield; - } - /** - * Put together a rev_deleted bitfield - * @deprecated since 1.22, use RevisionDeleter::extractBitfield instead - * @param array $bitPars extractBitParams() params - * @param int $oldfield current bitfield - * @return array - */ - public static function extractBitfield( $bitPars, $oldfield ) { - return RevisionDeleter::extractBitfield( $bitPars, $oldfield ); + return $bitfield; } /** - * Do the write operations. Simple wrapper for RevDel_*List::setVisibility(). - * @param $bitfield - * @param $reason - * @param $title - * @return + * Do the write operations. Simple wrapper for RevDel*List::setVisibility(). + * @param int $bitfield + * @param string $reason + * @param Title $title + * @return Status */ protected function save( $bitfield, $reason, $title ) { return $this->getList()->setVisibility( diff --git a/includes/specials/SpecialRunJobs.php b/includes/specials/SpecialRunJobs.php new file mode 100644 index 00000000..d4a06eb5 --- /dev/null +++ b/includes/specials/SpecialRunJobs.php @@ -0,0 +1,112 @@ +getOutput()->disable(); + + if ( wfReadOnly() ) { + header( "HTTP/1.0 423 Locked" ); + print 'Wiki is in read-only mode'; + + return; + } elseif ( !$this->getRequest()->wasPosted() ) { + header( "HTTP/1.0 400 Bad Request" ); + print 'Request must be POSTed'; + + return; + } + + $optional = array( 'maxjobs' => 0, 'maxtime' => 30, 'type' => false, 'async' => true ); + $required = array_flip( array( 'title', 'tasks', 'signature', 'sigexpiry' ) ); + + $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" ); + print 'Missing parameters: ' . implode( ', ', array_keys( $missing ) ); + + return; + } + + $squery = $params; + unset( $squery['signature'] ); + $cSig = self::getQuerySignature( $squery, $this->getConfig()->get( 'SecretKey' ) ); // correct signature + $rSig = $params['signature']; // provided signature + + $verified = is_string( $rSig ) && hash_equals( $cSig, $rSig ); + if ( !$verified || $params['sigexpiry'] < time() ) { + header( "HTTP/1.0 400 Bad Request" ); + print 'Invalid or stale signature provided'; + + return; + } + + // Apply any default parameter values + $params += $optional; + + if ( $params['async'] ) { + // Client will usually disconnect before checking the response, + // 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" ); + ob_flush(); + flush(); + // Once the client receives this response, it can disconnect + } + + // Do all of the specified tasks... + if ( in_array( 'jobs', explode( '|', $params['tasks'] ) ) ) { + $runner = new JobRunner(); + $response = $runner->run( array( + 'type' => $params['type'], + 'maxJobs' => $params['maxjobs'] ? $params['maxjobs'] : 1, + 'maxTime' => $params['maxtime'] ? $params['maxjobs'] : 30 + ) ); + if ( !$params['async'] ) { + print FormatJson::encode( $response, true ); + } + } + } + + /** + * @param array $query + * @param string $secretKey + * @return string + */ + public static function getQuerySignature( array $query, $secretKey ) { + ksort( $query ); // stable order + return hash_hmac( 'sha1', wfArrayToCgi( $query ), $secretKey ); + } +} diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 8609c740..88ab7d82 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -30,25 +30,24 @@ class SpecialSearch extends SpecialPage { /** * Current search profile. Search profile is just a name that identifies - * the active search tab on the search page (content, help, discussions...) + * the active search tab on the search page (content, discussions...) * For users tt replaces the set of enabled namespaces from the query * string when applicable. Extensions can add new profiles with hooks * with custom search options just for that profile. - * null|string + * @var null|string */ protected $profile; - function getProfile() { return $this->profile; } - /// Search engine + /** @var SearchEngine Search engine */ protected $searchEngine; - /// Search engine type, if not default + /** @var string Search engine type, if not default */ protected $searchEngineType; - /// For links + /** @var array For links */ protected $extraParams = array(); - /// No idea, apparently used by some other classes + /** @var string No idea, apparently used by some other classes */ protected $mPrefix; /** @@ -60,12 +59,6 @@ class SpecialSearch extends SpecialPage { * @var array */ protected $namespaces; - function getNamespaces() { return $this->namespaces; } - - /** - * @var bool - */ - protected $searchRedirects; /** * @var string @@ -81,14 +74,17 @@ class SpecialSearch extends SpecialPage { /** * Entry point * - * @param string $par or null + * @param string $par */ public function execute( $par ) { $this->setHeaders(); $this->outputHeader(); $out = $this->getOutput(); $out->allowClickjacking(); - $out->addModuleStyles( 'mediawiki.special' ); + $out->addModuleStyles( array( + 'mediawiki.special', 'mediawiki.special.search', 'mediawiki.ui', 'mediawiki.ui.button', + 'mediawiki.ui.input', + ) ); // Strip underscores from title parameter; most of the time we'll want // text form here. But don't strip underscores from actual text params! @@ -100,13 +96,22 @@ class SpecialSearch extends SpecialPage { $search = str_replace( "\n", " ", $request->getText( 'search', $titleParam ) ); $this->load(); + if ( !is_null( $request->getVal( 'nsRemember' ) ) ) { + $this->saveNamespaces(); + // Remove the token from the URL to prevent the user from inadvertently + // exposing it (e.g. by pasting it into a public wiki page) or undoing + // later settings changes (e.g. by reloading the page). + $query = $request->getValues(); + unset( $query['title'], $query['nsRemember'] ); + $out->redirect( $this->getPageTitle()->getFullURL( $query ) ); + return; + } $this->searchEngineType = $request->getVal( 'srbackend' ); if ( $request->getVal( 'fulltext' ) || !is_null( $request->getVal( 'offset' ) ) - || !is_null( $request->getVal( 'searchx' ) ) ) - { + ) { $this->showResults( $search ); } else { $this->goResult( $search ); @@ -120,7 +125,7 @@ class SpecialSearch extends SpecialPage { */ public function load() { $request = $this->getRequest(); - list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' ); + list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, '' ); $this->mPrefix = $request->getVal( 'prefix', '' ); $user = $this->getUser(); @@ -160,9 +165,6 @@ class SpecialSearch extends SpecialPage { } } - // Redirects defaults to true, but we don't know whether it was ticked of or just missing - $default = $request->getBool( 'profile' ) ? 0 : 1; - $this->searchRedirects = $request->getBool( 'redirs', $default ) ? 1 : 0; $this->didYouMeanHtml = ''; # html of did you mean... link $this->fulltext = $request->getVal( 'fulltext' ); $this->profile = $profile; @@ -171,57 +173,44 @@ class SpecialSearch extends SpecialPage { /** * If an exact title match can be found, jump straight ahead to it. * - * @param $term String + * @param string $term */ public function goResult( $term ) { $this->setupPage( $term ); # Try to go to page as entered. - $t = Title::newFromText( $term ); + $title = Title::newFromText( $term ); # If the string cannot be used to create a title - if ( is_null( $t ) ) { + if ( is_null( $title ) ) { $this->showResults( $term ); + return; } # If there's an exact or very near match, jump right there. - $t = SearchEngine::getNearMatch( $term ); + $title = SearchEngine::getNearMatch( $term ); - if ( !wfRunHooks( 'SpecialSearchGo', array( &$t, &$term ) ) ) { - # Hook requested termination - return; - } + if ( !is_null( $title ) ) { + $this->getOutput()->redirect( $title->getFullURL() ); - if ( !is_null( $t ) ) { - $this->getOutput()->redirect( $t->getFullURL() ); return; } # No match, generate an edit URL - $t = Title::newFromText( $term ); - if ( !is_null( $t ) ) { - global $wgGoToEdit; - wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) ); - wfDebugLog( 'nogomatch', $t->getText(), false ); - - # If the feature is enabled, go straight to the edit page - if ( $wgGoToEdit ) { - $this->getOutput()->redirect( $t->getFullURL( array( 'action' => 'edit' ) ) ); - return; - } + $title = Title::newFromText( $term ); + if ( !is_null( $title ) ) { + wfRunHooks( 'SpecialSearchNogomatch', array( &$title ) ); } $this->showResults( $term ); } /** - * @param $term String + * @param string $term */ public function showResults( $term ) { - global $wgDisableTextSearch, $wgSearchForwardUrl, $wgContLang, $wgScript; - wfProfileIn( __METHOD__ ); + global $wgContLang; + $profile = new ProfileSection( __METHOD__ ); $search = $this->getSearchEngine(); $search->setLimitOffset( $this->limit, $this->offset ); $search->setNamespaces( $this->namespaces ); - $search->showRedirects = $this->searchRedirects; // BC - $search->setFeatureData( 'list-redirects', $this->searchRedirects ); $search->prefix = $this->mPrefix; $term = $search->transformSearchTerm( $term ); @@ -231,15 +220,20 @@ class SpecialSearch extends SpecialPage { $out = $this->getOutput(); - if ( $wgDisableTextSearch ) { - if ( $wgSearchForwardUrl ) { - $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl ); + if ( $this->getConfig()->get( 'DisableTextSearch' ) ) { + $searchFowardUrl = $this->getConfig()->get( 'SearchForwardUrl' ); + if ( $searchFowardUrl ) { + $url = str_replace( '$1', urlencode( $term ), $searchFowardUrl ); $out->redirect( $url ); } else { $out->addHTML( Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) . - Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), $this->msg( 'searchdisabled' )->text() ) . + Xml::element( + 'p', + array( 'class' => 'mw-searchdisabled' ), + $this->msg( 'searchdisabled' )->text() + ) . $this->msg( 'googlesearch' )->rawParams( htmlspecialchars( $term ), 'UTF-8', @@ -248,19 +242,19 @@ class SpecialSearch extends SpecialPage { Xml::closeElement( 'fieldset' ) ); } - wfProfileOut( __METHOD__ ); + return; } - $t = Title::newFromText( $term ); + $title = Title::newFromText( $term ); + $showSuggestion = $title === null || !$title->isKnown(); + $search->setShowSuggestion( $showSuggestion ); // fetch search results $rewritten = $search->replacePrefixes( $term ); $titleMatches = $search->searchTitle( $rewritten ); - if ( !( $titleMatches instanceof SearchResultTooMany ) ) { - $textMatches = $search->searchText( $rewritten ); - } + $textMatches = $search->searchText( $rewritten ); $textStatus = null; if ( $textMatches instanceof Status ) { @@ -269,9 +263,7 @@ class SpecialSearch extends SpecialPage { } // did you mean... suggestions - if ( $textMatches && !$textStatus && $textMatches->hasSuggestion() ) { - $st = SpecialPage::getTitleFor( 'Search' ); - + if ( $showSuggestion && $textMatches && !$textStatus && $textMatches->hasSuggestion() ) { # mirror Go/Search behavior of original request .. $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() ); @@ -291,18 +283,18 @@ class SpecialSearch extends SpecialPage { } $suggestLink = Linker::linkKnown( - $st, + $this->getPageTitle(), $suggestionSnippet, array(), $stParams ); - $this->didYouMeanHtml = '
    ' . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '
    '; + $this->didYouMeanHtml = '
    ' + . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '
    '; } if ( !wfRunHooks( 'SpecialSearchResultsPrepend', array( $this, $out, $term ) ) ) { # Hook requested termination - wfProfileOut( __METHOD__ ); return; } @@ -313,78 +305,62 @@ class SpecialSearch extends SpecialPage { array( 'id' => ( $this->profile === 'advanced' ? 'powersearch' : 'search' ), 'method' => 'get', - 'action' => $wgScript + 'action' => wfScript(), ) ) ); - $out->addHtml( - Xml::openElement( 'table', array( 'id' => 'mw-search-top-table', 'cellpadding' => 0, 'cellspacing' => 0 ) ) . - Xml::openElement( 'tr' ) . - Xml::openElement( 'td' ) . "\n" . - $this->shortDialog( $term ) . - Xml::closeElement( 'td' ) . - Xml::closeElement( 'tr' ) . - Xml::closeElement( 'table' ) - ); - // Sometimes the search engine knows there are too many hits - if ( $titleMatches instanceof SearchResultTooMany ) { - $out->wrapWikiMsg( "==$1==\n", 'toomanymatches' ); - wfProfileOut( __METHOD__ ); - return; + // Get number of results + $titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0; + if ( $titleMatches ) { + $titleMatchesNum = $titleMatches->numRows(); + $numTitleMatches = $titleMatches->getTotalHits(); + } + if ( $textMatches ) { + $textMatchesNum = $textMatches->numRows(); + $numTextMatches = $textMatches->getTotalHits(); } + $num = $titleMatchesNum + $textMatchesNum; + $totalRes = $numTitleMatches + $numTextMatches; + + $out->addHtml( + # This is an awful awful ID name. It's not a table, but we + # named it poorly from when this was a table so now we're + # stuck with it + Xml::openElement( 'div', array( 'id' => 'mw-search-top-table' ) ) . + $this->shortDialog( $term, $num, $totalRes ) . + Xml::closeElement( 'div' ) . + $this->formHeader( $term ) . + Xml::closeElement( 'form' ) + ); $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':'; if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) { - $out->addHTML( $this->formHeader( $term, 0, 0 ) ); - $out->addHtml( $this->getProfileForm( $this->profile, $term ) ); - $out->addHTML( '' ); // Empty query -- straight view of search form - wfProfileOut( __METHOD__ ); return; } - // Get number of results - $titleMatchesNum = $titleMatches ? $titleMatches->numRows() : 0; - $textMatchesNum = $textMatches ? $textMatches->numRows() : 0; - // Total initial query matches (possible false positives) - $num = $titleMatchesNum + $textMatchesNum; - - // Get total actual results (after second filtering, if any) - $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ? - $titleMatches->getTotalHits() : $titleMatchesNum; - $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ? - $textMatches->getTotalHits() : $textMatchesNum; - - // get total number of results if backend can calculate it - $totalRes = 0; - if ( $titleMatches && !is_null( $titleMatches->getTotalHits() ) ) { - $totalRes += $titleMatches->getTotalHits(); - } - if ( $textMatches && !is_null( $textMatches->getTotalHits() ) ) { - $totalRes += $textMatches->getTotalHits(); - } - - // show number of results and current offset - $out->addHTML( $this->formHeader( $term, $num, $totalRes ) ); - $out->addHtml( $this->getProfileForm( $this->profile, $term ) ); - - $out->addHtml( Xml::closeElement( 'form' ) ); $out->addHtml( "
    " ); // prev/next links + $prevnext = null; if ( $num || $this->offset ) { // Show the create link ahead - $this->showCreateLink( $t ); - $prevnext = $this->getLanguage()->viewPrevNext( $this->getTitle(), $this->offset, $this->limit, - $this->powerSearchOptions() + array( 'search' => $term ), - max( $titleMatchesNum, $textMatchesNum ) < $this->limit - ); - //$out->addHTML( "

    {$prevnext}

    \n" ); - wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); - } else { - wfRunHooks( 'SpecialSearchNoResults', array( $term ) ); + $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); + if ( $totalRes > $this->limit || $this->offset ) { + if ( $this->searchEngineType !== null ) { + $this->setExtraParam( 'srbackend', $this->searchEngineType ); + } + $prevnext = $this->getLanguage()->viewPrevNext( + $this->getPageTitle(), + $this->offset, + $this->limit, + $this->powerSearchOptions() + array( 'search' => $term ), + $this->limit + $this->offset >= $totalRes + ); + } } + wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); $out->parserOptions()->setEditSection( false ); if ( $titleMatches ) { @@ -399,10 +375,8 @@ class SpecialSearch extends SpecialPage { if ( $numTextMatches > 0 && $numTitleMatches > 0 ) { // if no title matches the heading is redundant $out->wrapWikiMsg( "==$1==\n", 'textmatches' ); - } elseif ( $totalRes == 0 ) { - # Don't show the 'no text matches' if we received title matches - # $out->wrapWikiMsg( "==$1==\n", 'notextmatches' ); } + // show interwiki results if any if ( $textMatches->hasInterwikiResults() ) { $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) ); @@ -417,49 +391,60 @@ class SpecialSearch extends SpecialPage { if ( $num === 0 ) { if ( $textStatus ) { $out->addHTML( '
    ' . - htmlspecialchars( $textStatus->getWikiText( 'search-error' ) ) . '
    ' ); + $textStatus->getMessage( 'search-error' ) . '
    ' ); } else { $out->wrapWikiMsg( "

    \n$1

    ", array( 'search-nonefound', wfEscapeWikiText( $term ) ) ); - $this->showCreateLink( $t ); + $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); } } $out->addHtml( "" ); - if ( $num || $this->offset ) { + if ( $prevnext ) { $out->addHTML( "

    {$prevnext}

    \n" ); } - wfRunHooks( 'SpecialSearchResultsAppend', array( $this, $out, $term ) ); - wfProfileOut( __METHOD__ ); } /** - * @param $t Title + * @param Title $title + * @param int $num The number of search results found + * @param null|SearchResultSet $titleMatches Results from title search + * @param null|SearchResultSet $textMatches Results from text search */ - protected function showCreateLink( $t ) { + protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) { // show direct page/create link if applicable // Check DBkey !== '' in case of fragment link only. - if ( is_null( $t ) || $t->getDBkey() === '' ) { + if ( is_null( $title ) || $title->getDBkey() === '' + || ( $titleMatches !== null && $titleMatches->searchContainedSyntax() ) + || ( $textMatches !== null && $textMatches->searchContainedSyntax() ) + ) { // invalid title // preserve the paragraph for margins etc... $this->getOutput()->addHtml( '

    ' ); + return; } - if ( $t->isKnown() ) { + $linkClass = 'mw-search-createlink'; + if ( $title->isKnown() ) { $messageName = 'searchmenu-exists'; - } elseif ( $t->userCan( 'create', $this->getUser() ) ) { + $linkClass = 'mw-search-exists'; + } elseif ( $title->quickUserCan( 'create', $this->getUser() ) ) { $messageName = 'searchmenu-new'; } else { $messageName = 'searchmenu-new-nocreate'; } - $params = array( $messageName, wfEscapeWikiText( $t->getPrefixedText() ) ); - wfRunHooks( 'SpecialSearchCreateLink', array( $t, &$params ) ); + $params = array( + $messageName, + wfEscapeWikiText( $title->getPrefixedText() ), + Message::numParam( $num ) + ); + wfRunHooks( 'SpecialSearchCreateLink', array( $title, &$params ) ); // Extensions using the hook might still return an empty $messageName if ( $messageName ) { - $this->getOutput()->wrapWikiMsg( "

    \n$1

    ", $params ); + $this->getOutput()->wrapWikiMsg( "

    \n$1

    ", $params ); } else { // preserve the paragraph for margins etc... $this->getOutput()->addHtml( '

    ' ); @@ -467,7 +452,7 @@ class SpecialSearch extends SpecialPage { } /** - * @param $term string + * @param string $term */ protected function setupPage( $term ) { # Should advanced UI be used? @@ -475,9 +460,10 @@ class SpecialSearch extends SpecialPage { $out = $this->getOutput(); if ( strval( $term ) !== '' ) { $out->setPageTitle( $this->msg( 'searchresults' ) ); - $out->setHTMLTitle( $this->msg( 'pagetitle' )->rawParams( - $this->msg( 'searchresults-title' )->rawParams( $term )->text() - ) ); + $out->setHTMLTitle( $this->msg( 'pagetitle' ) + ->rawParams( $this->msg( 'searchresults-title' )->rawParams( $term )->text() ) + ->inContentLanguage()->text() + ); } // add javascript specific to special:search $out->addModules( 'mediawiki.special.search' ); @@ -487,8 +473,8 @@ class SpecialSearch extends SpecialPage { * Extract "power search" namespace settings from the request object, * returning a list of index numbers to search. * - * @param $request WebRequest - * @return Array + * @param WebRequest $request + * @return array */ protected function powerSearch( &$request ) { $arr = array(); @@ -504,11 +490,10 @@ class SpecialSearch extends SpecialPage { /** * Reconstruct the 'power search' options for links * - * @return Array + * @return array */ protected function powerSearchOptions() { $opt = array(); - $opt['redirs'] = $this->searchRedirects ? 1 : 0; if ( $this->profile !== 'advanced' ) { $opt['profile'] = $this->profile; } else { @@ -516,28 +501,58 @@ class SpecialSearch extends SpecialPage { $opt['ns' . $n] = 1; } } + return $opt + $this->extraParams; } + /** + * Save namespace preferences when we're supposed to + * + * @return bool Whether we wrote something + */ + protected function saveNamespaces() { + $user = $this->getUser(); + $request = $this->getRequest(); + + if ( $user->isLoggedIn() && + $user->matchEditToken( + $request->getVal( 'nsRemember' ), + 'searchnamespace', + $request + ) + ) { + // Reset namespace preferences: namespaces are not searched + // when they're not mentioned in the URL parameters. + foreach ( MWNamespace::getValidNamespaces() as $n ) { + $user->setOption( 'searchNs' . $n, false ); + } + // The request parameters include all the namespaces to be searched. + // Even if they're the same as an existing profile, they're not eaten. + foreach ( $this->namespaces as $n ) { + $user->setOption( 'searchNs' . $n, true ); + } + + $user->saveSettings(); + return true; + } + + return false; + } + /** * Show whole set of results * - * @param $matches SearchResultSet + * @param SearchResultSet $matches * * @return string */ protected function showMatches( &$matches ) { global $wgContLang; - wfProfileIn( __METHOD__ ); + $profile = new ProfileSection( __METHOD__ ); $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); - $out = ""; - $infoLine = $matches->getInfo(); - if ( !is_null( $infoLine ) ) { - $out .= "\n\n"; - } - $out .= "
      \n"; + $out = "
        \n"; $result = $matches->next(); while ( $result ) { $out .= $this->showHit( $result, $terms ); @@ -547,27 +562,26 @@ class SpecialSearch extends SpecialPage { // convert the whole thing to desired language variant $out = $wgContLang->convert( $out ); - wfProfileOut( __METHOD__ ); + return $out; } /** * Format a single hit result * - * @param $result SearchResult - * @param array $terms terms to highlight + * @param SearchResult $result + * @param array $terms Terms to highlight * * @return string */ protected function showHit( $result, $terms ) { - wfProfileIn( __METHOD__ ); + $profile = new ProfileSection( __METHOD__ ); if ( $result->isBrokenTitle() ) { - wfProfileOut( __METHOD__ ); - return "\n"; + return ''; } - $t = $result->getTitle(); + $title = $result->getTitle(); $titleSnippet = $result->getTitleSnippet( $terms ); @@ -575,10 +589,10 @@ class SpecialSearch extends SpecialPage { $titleSnippet = null; } - $link_t = clone $t; + $link_t = clone $title; wfRunHooks( 'ShowSearchHitTitle', - array( &$link_t, &$titleSnippet, $result, $terms, $this ) ); + array( &$link_t, &$titleSnippet, $result, $terms, $this ) ); $link = Linker::linkKnown( $link_t, @@ -588,8 +602,7 @@ class SpecialSearch extends SpecialPage { //If page content is not readable, just return the title. //This is not quite safe, but better than showing excerpts from non-readable pages //Note that hiding the entry entirely would screw up paging. - if ( !$t->userCan( 'read', $this->getUser() ) ) { - wfProfileOut( __METHOD__ ); + if ( !$title->userCan( 'read', $this->getUser() ) ) { return "
      • {$link}
      • \n"; } @@ -597,8 +610,7 @@ class SpecialSearch extends SpecialPage { // The least confusing at this point is to drop the result. // You may get less results, but... oh well. :P if ( $result->isMissingRevision() ) { - wfProfileOut( __METHOD__ ); - return "\n"; + return ''; } // format redirects / relevant sections @@ -637,16 +649,6 @@ class SpecialSearch extends SpecialPage { $lang = $this->getLanguage(); - // format score - if ( is_null( $result->getScore() ) ) { - // Search engine doesn't report scoring info - $score = ''; - } else { - $percent = sprintf( '%2.1f', $result->getScore() * 100 ); - $score = $this->msg( 'search-result-score' )->numParams( $percent )->text() - . ' - '; - } - // format description $byteSize = $result->getByteSize(); $wordCount = $result->getWordCount(); @@ -654,8 +656,8 @@ class SpecialSearch extends SpecialPage { $size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) ) ->numParams( $wordCount )->escaped(); - if ( $t->getNamespace() == NS_CATEGORY ) { - $cat = Category::newFromTitle( $t ); + if ( $title->getNamespace() == NS_CATEGORY ) { + $cat = Category::newFromTitle( $title ); $size = $this->msg( 'search-result-category-size' ) ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() ) ->escaped(); @@ -663,35 +665,19 @@ class SpecialSearch extends SpecialPage { $date = $lang->userTimeAndDate( $timestamp, $this->getUser() ); - // link to related articles if supported - $related = ''; - if ( $result->hasRelated() ) { - $st = SpecialPage::getTitleFor( 'Search' ); - $stParams = array_merge( - $this->powerSearchOptions(), - array( - 'search' => $this->msg( 'searchrelated' )->inContentLanguage()->text() . - ':' . $t->getPrefixedText(), - 'fulltext' => $this->msg( 'search' )->text() - ) - ); - - $related = ' -- ' . Linker::linkKnown( - $st, - $this->msg( 'search-relatedarticle' )->text(), - array(), - $stParams - ); - } - + $fileMatch = ''; // Include a thumbnail for media files... - if ( $t->getNamespace() == NS_FILE ) { - $img = wfFindFile( $t ); + if ( $title->getNamespace() == NS_FILE ) { + $img = $result->getFile(); + $img = $img ?: wfFindFile( $title ); + if ( $result->isFileMatch() ) { + $fileMatch = "" . + $this->msg( 'search-file-match' )->escaped() . ""; + } if ( $img ) { $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) ); if ( $thumb ) { $desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped(); - wfProfileOut( __METHOD__ ); // Float doesn't seem to interact well with the bullets. // Table messes up vertical alignment of the bullets. // Bullets are therefore disabled (didn't look great anyway). @@ -702,9 +688,9 @@ class SpecialSearch extends SpecialPage { $thumb->toHtml( array( 'desc-link' => true ) ) . '' . '' . - $link . + "{$link} {$redirect} {$section} {$fileMatch}" . $extract . - "
        {$score}{$desc} - {$date}{$related}
        " . + "
        {$desc} - {$date}
        " . '' . '' . '' . @@ -715,33 +701,33 @@ class SpecialSearch extends SpecialPage { $html = null; + $score = ''; if ( wfRunHooks( 'ShowSearchHit', array( $this, $result, $terms, &$link, &$redirect, &$section, &$extract, &$score, &$size, &$date, &$related, &$html ) ) ) { - $html = "
      • {$link} {$redirect} {$section}
        {$extract}\n" . - "
        {$score}{$size} - {$date}{$related}
        " . + $html = "
      • " . + "{$link} {$redirect} {$section} {$fileMatch}
        {$extract}\n" . + "
        {$size} - {$date}
        " . "
      • \n"; } - wfProfileOut( __METHOD__ ); return $html; } /** * Show results from other wikis * - * @param $matches SearchResultSet - * @param $query String + * @param SearchResultSet|array $matches + * @param string $query * * @return string */ - protected function showInterwiki( &$matches, $query ) { + protected function showInterwiki( $matches, $query ) { global $wgContLang; - wfProfileIn( __METHOD__ ); - $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); + $profile = new ProfileSection( __METHOD__ ); $out = "
        " . $this->msg( 'search-interwiki-caption' )->text() . "
        \n"; @@ -749,7 +735,8 @@ class SpecialSearch extends SpecialPage { // work out custom project captions $customCaptions = array(); - $customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() ); // format per line : + // format per line : + $customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() ); foreach ( $customLines as $line ) { $parts = explode( ":", $line, 2 ); if ( count( $parts ) == 2 ) { // validate line @@ -757,57 +744,62 @@ class SpecialSearch extends SpecialPage { } } - $prev = null; - $result = $matches->next(); - while ( $result ) { - $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions ); - $prev = $result->getInterwikiPrefix(); - $result = $matches->next(); + if ( !is_array( $matches ) ) { + $matches = array( $matches ); } - // TODO: should support paging in a non-confusing way (not sure how though, maybe via ajax).. + + foreach ( $matches as $set ) { + $prev = null; + $result = $set->next(); + while ( $result ) { + $out .= $this->showInterwikiHit( $result, $prev, $query, $customCaptions ); + $prev = $result->getInterwikiPrefix(); + $result = $set->next(); + } + } + + // @todo Should support paging in a non-confusing way (not sure how though, maybe via ajax).. $out .= "
      \n"; // convert the whole thing to desired language variant $out = $wgContLang->convert( $out ); - wfProfileOut( __METHOD__ ); + return $out; } /** * Show single interwiki link * - * @param $result SearchResult - * @param $lastInterwiki String - * @param $terms Array - * @param $query String - * @param array $customCaptions iw prefix -> caption + * @param SearchResult $result + * @param string $lastInterwiki + * @param string $query + * @param array $customCaptions Interwiki prefix -> caption * * @return string */ - protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions ) { - wfProfileIn( __METHOD__ ); + protected function showInterwikiHit( $result, $lastInterwiki, $query, $customCaptions ) { + $profile = new ProfileSection( __METHOD__ ); if ( $result->isBrokenTitle() ) { - wfProfileOut( __METHOD__ ); - return "\n"; + return ''; } - $t = $result->getTitle(); + $title = $result->getTitle(); - $titleSnippet = $result->getTitleSnippet( $terms ); + $titleSnippet = $result->getTitleSnippet(); if ( $titleSnippet == '' ) { $titleSnippet = null; } $link = Linker::linkKnown( - $t, + $title, $titleSnippet ); // format redirect if any $redirectTitle = $result->getRedirectTitle(); - $redirectText = $result->getRedirectSnippet( $terms ); + $redirectText = $result->getRedirectSnippet(); $redirect = ''; if ( !is_null( $redirectTitle ) ) { if ( $redirectText == '' ) { @@ -822,18 +814,18 @@ class SpecialSearch extends SpecialPage { $out = ""; // display project name - if ( is_null( $lastInterwiki ) || $lastInterwiki != $t->getInterwiki() ) { - if ( array_key_exists( $t->getInterwiki(), $customCaptions ) ) { + if ( is_null( $lastInterwiki ) || $lastInterwiki != $title->getInterwiki() ) { + if ( array_key_exists( $title->getInterwiki(), $customCaptions ) ) { // captions from 'search-interwiki-custom' - $caption = $customCaptions[$t->getInterwiki()]; + $caption = $customCaptions[$title->getInterwiki()]; } else { // default is to show the hostname of the other wiki which might suck // if there are many wikis on one hostname - $parsed = wfParseUrl( $t->getFullURL() ); + $parsed = wfParseUrl( $title->getFullURL() ); $caption = $this->msg( 'search-interwiki-default', $parsed['host'] )->text(); } // "more results" link (special page stuff could be localized, but we might not know target lang) - $searchTitle = Title::newFromText( $t->getInterwiki() . ":Special:Search" ); + $searchTitle = Title::newFromText( $title->getInterwiki() . ":Special:Search" ); $searchLink = Linker::linkKnown( $searchTitle, $this->msg( 'search-interwiki-more' )->text(), @@ -848,36 +840,16 @@ class SpecialSearch extends SpecialPage { } $out .= "
    • {$link} {$redirect}
    • \n"; - wfProfileOut( __METHOD__ ); - return $out; - } - /** - * @param $profile - * @param $term - * @return String - */ - protected function getProfileForm( $profile, $term ) { - // Hidden stuff - $opts = array(); - $opts['redirs'] = $this->searchRedirects; - $opts['profile'] = $this->profile; - - if ( $profile === 'advanced' ) { - return $this->powerSearchBox( $term, $opts ); - } else { - $form = ''; - wfRunHooks( 'SpecialSearchProfileForm', array( $this, &$form, $profile, $term, $opts ) ); - return $form; - } + return $out; } /** * Generates the power search box at [[Special:Search]] * - * @param string $term search term - * @param $opts array - * @return String: HTML form + * @param string $term Search term + * @param array $opts + * @return string HTML form */ protected function powerSearchBox( $term, $opts ) { global $wgContLang; @@ -896,9 +868,7 @@ class SpecialSearch extends SpecialPage { } $rows[$subject] .= - Xml::openElement( - 'td', array( 'style' => 'white-space: nowrap' ) - ) . + Xml::openElement( 'td' ) . Xml::checkLabel( $name, "ns{$namespace}", @@ -929,30 +899,41 @@ class SpecialSearch extends SpecialPage { $showSections = array( 'namespaceTables' => $namespaceTables ); - // Show redirects check only if backend supports it - if ( $this->getSearchEngine()->supports( 'list-redirects' ) ) { - $showSections['redirects'] = - Xml::checkLabel( $this->msg( 'powersearch-redir' )->text(), 'redirs', 'redirs', $this->searchRedirects ); - } - wfRunHooks( 'SpecialSearchPowerBox', array( &$showSections, $term, $opts ) ); $hidden = ''; - unset( $opts['redirs'] ); foreach ( $opts as $key => $value ) { $hidden .= Html::hidden( $key, $value ); } + + # Stuff to feed saveNamespaces() + $remember = ''; + $user = $this->getUser(); + if ( $user->isLoggedIn() ) { + $remember .= Xml::checkLabel( + wfMessage( 'powersearch-remember' )->text(), + 'nsRemember', + 'mw-search-powersearch-remember', + false, + // The token goes here rather than in a hidden field so it + // is only sent when necessary (not every form submission). + array( 'value' => $user->getEditToken( + 'searchnamespace', + $this->getRequest() + ) ) + ); + } + // Return final output - return Xml::openElement( - 'fieldset', - array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' ) - ) . + return Xml::openElement( 'fieldset', array( 'id' => 'mw-searchoptions' ) ) . Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) . Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) . - Html::element( 'div', array( 'id' => 'mw-search-togglebox' ) ) . + Xml::element( 'div', array( 'id' => 'mw-search-togglebox' ), '', false ) . Xml::element( 'div', array( 'class' => 'divider' ), '', false ) . implode( Xml::element( 'div', array( 'class' => 'divider' ), '', false ), $showSections ) . $hidden . + Xml::element( 'div', array( 'class' => 'divider' ), '', false ) . + $remember . Xml::closeElement( 'fieldset' ); } @@ -977,14 +958,6 @@ class SpecialSearch extends SpecialPage { 'tooltip' => 'searchprofile-images-tooltip', 'namespaces' => array( NS_FILE ), ), - 'help' => array( - 'message' => 'searchprofile-project', - 'tooltip' => 'searchprofile-project-tooltip', - 'namespaces' => SearchEngine::helpNamespaces(), - 'namespace-messages' => SearchEngine::namespacesAsText( - SearchEngine::helpNamespaces() - ), - ), 'all' => array( 'message' => 'searchprofile-everything', 'tooltip' => 'searchprofile-everything-tooltip', @@ -1010,12 +983,10 @@ class SpecialSearch extends SpecialPage { } /** - * @param $term - * @param $resultsShown - * @param $totalNum + * @param string $term * @return string */ - protected function formHeader( $term, $resultsShown, $totalNum ) { + protected function formHeader( $term ) { $out = Xml::openElement( 'div', array( 'class' => 'mw-search-formheader' ) ); $bareterm = $term; @@ -1054,69 +1025,74 @@ class SpecialSearch extends SpecialPage { } $out .= Xml::closeElement( 'ul' ); $out .= Xml::closeElement( 'div' ); - - // Results-info - if ( $resultsShown > 0 ) { - if ( $totalNum > 0 ) { - $top = $this->msg( 'showingresultsheader' ) - ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum ) - ->params( wfEscapeWikiText( $term ) ) - ->numParams( $resultsShown ) - ->parse(); - } elseif ( $resultsShown >= $this->limit ) { - $top = $this->msg( 'showingresults' ) - ->numParams( $this->limit, $this->offset + 1 ) - ->parse(); - } else { - $top = $this->msg( 'showingresultsnum' ) - ->numParams( $this->limit, $this->offset + 1, $resultsShown ) - ->parse(); - } - $out .= Xml::tags( 'div', array( 'class' => 'results-info' ), - Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) ) - ); - } - $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false ); $out .= Xml::closeElement( 'div' ); + // Hidden stuff + $opts = array(); + $opts['profile'] = $this->profile; + + if ( $this->profile === 'advanced' ) { + $out .= $this->powerSearchBox( $term, $opts ); + } else { + $form = ''; + wfRunHooks( 'SpecialSearchProfileForm', array( $this, &$form, $this->profile, $term, $opts ) ); + $out .= $form; + } + return $out; } /** - * @param $term string + * @param string $term + * @param int $resultsShown + * @param int $totalNum * @return string */ - protected function shortDialog( $term ) { - $out = Html::hidden( 'title', $this->getTitle()->getPrefixedText() ); + protected function shortDialog( $term, $resultsShown, $totalNum ) { + $out = Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ); $out .= Html::hidden( 'profile', $this->profile ) . "\n"; // Term box $out .= Html::input( 'search', $term, 'search', array( 'id' => $this->profile === 'advanced' ? 'powerSearchText' : 'searchText', 'size' => '50', - 'autofocus' + 'autofocus', + 'class' => 'mw-ui-input mw-ui-input-inline', ) ) . "\n"; $out .= Html::hidden( 'fulltext', 'Search' ) . "\n"; - $out .= Xml::submitButton( $this->msg( 'searchbutton' )->text() ) . "\n"; + $out .= Xml::submitButton( + $this->msg( 'searchbutton' )->text(), + array( 'class' => array( 'mw-ui-button', 'mw-ui-progressive' ) ) + ) . "\n"; + + // Results-info + if ( $totalNum > 0 && $this->offset < $totalNum ) { + $top = $this->msg( 'search-showingresults' ) + ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum ) + ->numParams( $resultsShown ) + ->parse(); + $out .= Xml::tags( 'div', array( 'class' => 'results-info' ), $top ) . + Xml::element( 'div', array( 'style' => 'clear:both' ), '', false ); + } + return $out . $this->didYouMeanHtml; } /** * Make a search link with some target namespaces * - * @param $term String - * @param array $namespaces ignored - * @param string $label link's text - * @param string $tooltip link's tooltip - * @param array $params query string parameters - * @return String: HTML fragment + * @param string $term + * @param array $namespaces Ignored + * @param string $label Link's text + * @param string $tooltip Link's tooltip + * @param array $params Query string parameters + * @return string HTML fragment */ protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = array() ) { $opt = $params; foreach ( $namespaces as $n ) { $opt['ns' . $n] = 1; } - $opt['redirs'] = $this->searchRedirects; $stParams = array_merge( array( @@ -1129,7 +1105,7 @@ class SpecialSearch extends SpecialPage { return Xml::element( 'a', array( - 'href' => $this->getTitle()->getLocalURL( $stParams ), + 'href' => $this->getPageTitle()->getLocalURL( $stParams ), 'title' => $tooltip ), $label @@ -1139,33 +1115,35 @@ class SpecialSearch extends SpecialPage { /** * Check if query starts with image: prefix * - * @param string $term the string to check - * @return Boolean + * @param string $term The string to check + * @return bool */ protected function startsWithImage( $term ) { global $wgContLang; - $p = explode( ':', $term ); - if ( count( $p ) > 1 ) { - return $wgContLang->getNsIndex( $p[0] ) == NS_FILE; + $parts = explode( ':', $term ); + if ( count( $parts ) > 1 ) { + return $wgContLang->getNsIndex( $parts[0] ) == NS_FILE; } + return false; } /** * Check if query starts with all: prefix * - * @param string $term the string to check - * @return Boolean + * @param string $term The string to check + * @return bool */ protected function startsWithAll( $term ) { $allkeyword = $this->msg( 'searchall' )->inContentLanguage()->text(); - $p = explode( ':', $term ); - if ( count( $p ) > 1 ) { - return $p[0] == $allkeyword; + $parts = explode( ':', $term ); + if ( count( $parts ) > 1 ) { + return $parts[0] == $allkeyword; } + return false; } @@ -1179,17 +1157,34 @@ class SpecialSearch extends SpecialPage { $this->searchEngine = $this->searchEngineType ? SearchEngine::create( $this->searchEngineType ) : SearchEngine::create(); } + return $this->searchEngine; } + /** + * Current search profile. + * @return null|string + */ + function getProfile() { + return $this->profile; + } + + /** + * Current namespaces. + * @return array + */ + function getNamespaces() { + return $this->namespaces; + } + /** * Users of hook SpecialSearchSetupEngine can use this to * add more params to links to not lose selection when * user navigates search results. * @since 1.18 * - * @param $key - * @param $value + * @param string $key + * @param mixed $value */ public function setExtraParam( $key, $value ) { $this->extraParams[$key] = $value; diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php index 9b50875a..782d9a17 100644 --- a/includes/specials/SpecialShortpages.php +++ b/includes/specials/SpecialShortpages.php @@ -40,12 +40,15 @@ class ShortPagesPage extends QueryPage { function getQueryInfo() { return array( 'tables' => array( 'page' ), - 'fields' => array( 'namespace' => 'page_namespace', - 'title' => 'page_title', - 'value' => 'page_len' ), - 'conds' => array( 'page_namespace' => - MWNamespace::getContentNamespaces(), - 'page_is_redirect' => 0 ), + 'fields' => array( + 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_len' + ), + 'conds' => array( + 'page_namespace' => MWNamespace::getContentNamespaces(), + 'page_is_redirect' => 0 + ), 'options' => array( 'USE INDEX' => 'page_redirect_namespace_len' ) ); } @@ -55,7 +58,7 @@ class ShortPagesPage extends QueryPage { } /** - * @param $db DatabaseBase + * @param DatabaseBase $db * @param ResultWrapper $res */ function preprocessResults( $db, $res ) { @@ -111,8 +114,8 @@ class ShortPagesPage extends QueryPage { $size = $this->msg( 'nbytes' )->numParams( $result->value )->escaped(); return $exists - ? "${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]" - : "${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]"; + ? "${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]" + : "${hlinkInParentheses} {$dm}{$plink} {$dm}[{$size}]"; } protected function getGroupName() { diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php index 47c89d0d..eff06f46 100644 --- a/includes/specials/SpecialSpecialpages.php +++ b/includes/specials/SpecialSpecialpages.php @@ -49,8 +49,6 @@ class SpecialSpecialpages extends UnlistedSpecialPage { } private function getPageGroups() { - global $wgSortSpecialPages; - $pages = SpecialPageFactory::getUsablePages( $this->getUser() ); if ( !count( $pages ) ) { @@ -68,7 +66,7 @@ class SpecialSpecialpages extends UnlistedSpecialPage { $groups[$group] = array(); } $groups[$group][$page->getDescription()] = array( - $page->getTitle(), + $page->getPageTitle(), $page->isRestricted(), $page->isCached() ); @@ -76,10 +74,8 @@ class SpecialSpecialpages extends UnlistedSpecialPage { } /** Sort */ - if ( $wgSortSpecialPages ) { - foreach ( $groups as $group => $sortedPages ) { - ksort( $groups[$group] ); - } + foreach ( $groups as $group => $sortedPages ) { + ksort( $groups[$group] ); } /** Always move "other" to end */ @@ -103,9 +99,15 @@ class SpecialSpecialpages extends UnlistedSpecialPage { $middle = ceil( $total / 2 ); $count = 0; - $out->wrapWikiMsg( "

      $1

      \n", "specialpages-group-$group" ); + $out->wrapWikiMsg( + "

      $1

      \n", + "specialpages-group-$group" + ); $out->addHTML( - Html::openElement( 'table', array( 'style' => 'width:100%;', 'class' => 'mw-specialpages-table' ) ) . "\n" . + 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" @@ -124,7 +126,11 @@ class SpecialSpecialpages extends UnlistedSpecialPage { } $link = Linker::linkKnown( $title, htmlspecialchars( $desc ) ); - $out->addHTML( Html::rawElement( 'li', array( 'class' => implode( ' ', $pageClasses ) ), $link ) . "\n" ); + $out->addHTML( Html::rawElement( + 'li', + array( 'class' => implode( ' ', $pageClasses ) ), + $link + ) . "\n" ); # Split up the larger groups $count++; @@ -144,6 +150,7 @@ class SpecialSpecialpages extends UnlistedSpecialPage { } if ( $includesRestrictedPages || $includesCachedPages ) { + $out->wrapWikiMsg( "

      $1

      ", 'specialpages-note-top' ); $out->wrapWikiMsg( "
      \n$1\n
      ", 'specialpages-note' ); } } diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php index f26d1a60..f0e360e8 100644 --- a/includes/specials/SpecialStatistics.php +++ b/includes/specials/SpecialStatistics.php @@ -28,16 +28,18 @@ * @ingroup SpecialPage */ class SpecialStatistics extends SpecialPage { - private $views, $edits, $good, $images, $total, $users, - $activeUsers = 0; + $activeUsers = 0; public function __construct() { parent::__construct( 'Statistics' ); } public function execute( $par ) { - global $wgMemc, $wgDisableCounters, $wgMiserMode; + global $wgMemc; + + $disableCounters = $this->getConfig()->get( 'DisableCounters' ); + $miserMode = $this->getConfig()->get( 'MiserMode' ); $this->setHeaders(); $this->getOutput()->addModuleStyles( 'mediawiki.special' ); @@ -53,12 +55,12 @@ class SpecialStatistics extends SpecialPage { # Staticic - views $viewsStats = ''; - if ( !$wgDisableCounters ) { + if ( !$disableCounters ) { $viewsStats = $this->getViewsStats(); } # Set active user count - if ( !$wgMiserMode ) { + if ( !$miserMode ) { $key = wfMemcKey( 'sitestats', 'activeusers-updated' ); // Re-calculate the count if the last tally is old... if ( !$wgMemc->get( $key ) ) { @@ -84,7 +86,7 @@ class SpecialStatistics extends SpecialPage { $text .= $viewsStats; # Statistic - popular pages - if ( !$wgDisableCounters && !$wgMiserMode ) { + if ( !$disableCounters && !$miserMode ) { $text .= $this->getMostViewedPages(); } @@ -107,14 +109,16 @@ class SpecialStatistics extends SpecialPage { /** * Format a row - * @param $text String: description of the row - * @param $number Float: a statistical number - * @param $trExtraParams Array: params to table row, see Html::elememt - * @param $descMsg String: message key + * @param string $text Description of the row + * @param float $number A statistical number + * @param array $trExtraParams Params to table row, see Html::elememt + * @param string $descMsg Message key * @param array|string $descMsgParam Message parameters - * @return string table row in HTML format + * @return string Table row in HTML format */ - private function formatRow( $text, $number, $trExtraParams = array(), $descMsg = '', $descMsgParam = '' ) { + private function formatRow( $text, $number, $trExtraParams = array(), + $descMsg = '', $descMsgParam = '' + ) { if ( $descMsg ) { $msg = $this->msg( $descMsg, $descMsgParam ); if ( $msg->exists() ) { @@ -123,6 +127,7 @@ class SpecialStatistics extends SpecialPage { " $descriptionText" ); } } + return Html::rawElement( 'tr', $trExtraParams, Html::rawElement( 'td', array(), $text ) . Html::rawElement( 'td', array( 'class' => 'mw-statistics-numbers' ), $number ) @@ -139,55 +144,59 @@ class SpecialStatistics extends SpecialPage { 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' ) ) . + $this->msg( 'statistics-articles' )->parse() ), + $this->getLanguage()->formatNum( $this->good ), + array( 'class' => 'mw-statistics-articles' ) ) . $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( 'Listfiles' ), - $this->msg( 'statistics-files' )->parse() ), - $this->getLanguage()->formatNum( $this->images ), - array( 'class' => 'mw-statistics-files' ) ); + $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' ) ); } + private function getEditStats() { return Xml::openElement( 'tr' ) . 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 ), - array( 'class' => 'mw-statistics-edits' ) ) . - $this->formatRow( $this->msg( 'statistics-edits-average' )->parse(), - $this->getLanguage()->formatNum( sprintf( '%.2f', $this->total ? $this->edits / $this->total : 0 ) ), - array( 'class' => 'mw-statistics-edits-average' ) ); + $this->formatRow( $this->msg( 'statistics-edits' )->parse(), + $this->getLanguage()->formatNum( $this->edits ), + array( 'class' => 'mw-statistics-edits' ) + ) . + $this->formatRow( $this->msg( 'statistics-edits-average' )->parse(), + $this->getLanguage() + ->formatNum( sprintf( '%.2f', $this->total ? $this->edits / $this->total : 0 ) ), + array( 'class' => 'mw-statistics-edits-average' ) + ); } private function getUserStats() { - global $wgActiveUserDays; return Xml::openElement( 'tr' ) . 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 ), - array( 'class' => 'mw-statistics-users' ) ) . - $this->formatRow( $this->msg( 'statistics-users-active' )->parse() . ' ' . - Linker::linkKnown( - SpecialPage::getTitleFor( 'Activeusers' ), - $this->msg( 'listgrouprights-members' )->escaped() - ), - $this->getLanguage()->formatNum( $this->activeUsers ), - array( 'class' => 'mw-statistics-users-active' ), - 'statistics-users-active-desc', - $this->getLanguage()->formatNum( $wgActiveUserDays ) ); + $this->formatRow( $this->msg( 'statistics-users' )->parse(), + $this->getLanguage()->formatNum( $this->users ), + array( 'class' => 'mw-statistics-users' ) + ) . + $this->formatRow( $this->msg( 'statistics-users-active' )->parse() . ' ' . + Linker::linkKnown( + SpecialPage::getTitleFor( 'Activeusers' ), + $this->msg( 'listgrouprights-members' )->escaped() + ), + $this->getLanguage()->formatNum( $this->activeUsers ), + array( 'class' => 'mw-statistics-users-active' ), + 'statistics-users-active-desc', + $this->getLanguage()->formatNum( $this->getConfig()->get( 'ActiveUserDays' ) ) + ); } private function getGroupStats() { - global $wgGroupPermissions, $wgImplicitGroups; $text = ''; - foreach ( $wgGroupPermissions as $group => $permissions ) { + foreach ( $this->getConfig()->get( 'GroupPermissions' ) as $group => $permissions ) { # Skip generic * and implicit groups - if ( in_array( $group, $wgImplicitGroups ) || $group == '*' ) { + if ( in_array( $group, $this->getConfig()->get( 'ImplicitGroups' ) ) || $group == '*' ) { continue; } $groupname = htmlspecialchars( $group ); @@ -224,6 +233,7 @@ class SpecialStatistics extends SpecialPage { $this->getLanguage()->formatNum( $countUsers ), array( 'class' => 'statistics-group-' . Sanitizer::escapeClass( $group ) . $classZero ) ); } + return $text; } @@ -244,36 +254,43 @@ class SpecialStatistics extends SpecialPage { $text = ''; $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( - 'page', - array( - 'page_namespace', - 'page_title', - 'page_counter', - ), - array( - 'page_is_redirect' => 0, - 'page_counter > 0', - ), - __METHOD__, - array( - 'ORDER BY' => 'page_counter DESC', - 'LIMIT' => 10, - ) + 'page', + array( + 'page_namespace', + 'page_title', + 'page_counter', + ), + array( + 'page_is_redirect' => 0, + 'page_counter > 0', + ), + __METHOD__, + array( + 'ORDER BY' => 'page_counter DESC', + 'LIMIT' => 10, + ) + ); + + if ( $res->numRows() > 0 ) { + $text .= Xml::openElement( 'tr' ); + $text .= Xml::tags( + 'th', + array( 'colspan' => '2' ), + $this->msg( 'statistics-mostpopular' )->parse() ); - if ( $res->numRows() > 0 ) { - $text .= Xml::openElement( 'tr' ); - $text .= Xml::tags( 'th', array( 'colspan' => '2' ), $this->msg( 'statistics-mostpopular' )->parse() ); - $text .= Xml::closeElement( 'tr' ); - foreach ( $res as $row ) { - $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); - if ( $title instanceof Title ) { - $text .= $this->formatRow( Linker::link( $title ), - $this->getLanguage()->formatNum( $row->page_counter ) ); - - } + $text .= Xml::closeElement( 'tr' ); + + foreach ( $res as $row ) { + $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); + + if ( $title instanceof Title ) { + $text .= $this->formatRow( Linker::link( $title ), + $this->getLanguage()->formatNum( $row->page_counter ) ); } - $res->free(); } + $res->free(); + } + return $text; } @@ -301,7 +318,11 @@ class SpecialStatistics extends SpecialPage { $name = $this->msg( $key )->parse(); $number = htmlspecialchars( $value ); - $return .= $this->formatRow( $name, $this->getLanguage()->formatNum( $number ), array( 'class' => 'mw-statistics-hook', 'id' => 'mw-' . $key ) ); + $return .= $this->formatRow( + $name, + $this->getLanguage()->formatNum( $number ), + array( 'class' => 'mw-statistics-hook', 'id' => 'mw-' . $key ) + ); } } else { // Create the legacy header only once @@ -310,7 +331,8 @@ class SpecialStatistics extends SpecialPage { } // Recursively remap the legacy structure - $return .= $this->getOtherStats( array( 'statistics-header-hooks' => array( $header => $items ) ) ); + $return .= $this->getOtherStats( array( 'statistics-header-hooks' => + array( $header => $items ) ) ); } } diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php index 077e7cbc..b7627285 100644 --- a/includes/specials/SpecialTags.php +++ b/includes/specials/SpecialTags.php @@ -46,11 +46,11 @@ class SpecialTags extends SpecialPage { // 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-active-header' )->parse() ) . - Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->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-active-header' )->parse() ) . + Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) + ); // Used in #doTagRow() $this->definedTags = array_fill_keys( ChangeTags::listDefinedTags(), true ); diff --git a/includes/specials/SpecialTrackingCategories.php b/includes/specials/SpecialTrackingCategories.php new file mode 100644 index 00000000..552031f1 --- /dev/null +++ b/includes/specials/SpecialTrackingCategories.php @@ -0,0 +1,148 @@ +setHeaders(); + $this->outputHeader(); + $this->getOutput()->allowClickjacking(); + $this->getOutput()->addHTML( + Html::openElement( 'table', array( 'class' => 'mw-datatable', + 'id' => 'mw-trackingcategories-table' ) ) . "\n" . + " + " . + $this->msg( 'trackingcategories-msg' )->escaped() . " + + " . + $this->msg( 'trackingcategories-name' )->escaped() . + " + " . + $this->msg( 'trackingcategories-desc' )->escaped() . " + + " + ); + + foreach ( $this->getConfig()->get( 'TrackingCategories' ) as $catMsg ) { + /* + * Check if the tracking category varies by namespace + * Otherwise only pages in the current namespace will be displayed + * If it does vary, show pages considering all namespaces + */ + $msgObj = $this->msg( $catMsg )->inContentLanguage(); + $allMsgs = array(); + $catDesc = $catMsg . '-desc'; + $catMsgTitle = Title::makeTitleSafe( NS_MEDIAWIKI, $catMsg ); + if ( !$catMsgTitle ) { + continue; + } + $catMsgTitleText = Linker::link( + $catMsgTitle, + htmlspecialchars( $catMsg ) + ); + + // Match things like {{NAMESPACE}} and {{NAMESPACENUMBER}}. + // False positives are ok, this is just an efficiency shortcut + if ( strpos( $msgObj->plain(), '{{' ) !== false ) { + $ns = MWNamespace::getValidNamespaces(); + foreach ( $ns as $namesp ) { + $tempTitle = Title::makeTitleSafe( $namesp, $catMsg ); + if ( !$tempTitle ) { + continue; + } + $catName = $msgObj->title( $tempTitle )->text(); + # Allow tracking categories to be disabled by setting them to "-" + if ( $catName !== '-' ) { + $catTitle = Title::makeTitleSafe( NS_CATEGORY, $catName ); + if ( $catTitle ) { + $catTitleText = Linker::link( + $catTitle, + htmlspecialchars( $catName ) + ); + $allMsgs[] = $catTitleText; + } + } + } + } else { + $catName = $msgObj->text(); + # Allow tracking categories to be disabled by setting them to "-" + if ( $catName !== '-' ) { + $catTitle = Title::makeTitleSafe( NS_CATEGORY, $catName ); + if ( $catTitle ) { + $catTitleText = Linker::link( + $catTitle, + htmlspecialchars( $catName ) + ); + $allMsgs[] = $catTitleText; + } + } + } + + # Extra message, when no category was found + if ( !count( $allMsgs ) ) { + $allMsgs[] = $this->msg( 'trackingcategories-disabled' )->parse(); + } + + /* + * Show category description if it exists as a system message + * as category-name-desc + */ + $descMsg = $this->msg( $catDesc ); + if ( $descMsg->isBlank() ) { + $descMsg = $this->msg( 'trackingcategories-nodesc' ); + } + + $this->getOutput()->addHTML( + Html::openElement( 'tr' ) . + Html::openElement( 'td', array( 'class' => 'mw-trackingcategories-name' ) ) . + $this->getLanguage()->commaList( array_unique( $allMsgs ) ) . + Html::closeElement( 'td' ) . + Html::openElement( 'td', array( 'class' => 'mw-trackingcategories-msg' ) ) . + $catMsgTitleText . + Html::closeElement( 'td' ) . + Html::openElement( 'td', array( 'class' => 'mw-trackingcategories-desc' ) ) . + $descMsg->parse() . + Html::closeElement( 'td' ) . + Html::closeElement( 'tr' ) + ); + } + $this->getOutput()->addHTML( Html::closeElement( 'table' ) ); + } + + protected function getGroupName() { + return 'pages'; + } +} diff --git a/includes/specials/SpecialUnblock.php b/includes/specials/SpecialUnblock.php index ca93b6d1..244b8894 100644 --- a/includes/specials/SpecialUnblock.php +++ b/includes/specials/SpecialUnblock.php @@ -42,6 +42,11 @@ class SpecialUnblock extends SpecialPage { list( $this->target, $this->type ) = SpecialBlock::getTargetAndType( $par, $this->getRequest() ); $this->block = Block::newFromTarget( $this->target ); + if ( $this->target instanceof User ) { + # Set the 'relevant user' in the skin, so it displays links like Contributions, + # User logs, UserRights, etc. + $this->getSkin()->setRelevantUser( $this->target ); + } $this->setHeaders(); $this->outputHeader(); @@ -58,8 +63,10 @@ class SpecialUnblock extends SpecialPage { if ( $form->show() ) { switch ( $this->type ) { - case Block::TYPE_USER: case Block::TYPE_IP: + $out->addWikiMsg( 'unblocked-ip', wfEscapeWikiText( $this->target ) ); + break; + case Block::TYPE_USER: $out->addWikiMsg( 'unblocked', wfEscapeWikiText( $this->target ) ); break; case Block::TYPE_RANGE: @@ -77,14 +84,14 @@ class SpecialUnblock extends SpecialPage { $fields = array( 'Target' => array( 'type' => 'text', - 'label-message' => 'ipadressorusername', - 'tabindex' => '1', + 'label-message' => 'ipaddressorusername', + 'autofocus' => true, 'size' => '45', 'required' => true, ), 'Name' => array( 'type' => 'info', - 'label-message' => 'ipadressorusername', + 'label-message' => 'ipaddressorusername', ), 'Reason' => array( 'type' => 'text', @@ -102,13 +109,18 @@ class SpecialUnblock extends SpecialPage { if ( $type == Block::TYPE_AUTO && $this->type == Block::TYPE_IP ) { $fields['Target']['default'] = $this->target; unset( $fields['Name'] ); - } else { $fields['Target']['default'] = $target; $fields['Target']['type'] = 'hidden'; switch ( $type ) { - case Block::TYPE_USER: case Block::TYPE_IP: + $fields['Name']['default'] = Linker::linkKnown( + SpecialPage::getTitleFor( 'Contributions', $target->getName() ), + $target->getName() + ); + $fields['Name']['raw'] = true; + break; + case Block::TYPE_USER: $fields['Name']['default'] = Linker::link( $target->getUserPage(), $target->getName() @@ -127,12 +139,15 @@ class SpecialUnblock extends SpecialPage { $fields['Target']['default'] = "#{$this->target}"; break; } + // target is hidden, so the reason is the first element + $fields['Target']['autofocus'] = false; + $fields['Reason']['autofocus'] = true; } - } else { $fields['Target']['default'] = $this->target; unset( $fields['Name'] ); } + return $fields; } @@ -140,7 +155,7 @@ class SpecialUnblock extends SpecialPage { * Submit callback for an HTMLForm object * @param array $data * @param HTMLForm $form - * @return Array( Array(message key, parameters) + * @return array|bool Array(message key, parameters) */ public static function processUIUnblock( array $data, HTMLForm $form ) { return self::processUnblock( $data, $form->getContext() ); @@ -149,10 +164,10 @@ class SpecialUnblock extends SpecialPage { /** * Process the form * - * @param $data Array - * @param $context IContextSource + * @param array $data + * @param IContextSource $context * @throws ErrorPageError - * @return Array( Array(message key, parameters) ) on failure, True on success + * @return array|bool Array(message key, parameters) on failure, True on success */ public static function processUnblock( array $data, IContextSource $context ) { $performer = $context->getUser(); @@ -176,6 +191,7 @@ class SpecialUnblock extends SpecialPage { list( $target, $type ) = SpecialBlock::getTargetAndType( $target ); if ( $block->getType() == Block::TYPE_RANGE && $type == Block::TYPE_IP ) { $range = $block->getTarget(); + return array( array( 'ipb_blocked_as_range', $target, $range ) ); } diff --git a/includes/specials/SpecialUncategorizedimages.php b/includes/specials/SpecialUncategorizedimages.php index 3bfcedec..9060f53a 100644 --- a/includes/specials/SpecialUncategorizedimages.php +++ b/includes/specials/SpecialUncategorizedimages.php @@ -26,10 +26,9 @@ * Special page lists images which haven't been categorised * * @ingroup SpecialPage + * @todo FIXME: Use an instance of UncategorizedPagesPage or something */ -// @todo FIXME: Use an instance of UncategorizedPagesPage or something class UncategorizedImagesPage extends ImageQueryPage { - function __construct( $name = 'Uncategorizedimages' ) { parent::__construct( $name ); } @@ -50,13 +49,13 @@ class UncategorizedImagesPage extends ImageQueryPage { return array( 'tables' => array( 'page', 'categorylinks' ), 'fields' => array( 'namespace' => 'page_namespace', - 'title' => 'page_title', - 'value' => 'page_title' ), + 'title' => 'page_title', + 'value' => 'page_title' ), 'conds' => array( 'cl_from IS NULL', - 'page_namespace' => NS_FILE, - 'page_is_redirect' => 0 ), + 'page_namespace' => NS_FILE, + 'page_is_redirect' => 0 ), 'join_conds' => array( 'categorylinks' => array( - 'LEFT JOIN', 'cl_from=page_id' ) ) + 'LEFT JOIN', 'cl_from=page_id' ) ) ); } diff --git a/includes/specials/SpecialUncategorizedpages.php b/includes/specials/SpecialUncategorizedpages.php index 8bc9e489..8251d5b3 100644 --- a/includes/specials/SpecialUncategorizedpages.php +++ b/includes/specials/SpecialUncategorizedpages.php @@ -25,8 +25,8 @@ * A special page looking for page without any category. * * @ingroup SpecialPage + * @todo FIXME: Make $requestedNamespace selectable, unify all subclasses into one */ -// @todo FIXME: Make $requestedNamespace selectable, unify all subclasses into one class UncategorizedPagesPage extends PageQueryPage { protected $requestedNamespace = false; @@ -49,16 +49,23 @@ class UncategorizedPagesPage extends PageQueryPage { function getQueryInfo() { return array( 'tables' => array( 'page', 'categorylinks' ), - 'fields' => array( 'namespace' => 'page_namespace', - 'title' => 'page_title', - 'value' => 'page_title' ), + 'fields' => array( + 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' + ), // default for page_namespace is all content namespaces (if requestedNamespace is false) // otherwise, page_namespace is requestedNamespace - 'conds' => array( 'cl_from IS NULL', - 'page_namespace' => ( $this->requestedNamespace !== false ? $this->requestedNamespace : MWNamespace::getContentNamespaces() ), - 'page_is_redirect' => 0 ), - 'join_conds' => array( 'categorylinks' => array( - 'LEFT JOIN', 'cl_from = page_id' ) ) + 'conds' => array( + 'cl_from IS NULL', + 'page_namespace' => $this->requestedNamespace !== false + ? $this->requestedNamespace + : MWNamespace::getContentNamespaces(), + 'page_is_redirect' => 0 + ), + 'join_conds' => array( + 'categorylinks' => array( 'LEFT JOIN', 'cl_from = page_id' ) + ) ); } @@ -68,6 +75,7 @@ class UncategorizedPagesPage extends PageQueryPage { if ( $this->requestedNamespace === false && count( MWNamespace::getContentNamespaces() ) > 1 ) { return array( 'page_namespace', 'page_title' ); } + return array( 'page_title' ); } diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index d4aed113..c3e871b8 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -27,26 +27,28 @@ * @ingroup SpecialPage */ class PageArchive { - /** - * @var Title - */ + /** @var Title */ protected $title; - /** - * @var Status - */ + /** @var Status */ protected $fileStatus; - /** - * @var Status - */ + /** @var Status */ protected $revisionStatus; - function __construct( $title ) { + /** @var Config */ + protected $config; + + function __construct( $title, Config $config = null ) { if ( is_null( $title ) ) { throw new MWException( __METHOD__ . ' given a null title.' ); } $this->title = $title; + if ( $config === null ) { + wfDebug( __METHOD__ . ' did not have a Config object passed to it' ); + $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); + } + $this->config = $config; } /** @@ -58,6 +60,7 @@ class PageArchive { */ public static function listAllPages() { $dbr = wfGetDB( DB_SLAVE ); + return self::listPages( $dbr, '' ); } @@ -96,7 +99,7 @@ class PageArchive { * @return bool|ResultWrapper */ protected static function listPages( $dbr, $condition ) { - return $dbr->resultObject( $dbr->select( + return $dbr->select( array( 'archive' ), array( 'ar_namespace', @@ -110,7 +113,7 @@ class PageArchive { 'ORDER BY' => array( 'ar_namespace', 'ar_title' ), 'LIMIT' => 100, ) - ) ); + ); } /** @@ -120,28 +123,42 @@ class PageArchive { * @return ResultWrapper */ function listRevisions() { - global $wgContentHandlerUseDB; - $dbr = wfGetDB( DB_SLAVE ); + $tables = array( 'archive' ); + $fields = array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1', ); - if ( $wgContentHandlerUseDB ) { + if ( $this->config->get( 'ContentHandlerUseDB' ) ) { $fields[] = 'ar_content_format'; $fields[] = 'ar_content_model'; } - $res = $dbr->select( 'archive', + $conds = array( 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey() ); + + $options = array( 'ORDER BY' => 'ar_timestamp DESC' ); + + $join_conds = array(); + + ChangeTags::modifyDisplayQuery( + $tables, $fields, - array( 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey() ), - __METHOD__, - array( 'ORDER BY' => 'ar_timestamp DESC' ) ); + $conds, + $join_conds, + $options + ); - return $dbr->resultObject( $res ); + return $dbr->select( $tables, + $fields, + $conds, + __METHOD__, + $options, + $join_conds + ); } /** @@ -158,15 +175,13 @@ class PageArchive { } $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( + return $dbr->select( 'filearchive', ArchivedFile::selectFields(), array( 'fa_name' => $this->title->getDBkey() ), __METHOD__, array( 'ORDER BY' => 'fa_timestamp DESC' ) ); - - return $dbr->resultObject( $res ); } /** @@ -177,8 +192,6 @@ class PageArchive { * @return Revision|null */ function getRevision( $timestamp ) { - global $wgContentHandlerUseDB; - $dbr = wfGetDB( DB_SLAVE ); $fields = array( @@ -196,7 +209,7 @@ class PageArchive { 'ar_sha1', ); - if ( $wgContentHandlerUseDB ) { + if ( $this->config->get( 'ContentHandlerUseDB' ) ) { $fields[] = 'ar_content_format'; $fields[] = 'ar_content_model'; } @@ -318,7 +331,7 @@ class PageArchive { /** * Quick check if any archived revisions are present for the page. * - * @return boolean + * @return bool */ function isDeleted() { $dbr = wfGetDB( DB_SLAVE ); @@ -336,16 +349,18 @@ class PageArchive { * Once restored, the items will be removed from the archive tables. * The deletion log will be updated with an undeletion notice. * - * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete. + * @param array $timestamps Pass an empty array to restore all revisions, + * otherwise list the ones to undelete. * @param string $comment * @param array $fileVersions * @param bool $unsuppress * @param User $user User performing the action, or null to use $wgUser - * - * @return array(number of file revisions restored, number of image revisions restored, log message) - * on success, false on failure + * @return array(number of file revisions restored, number of image revisions + * restored, log message) on success, false on failure. */ - function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false, User $user = null ) { + function undelete( $timestamps, $comment = '', $fileVersions = array(), + $unsuppress = false, User $user = null + ) { // If both the set of text revisions and file revisions are empty, // restore everything. Otherwise, just restore the requested items. $restoreAll = empty( $timestamps ) && empty( $fileVersions ); @@ -388,6 +403,7 @@ class PageArchive { ->inContentLanguage()->text(); } else { wfDebug( "Undelete: nothing undeleted...\n" ); + return false; } @@ -418,15 +434,14 @@ class PageArchive { * to the cur/old tables. If the page currently exists, all revisions will * be stuffed into old, otherwise the most recent will go into cur. * - * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete. + * @param array $timestamps Pass an empty array to restore all revisions, + * otherwise list the ones to undelete. * @param bool $unsuppress Remove all ar_deleted/fa_deleted restrictions of seletected revs * @param string $comment * @throws ReadOnlyError - * @return Status Object containing the number of revisions restored on success + * @return Status Status object containing the number of revisions restored on success */ private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) { - global $wgContentHandlerUseDB; - if ( wfReadOnly() ) { throw new ReadOnlyError(); } @@ -475,15 +490,12 @@ class PageArchive { $previousTimestamp = 0; } - if ( $restoreAll ) { - $oldones = '1 = 1'; # All revisions... - } else { - $oldts = implode( ',', - array_map( array( &$dbw, 'addQuotes' ), - array_map( array( &$dbw, 'timestamp' ), - $timestamps ) ) ); - - $oldones = "ar_timestamp IN ( {$oldts} )"; + $oldWhere = array( + 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey(), + ); + if ( !$restoreAll ) { + $oldWhere['ar_timestamp'] = array_map( array( &$dbw, 'timestamp' ), $timestamps ); } $fields = array( @@ -502,7 +514,7 @@ class PageArchive { 'ar_sha1' ); - if ( $wgContentHandlerUseDB ) { + if ( $this->config->get( 'ContentHandlerUseDB' ) ) { $fields[] = 'ar_content_format'; $fields[] = 'ar_content_model'; } @@ -512,27 +524,25 @@ class PageArchive { */ $result = $dbw->select( 'archive', $fields, - /* WHERE */ array( - 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey(), - $oldones ), + $oldWhere, __METHOD__, /* options */ array( 'ORDER BY' => 'ar_timestamp' ) ); - $ret = $dbw->resultObject( $result ); - $rev_count = $dbw->numRows( $result ); + $rev_count = $result->numRows(); if ( !$rev_count ) { wfDebug( __METHOD__ . ": no revisions to restore\n" ); $status = Status::newGood( 0 ); $status->warning( "undelete-no-results" ); + return $status; } - $ret->seek( $rev_count - 1 ); // move to last - $row = $ret->fetchObject(); // get newest archived rev - $ret->seek( 0 ); // move back + $result->seek( $rev_count - 1 ); // move to last + $row = $result->fetchObject(); // get newest archived rev + $oldPageId = (int)$row->ar_page_id; // pass this to ArticleUndelete hook + $result->seek( 0 ); // move back // grab the content to check consistency with global state before restoring the page. $revision = Revision::newFromArchiveRow( $row, @@ -574,7 +584,7 @@ class PageArchive { $revision = null; $restored = 0; - foreach ( $ret as $row ) { + foreach ( $result as $row ) { // Check for key dupes due to shitty archive integrity. if ( $row->ar_rev_id ) { $exists = $dbw->selectField( 'revision', '1', @@ -599,10 +609,7 @@ class PageArchive { } # Now that it's safely stored, take it out of the archive $dbw->delete( 'archive', - /* WHERE */ array( - 'ar_namespace' => $this->title->getNamespace(), - 'ar_title' => $this->title->getDBkey(), - $oldones ), + $oldWhere, __METHOD__ ); // Was anything restored at all? @@ -617,10 +624,14 @@ class PageArchive { if ( $created || $wasnew ) { // Update site stats, link tables, etc $user = User::newFromName( $revision->getRawUserText(), false ); - $article->doEditUpdates( $revision, $user, array( 'created' => $created, 'oldcountable' => $oldcountable ) ); + $article->doEditUpdates( + $revision, + $user, + array( 'created' => $created, 'oldcountable' => $oldcountable ) + ); } - wfRunHooks( 'ArticleUndelete', array( &$this->title, $created, $comment ) ); + wfRunHooks( 'ArticleUndelete', array( &$this->title, $created, $comment, $oldPageId ) ); if ( $this->title->getNamespace() == NS_FILE ) { $update = new HTMLCacheUpdate( $this->title, 'imagelinks' ); @@ -652,13 +663,20 @@ class PageArchive { * @ingroup SpecialPage */ class SpecialUndelete extends SpecialPage { - var $mAction, $mTarget, $mTimestamp, $mRestore, $mInvert, $mFilename; - var $mTargetTimestamp, $mAllowed, $mCanView, $mComment, $mToken; - - /** - * @var Title - */ - var $mTargetObj; + private $mAction; + private $mTarget; + private $mTimestamp; + private $mRestore; + private $mInvert; + private $mFilename; + private $mTargetTimestamp; + private $mAllowed; + private $mCanView; + private $mComment; + private $mToken; + + /** @var Title */ + private $mTargetObj; function __construct() { parent::__construct( 'Undelete', 'deletedhistory' ); @@ -697,10 +715,10 @@ class SpecialUndelete extends SpecialPage { $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $user->isAllowed( 'suppressrevision' ); $this->mToken = $request->getVal( 'token' ); - if ( $user->isAllowed( 'undelete' ) && !$user->isBlocked() ) { + if ( $this->isAllowed( 'undelete' ) && !$user->isBlocked() ) { $this->mAllowed = true; // user can restore $this->mCanView = true; // user can view content - } elseif ( $user->isAllowed( 'deletedtext' ) ) { + } elseif ( $this->isAllowed( 'deletedtext' ) ) { $this->mAllowed = false; // user cannot restore $this->mCanView = true; // user can view content $this->mRestore = false; @@ -729,14 +747,35 @@ class SpecialUndelete extends SpecialPage { } } + /** + * Checks whether a user is allowed the permission for the + * specific title if one is set. + * + * @param string $permission + * @param User $user + * @return bool + */ + private function isAllowed( $permission, User $user = null ) { + $user = $user ? : $this->getUser(); + if ( $this->mTargetObj !== null ) { + return $this->mTargetObj->userCan( $permission, $user ); + } else { + return $user->isAllowed( $permission ); + } + } + + function userCanExecute( User $user ) { + return $this->isAllowed( $this->mRestriction, $user ); + } + function execute( $par ) { - $this->checkPermissions(); $user = $this->getUser(); $this->setHeaders(); $this->outputHeader(); $this->loadRequest( $par ); + $this->checkPermissions(); // Needs to be after mTargetObj is set $out = $this->getOutput(); @@ -747,6 +786,7 @@ class SpecialUndelete extends SpecialPage { if ( $user->isAllowed( 'browsearchive' ) ) { $this->showSearchForm(); } + return; } @@ -784,14 +824,12 @@ class SpecialUndelete extends SpecialPage { } function showSearchForm() { - global $wgScript; - $out = $this->getOutput(); $out->setPageTitle( $this->msg( 'undelete-search-title' ) ); $out->addHTML( - Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . + Xml::openElement( 'form', array( 'method' => 'get', 'action' => wfScript() ) ) . Xml::fieldset( $this->msg( 'undelete-search-box' )->text() ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . + Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . Html::rawElement( 'label', array( 'for' => 'prefix' ), @@ -826,12 +864,13 @@ class SpecialUndelete extends SpecialPage { if ( $result->numRows() == 0 ) { $out->addWikiMsg( 'undelete-no-results' ); + return false; } $out->addWikiMsg( 'undeletepagetext', $this->getLanguage()->formatNum( $result->numRows() ) ); - $undelete = $this->getTitle(); + $undelete = $this->getPageTitle(); $out->addHTML( "
        \n" ); foreach ( $result as $row ) { $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); @@ -868,7 +907,7 @@ class SpecialUndelete extends SpecialPage { return; } - $archive = new PageArchive( $this->mTargetObj ); + $archive = new PageArchive( $this->mTargetObj, $this->getConfig() ); if ( !wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ) ) { return; } @@ -879,6 +918,7 @@ class SpecialUndelete extends SpecialPage { if ( !$rev ) { $out->addWikiMsg( 'undeleterevision-missing' ); + return; } @@ -886,14 +926,17 @@ class SpecialUndelete extends SpecialPage { if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) { $out->wrapWikiMsg( "\n", - 'rev-deleted-text-permission' + $rev->isDeleted( Revision::DELETED_RESTRICTED ) ? + 'rev-suppressed-text-permission' : 'rev-deleted-text-permission' ); + return; } $out->wrapWikiMsg( "\n", - 'rev-deleted-text-view' + $rev->isDeleted( Revision::DELETED_RESTRICTED ) ? + 'rev-suppressed-text-view' : 'rev-deleted-text-view' ); $out->addHTML( '
        ' ); // and we are allowed to see... @@ -914,7 +957,7 @@ class SpecialUndelete extends SpecialPage { } $link = Linker::linkKnown( - $this->getTitle( $this->mTargetObj->getPrefixedDBkey() ), + $this->getPageTitle( $this->mTargetObj->getPrefixedDBkey() ), htmlspecialchars( $this->mTargetObj->getPrefixedText() ) ); @@ -997,7 +1040,7 @@ class SpecialUndelete extends SpecialPage { 'style' => 'clear: both' ) ) . Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ) ) ) . + 'action' => $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ) ) ) . Xml::element( 'input', array( 'type' => 'hidden', 'name' => 'target', @@ -1032,26 +1075,19 @@ class SpecialUndelete extends SpecialPage { $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext ); $diffEngine->showDiffStyle(); - $this->getOutput()->addHTML( "
        " . - "" . - "" . - "" . - "" . - "" . - "" . - "\n" . - "\n" . - "" . - $diffEngine->generateContentDiffBody( - $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ), - $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) ) . - "
        " . - $this->diffHeader( $previousRev, 'o' ) . - "" . - $this->diffHeader( $currentRev, 'n' ) . - "
        " . - "
        \n" + + $formattedDiff = $diffEngine->generateContentDiffBody( + $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ), + $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) ); + + $formattedDiff = $diffEngine->addHeader( + $formattedDiff, + $this->diffHeader( $previousRev, 'o' ), + $this->diffHeader( $currentRev, 'n' ) + ); + + $this->getOutput()->addHTML( "
        $formattedDiff
        \n" ); } /** @@ -1063,7 +1099,7 @@ class SpecialUndelete extends SpecialPage { $isDeleted = !( $rev->getId() && $rev->getTitle() ); if ( $isDeleted ) { /// @todo FIXME: $rev->getTitle() is null for deleted revs...? - $targetPage = $this->getTitle(); + $targetPage = $this->getPageTitle(); $targetQuery = array( 'target' => $this->mTargetObj->getPrefixedText(), 'timestamp' => wfTimestamp( TS_MW, $rev->getTimestamp() ) @@ -1083,6 +1119,18 @@ class SpecialUndelete extends SpecialPage { $rdel = " $rdel"; } + $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : ''; + + $tags = wfGetDB( DB_SLAVE )->selectField( + 'tag_summary', + 'ts_tags', + array( 'ts_rev_id' => $rev->getId() ), + __METHOD__ + ); + $tagSummary = ChangeTags::formatSummaryRow( $tags, 'deleteddiff' ); + + // FIXME This is reimplementing DifferenceEngine#getRevisionHeader + // and partially #showDiffPage, but worse return '
        ' . Linker::link( $targetPage, @@ -1100,12 +1148,16 @@ class SpecialUndelete extends SpecialPage { Linker::revUserTools( $rev ) . '
        ' . '
        ' . '
        ' . - Linker::revComment( $rev ) . $rdel . '
        ' . + $minor . Linker::revComment( $rev ) . $rdel . '
        ' . + '
        ' . + '
        ' . + $tagSummary[0] . '
        ' . '
        '; } /** * Show a form confirming whether a tokenless user really wants to see a file + * @param string $key */ private function showFileConfirmationForm( $key ) { $out = $this->getOutput(); @@ -1119,7 +1171,7 @@ class SpecialUndelete extends SpecialPage { $out->addHTML( Xml::openElement( 'form', array( 'method' => 'POST', - 'action' => $this->getTitle()->getLocalURL( array( + 'action' => $this->getPageTitle()->getLocalURL( array( 'target' => $this->mTarget, 'file' => $key, 'token' => $user->getEditToken( $key ), @@ -1133,6 +1185,7 @@ class SpecialUndelete extends SpecialPage { /** * Show a deleted file version requested by the visitor. + * @param string $key */ private function showFile( $key ) { $this->getOutput()->disable(); @@ -1161,7 +1214,7 @@ class SpecialUndelete extends SpecialPage { array( 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) ) ); - $archive = new PageArchive( $this->mTargetObj ); + $archive = new PageArchive( $this->mTargetObj, $this->getConfig() ); wfRunHooks( 'UndeleteForm::showHistory', array( &$archive, $this->mTargetObj ) ); /* $text = $archive->getLastRevisionText(); @@ -1207,7 +1260,7 @@ class SpecialUndelete extends SpecialPage { } if ( $this->mAllowed ) { - $action = $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ); + $action = $this->getPageTitle()->getLocalURL( array( 'action' => 'submit' ) ); # Start the form here $top = Xml::openElement( 'form', @@ -1243,32 +1296,42 @@ class SpecialUndelete extends SpecialPage { $unsuppressBox = ''; } - $table = - Xml::fieldset( $this->msg( 'undelete-fieldset-title' )->text() ) . - Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) . - " - " . - $this->msg( 'undeleteextrahelp' )->parseAsBlock() . - " - - - " . - Xml::label( $this->msg( 'undeletecomment' )->text(), 'wpComment' ) . - " - " . - Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment', 'autofocus' => true ) ) . - " - - -   - " . - Xml::submitButton( $this->msg( 'undeletebtn' )->text(), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) . ' ' . - Xml::submitButton( $this->msg( 'undeleteinvert' )->text(), array( 'name' => 'invert', 'id' => 'mw-undelete-invert' ) ) . - " - " . - $unsuppressBox . - Xml::closeElement( 'table' ) . - Xml::closeElement( 'fieldset' ); + $table = Xml::fieldset( $this->msg( 'undelete-fieldset-title' )->text() ) . + Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) . + " + " . + $this->msg( 'undeleteextrahelp' )->parseAsBlock() . + " + + + " . + Xml::label( $this->msg( 'undeletecomment' )->text(), 'wpComment' ) . + " + " . + Xml::input( + 'wpComment', + 50, + $this->mComment, + array( 'id' => 'wpComment', 'autofocus' => true ) + ) . + " + + +   + " . + Xml::submitButton( + $this->msg( 'undeletebtn' )->text(), + array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) + ) . ' ' . + Xml::submitButton( + $this->msg( 'undeleteinvert' )->text(), + array( 'name' => 'invert', 'id' => 'mw-undelete-invert' ) + ) . + " + " . + $unsuppressBox . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ); $out->addHTML( $table ); } @@ -1338,7 +1401,7 @@ class SpecialUndelete extends SpecialPage { // Build page & diff links... $user = $this->getUser(); if ( $this->mCanView ) { - $titleObj = $this->getTitle(); + $titleObj = $this->getPageTitle(); # Last link if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { $pageLink = htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ); @@ -1367,6 +1430,9 @@ class SpecialUndelete extends SpecialPage { // User links $userLink = Linker::revUserTools( $rev ); + // Minor edit + $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : ''; + // Revision text size $size = $row->ar_len; if ( !is_null( $size ) ) { @@ -1376,14 +1442,31 @@ class SpecialUndelete extends SpecialPage { // Edit summary $comment = Linker::revComment( $rev ); + // Tags + $attribs = array(); + list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'deletedhistory' ); + if ( $classes ) { + $attribs['class'] = implode( ' ', $classes ); + } + // Revision delete links $revdlink = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj ); - $revisionRow = $this->msg( 'undelete-revisionrow' ) - ->rawParams( $checkBox, $revdlink, $last, $pageLink, $userLink, $revTextSize, $comment ) + $revisionRow = $this->msg( 'undelete-revision-row' ) + ->rawParams( + $checkBox, + $revdlink, + $last, + $pageLink, + $userLink, + $minor, + $revTextSize, + $comment, + $tagSummary + ) ->escaped(); - return "
      • $revisionRow
      • "; + return Xml::tags( 'li', $attribs, $revisionRow ) . "\n"; } private function formatFileRow( $row ) { @@ -1391,12 +1474,14 @@ class SpecialUndelete extends SpecialPage { $ts = wfTimestamp( TS_MW, $row->fa_timestamp ); $user = $this->getUser(); - if ( $this->mAllowed && $row->fa_storage_key ) { - $checkBox = Xml::check( 'fileid' . $row->fa_id ); + $checkBox = ''; + if ( $this->mCanView && $row->fa_storage_key ) { + if ( $this->mAllowed ) { + $checkBox = Xml::check( 'fileid' . $row->fa_id ); + } $key = urlencode( $row->fa_storage_key ); - $pageLink = $this->getFileLink( $file, $this->getTitle(), $ts, $key ); + $pageLink = $this->getFileLink( $file, $this->getPageTitle(), $ts, $key ); } else { - $checkBox = ''; $pageLink = $this->getLanguage()->userTimeAndDate( $ts, $user ); } $userLink = $this->getFileUser( $file ); @@ -1408,8 +1493,8 @@ class SpecialUndelete extends SpecialPage { $comment = $this->getFileComment( $file ); // Add show/hide deletion links if available - $canHide = $user->isAllowed( 'deleterevision' ); - if ( $canHide || ( $file->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) { + $canHide = $this->isAllowed( 'deleterevision' ); + if ( $canHide || ( $file->getVisibility() && $this->isAllowed( 'deletedhistory' ) ) ) { if ( !$file->userCan( File::DELETED_RESTRICTED, $user ) ) { // Revision was hidden from sysops $revdlink = Linker::revDeleteLinkDisabled( $canHide ); @@ -1468,7 +1553,7 @@ class SpecialUndelete extends SpecialPage { * @param File|ArchivedFile $file * @param Title $titleObj * @param string $ts A timestamp - * @param string $key a storage key + * @param string $key A storage key * * @return string HTML fragment */ @@ -1543,9 +1628,7 @@ class SpecialUndelete extends SpecialPage { } function undelete() { - global $wgUploadMaintenance; - - if ( $wgUploadMaintenance && $this->mTargetObj->getNamespace() == NS_FILE ) { + if ( $this->getConfig()->get( 'UploadMaintenance' ) && $this->mTargetObj->getNamespace() == NS_FILE ) { throw new ErrorPageError( 'undelete-error', 'filedelete-maintenance' ); } @@ -1554,7 +1637,7 @@ class SpecialUndelete extends SpecialPage { } $out = $this->getOutput(); - $archive = new PageArchive( $this->mTargetObj ); + $archive = new PageArchive( $this->mTargetObj, $this->getConfig() ); wfRunHooks( 'UndeleteForm::undelete', array( &$archive, $this->mTargetObj ) ); $ok = $archive->undelete( $this->mTargetTimestamp, @@ -1580,13 +1663,23 @@ class SpecialUndelete extends SpecialPage { // Show revision undeletion warnings and errors $status = $archive->getRevisionStatus(); if ( $status && !$status->isGood() ) { - $out->addWikiText( '
        ' . $status->getWikiText( 'cannotundelete', 'cannotundelete' ) . '
        ' ); + $out->addWikiText( '
        ' . + $status->getWikiText( + 'cannotundelete', + 'cannotundelete' + ) . '
        ' + ); } // Show file undeletion warnings and errors $status = $archive->getFileStatus(); if ( $status && !$status->isGood() ) { - $out->addWikiText( '
        ' . $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) . '
        ' ); + $out->addWikiText( '
        ' . + $status->getWikiText( + 'undelete-error-short', + 'undelete-error-long' + ) . '
        ' + ); } } diff --git a/includes/specials/SpecialUnlockdb.php b/includes/specials/SpecialUnlockdb.php index 35141d80..a8b97d78 100644 --- a/includes/specials/SpecialUnlockdb.php +++ b/includes/specials/SpecialUnlockdb.php @@ -37,11 +37,9 @@ class SpecialUnlockdb extends FormSpecialPage { } public function checkExecutePermissions( User $user ) { - global $wgReadOnlyFile; - parent::checkExecutePermissions( $user ); # If the lock file isn't writable, we can do sweet bugger all - if ( !file_exists( $wgReadOnlyFile ) ) { + if ( !file_exists( $this->getConfig()->get( 'ReadOnlyFile' ) ) ) { throw new ErrorPageError( 'lockdb', 'databasenotlocked' ); } } @@ -62,20 +60,19 @@ class SpecialUnlockdb extends FormSpecialPage { } public function onSubmit( array $data ) { - global $wgReadOnlyFile; - if ( !$data['Confirm'] ) { return Status::newFatal( 'locknoconfirm' ); } + $readOnlyFile = $this->getConfig()->get( 'ReadOnlyFile' ); wfSuppressWarnings(); - $res = unlink( $wgReadOnlyFile ); + $res = unlink( $readOnlyFile ); wfRestoreWarnings(); if ( $res ) { return Status::newGood(); } else { - return Status::newFatal( 'filedeleteerror', $wgReadOnlyFile ); + return Status::newFatal( 'filedeleteerror', $readOnlyFile ); } } diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php index b686a5b5..713823bb 100644 --- a/includes/specials/SpecialUnusedcategories.php +++ b/includes/specials/SpecialUnusedcategories.php @@ -25,15 +25,14 @@ * @ingroup SpecialPage */ class UnusedCategoriesPage extends QueryPage { + function __construct( $name = 'Unusedcategories' ) { + parent::__construct( $name ); + } function isExpensive() { return true; } - function __construct( $name = 'Unusedcategories' ) { - parent::__construct( $name ); - } - function getPageHeader() { return $this->msg( 'unusedcategoriestext' )->parseAsBlock(); } @@ -41,14 +40,17 @@ class UnusedCategoriesPage extends QueryPage { function getQueryInfo() { return array( 'tables' => array( 'page', 'categorylinks' ), - 'fields' => array( 'namespace' => 'page_namespace', - 'title' => 'page_title', - 'value' => 'page_title' ), - 'conds' => array( 'cl_from IS NULL', - 'page_namespace' => NS_CATEGORY, - 'page_is_redirect' => 0 ), - 'join_conds' => array( 'categorylinks' => array( - 'LEFT JOIN', 'cl_to = page_title' ) ) + 'fields' => array( + 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' + ), + 'conds' => array( + 'cl_from IS NULL', + 'page_namespace' => NS_CATEGORY, + 'page_is_redirect' => 0 + ), + 'join_conds' => array( 'categorylinks' => array( 'LEFT JOIN', 'cl_to = page_title' ) ) ); } @@ -67,6 +69,7 @@ class UnusedCategoriesPage extends QueryPage { */ function formatResult( $skin, $result ) { $title = Title::makeTitle( NS_CATEGORY, $result->title ); + return Linker::link( $title, htmlspecialchars( $title->getText() ) ); } diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php index d332db75..36ec09ec 100644 --- a/includes/specials/SpecialUnusedimages.php +++ b/includes/specials/SpecialUnusedimages.php @@ -44,31 +44,32 @@ class UnusedimagesPage extends ImageQueryPage { } function getQueryInfo() { - global $wgCountCategorizedImagesAsUsed; $retval = array( 'tables' => array( 'image', 'imagelinks' ), - 'fields' => array( 'namespace' => NS_FILE, - 'title' => 'img_name', - 'value' => 'img_timestamp', - 'img_user', 'img_user_text', - 'img_description' ), + 'fields' => array( + 'namespace' => NS_FILE, + 'title' => 'img_name', + 'value' => 'img_timestamp', + 'img_user', 'img_user_text', + 'img_description' + ), 'conds' => array( 'il_to IS NULL' ), - 'join_conds' => array( 'imagelinks' => array( - 'LEFT JOIN', 'il_to = img_name' ) ) + 'join_conds' => array( 'imagelinks' => array( 'LEFT JOIN', 'il_to = img_name' ) ) ); - if ( $wgCountCategorizedImagesAsUsed ) { + if ( $this->getConfig()->get( 'CountCategorizedImagesAsUsed' ) ) { // Order is significant $retval['tables'] = array( 'image', 'page', 'categorylinks', - 'imagelinks' ); + 'imagelinks' ); $retval['conds']['page_namespace'] = NS_FILE; $retval['conds'][] = 'cl_from IS NULL'; $retval['conds'][] = 'img_name = page_title'; $retval['join_conds']['categorylinks'] = array( - 'LEFT JOIN', 'cl_from = page_id' ); + 'LEFT JOIN', 'cl_from = page_id' ); $retval['join_conds']['imagelinks'] = array( - 'LEFT JOIN', 'il_to = page_title' ); + 'LEFT JOIN', 'il_to = page_title' ); } + return $retval; } diff --git a/includes/specials/SpecialUnusedtemplates.php b/includes/specials/SpecialUnusedtemplates.php index 1dc9f420..0c2b8707 100644 --- a/includes/specials/SpecialUnusedtemplates.php +++ b/includes/specials/SpecialUnusedtemplates.php @@ -30,7 +30,6 @@ * @ingroup SpecialPage */ class UnusedtemplatesPage extends QueryPage { - function __construct( $name = 'Unusedtemplates' ) { parent::__construct( $name ); } @@ -50,12 +49,16 @@ class UnusedtemplatesPage extends QueryPage { function getQueryInfo() { return array( 'tables' => array( 'page', 'templatelinks' ), - 'fields' => array( 'namespace' => 'page_namespace', - 'title' => 'page_title', - 'value' => 'page_title' ), - 'conds' => array( 'page_namespace' => NS_TEMPLATE, - 'tl_from IS NULL', - 'page_is_redirect' => 0 ), + 'fields' => array( + 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_title' + ), + 'conds' => array( + 'page_namespace' => NS_TEMPLATE, + 'tl_from IS NULL', + 'page_is_redirect' => 0 + ), 'join_conds' => array( 'templatelinks' => array( 'LEFT JOIN', array( 'tl_title = page_title', 'tl_namespace = page_namespace' ) ) ) @@ -79,6 +82,7 @@ class UnusedtemplatesPage extends QueryPage { SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() ), $this->msg( 'unusedtemplateswlh' )->escaped() ); + return $this->getLanguage()->specialList( $pageLink, $wlhLink ); } diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php index 954e3ffe..bb07c197 100644 --- a/includes/specials/SpecialUnwatchedpages.php +++ b/includes/specials/SpecialUnwatchedpages.php @@ -46,13 +46,16 @@ class UnwatchedpagesPage extends QueryPage { function getQueryInfo() { return array( 'tables' => array( 'page', 'watchlist' ), - 'fields' => array( 'namespace' => 'page_namespace', - 'title' => 'page_title', - 'value' => 'page_namespace' ), - 'conds' => array( 'wl_title IS NULL', - 'page_is_redirect' => 0, - "page_namespace != '" . NS_MEDIAWIKI . - "'" ), + 'fields' => array( + 'namespace' => 'page_namespace', + 'title' => 'page_title', + 'value' => 'page_namespace' + ), + 'conds' => array( + 'wl_title IS NULL', + 'page_is_redirect' => 0, + "page_namespace != '" . NS_MEDIAWIKI . "'" + ), 'join_conds' => array( 'watchlist' => array( 'LEFT JOIN', array( 'wl_title = page_title', 'wl_namespace = page_namespace' ) ) ) @@ -67,6 +70,15 @@ class UnwatchedpagesPage extends QueryPage { return array( 'page_namespace', 'page_title' ); } + /** + * Add the JS + * @param string|null $par + */ + public function execute( $par ) { + parent::execute( $par ); + $this->getOutput()->addModules( 'mediawiki.special.unwatchedPages' ); + } + /** * @param Skin $skin * @param object $result Result row @@ -88,7 +100,7 @@ class UnwatchedpagesPage extends QueryPage { $wlink = Linker::linkKnown( $nt, $this->msg( 'watch' )->escaped(), - array(), + array( 'class' => 'mw-watch-link' ), array( 'action' => 'watch', 'token' => $token ) ); diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index 09facf4f..55d09dd6 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -32,51 +32,57 @@ class SpecialUpload extends SpecialPage { /** * Constructor : initialise object * Get data POSTed through the form and assign them to the object - * @param $request WebRequest : data posted. + * @param WebRequest $request Data posted. */ public function __construct( $request = null ) { parent::__construct( 'Upload', 'upload' ); } /** Misc variables **/ - public $mRequest; // The WebRequest or FauxRequest this form is supposed to handle + + /** @var WebRequest|FauxRequest The request this form is supposed to handle */ + public $mRequest; public $mSourceType; - /** - * @var UploadBase - */ + /** @var UploadBase */ public $mUpload; - /** - * @var LocalFile - */ + /** @var LocalFile */ public $mLocalFile; public $mUploadClicked; /** User input variables from the "description" section **/ - public $mDesiredDestName; // The requested target file name + + /** @var string The requested target file name */ + public $mDesiredDestName; public $mComment; public $mLicense; /** User input variables from the root section **/ + public $mIgnoreWarning; - public $mWatchThis; + public $mWatchthis; public $mCopyrightStatus; public $mCopyrightSource; /** Hidden variables **/ + public $mDestWarningAck; - public $mForReUpload; // The user followed an "overwrite this file" link - public $mCancelUpload; // The user clicked "Cancel and return to upload form" button + + /** @var bool The user followed an "overwrite this file" link */ + public $mForReUpload; + + /** @var bool The user clicked "Cancel and return to upload form" button */ + public $mCancelUpload; public $mTokenOk; - public $mUploadSuccessful = false; // Subclasses can use this to determine whether a file was uploaded + + /** @var bool Subclasses can use this to determine whether a file was uploaded */ + public $mUploadSuccessful = false; /** Text injection points for hooks not using HTMLForm **/ public $uploadFormTextTop; public $uploadFormTextAfterSummary; - public $mWatchthis; - /** * Initialize instance variables from request and create an Upload handler */ @@ -127,8 +133,8 @@ class SpecialUpload extends SpecialPage { * Handle permission checking elsewhere in order to be able to show * custom error messages. * - * @param $user User object - * @return Boolean + * @param User $user + * @return bool */ public function userCanExecute( User $user ) { return UploadBase::isEnabled() && parent::userCanExecute( $user ); @@ -136,6 +142,7 @@ class SpecialUpload extends SpecialPage { /** * Special page entry point + * @param string $par */ public function execute( $par ) { $this->setHeaders(); @@ -180,7 +187,8 @@ class SpecialUpload extends SpecialPage { } else { # Backwards compatibility hook if ( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) { - wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); + wfDebug( "Hook 'UploadForm:initial' broke output of the upload form\n" ); + return; } $this->showUploadForm( $this->getUploadForm() ); @@ -195,7 +203,7 @@ class SpecialUpload extends SpecialPage { /** * Show the main upload form * - * @param $form Mixed: an HTMLForm instance or HTML string to show + * @param HTMLForm|string $form An HTMLForm instance or HTML string to show */ protected function showUploadForm( $form ) { # Add links if file was previously deleted @@ -208,21 +216,20 @@ class SpecialUpload extends SpecialPage { } else { $this->getOutput()->addHTML( $form ); } - } /** * Get an UploadForm instance with title and text properly set. * * @param string $message HTML string to add to the form - * @param string $sessionKey session key in case this is a stashed upload - * @param $hideIgnoreWarning Boolean: whether to hide "ignore warning" check box + * @param string $sessionKey Session key in case this is a stashed upload + * @param bool $hideIgnoreWarning Whether to hide "ignore warning" check box * @return UploadForm */ protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) { # Initialize form $context = new DerivativeContext( $this->getContext() ); - $context->setTitle( $this->getTitle() ); // Remove subpage + $context->setTitle( $this->getPageTitle() ); // Remove subpage $form = new UploadForm( array( 'watch' => $this->getWatchCheck(), 'forreupload' => $this->mForReUpload, @@ -317,23 +324,24 @@ class SpecialUpload extends SpecialPage { $form->setSubmitText( $this->msg( 'upload-tryagain' )->escaped() ); $this->showUploadForm( $form ); } + /** * Stashes the upload, shows the main form, but adds a "continue anyway button". * Also checks whether there are actually warnings to display. * - * @param $warnings Array - * @return boolean true if warnings were displayed, false if there are no - * warnings and it should continue processing + * @param array $warnings + * @return bool True if warnings were displayed, false if there are no + * warnings and it should continue processing */ protected function showUploadWarning( $warnings ) { # If there are no warnings, or warnings we can ignore, return early. # mDestWarningAck is set when some javascript has shown the warning # to the user. mForReUpload is set when the user clicks the "upload a # new version" link. - if ( !$warnings || ( count( $warnings ) == 1 && - isset( $warnings['exists'] ) && - ( $this->mDestWarningAck || $this->mForReUpload ) ) ) - { + if ( !$warnings || ( count( $warnings ) == 1 + && isset( $warnings['exists'] ) + && ( $this->mDestWarningAck || $this->mForReUpload ) ) + ) { return false; } @@ -350,9 +358,14 @@ class SpecialUpload extends SpecialPage { } elseif ( $warning == 'duplicate' ) { $msg = $this->getDupeWarning( $args ); } elseif ( $warning == 'duplicate-archive' ) { - $msg = "\t
      • " . $this->msg( 'file-deleted-duplicate', - Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse() - . "
      • \n"; + if ( $args === '' ) { + $msg = "\t
      • " . $this->msg( 'file-deleted-duplicate-notitle' )->parse() + . "
      • \n"; + } else { + $msg = "\t
      • " . $this->msg( 'file-deleted-duplicate', + Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse() + . "
      • \n"; + } } else { if ( $args === true ) { $args = array(); @@ -397,6 +410,7 @@ class SpecialUpload extends SpecialPage { $status = $this->mUpload->fetchFile(); if ( !$status->isOK() ) { $this->showUploadError( $this->getOutput()->parse( $status->getWikiText() ) ); + return; } @@ -414,6 +428,7 @@ class SpecialUpload extends SpecialPage { $details = $this->mUpload->verifyUpload(); if ( $details['status'] != UploadBase::OK ) { $this->processVerificationError( $details ); + return; } @@ -422,6 +437,7 @@ class SpecialUpload extends SpecialPage { if ( $permErrors !== true ) { $code = array_shift( $permErrors[0] ); $this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() ); + return; } @@ -442,9 +458,17 @@ class SpecialUpload extends SpecialPage { } else { $pageText = false; } - $status = $this->mUpload->performUpload( $this->mComment, $pageText, $this->mWatchthis, $this->getUser() ); + + $status = $this->mUpload->performUpload( + $this->mComment, + $pageText, + $this->mWatchthis, + $this->getUser() + ); + if ( !$status->isGood() ) { $this->showUploadError( $this->getOutput()->parse( $status->getWikiText() ) ); + return; } @@ -456,13 +480,16 @@ class SpecialUpload extends SpecialPage { /** * Get the initial image page text based on a comment and optional file status information - * @param $comment string - * @param $license string - * @param $copyStatus string - * @param $source string + * @param string $comment + * @param string $license + * @param string $copyStatus + * @param string $source * @return string + * @todo Use Config obj instead of globals */ - public static function getInitialPageText( $comment = '', $license = '', $copyStatus = '', $source = '' ) { + public static function getInitialPageText( $comment = '', $license = '', + $copyStatus = '', $source = '' + ) { global $wgUseCopyrightUpload, $wgForceUIMsgAsContentMsg; $msg = array(); @@ -496,6 +523,7 @@ class SpecialUpload extends SpecialPage { $pageText = $comment; } } + return $pageText; } @@ -509,7 +537,7 @@ class SpecialUpload extends SpecialPage { * * Note that the page target can be changed *on the form*, so our check * state can get out of sync. - * @return Bool|String + * @return bool|string */ protected function getWatchCheck() { if ( $this->getUser()->getOption( 'watchdefault' ) ) { @@ -517,11 +545,17 @@ class SpecialUpload extends SpecialPage { return true; } + $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); + if ( $desiredTitleObj instanceof Title && $this->getUser()->isWatched( $desiredTitleObj ) ) { + // Already watched, don't change that + return true; + } + $local = wfLocalFile( $this->mDesiredDestName ); if ( $local && $local->exists() ) { // We're uploading a new version of an existing file. // No creation, so don't watch it if we're not already. - return $this->getUser()->isWatched( $local->getTitle() ); + return false; } else { // New page should get watched if that's our option. return $this->getUser()->getOption( 'watchcreations' ); @@ -531,12 +565,10 @@ class SpecialUpload extends SpecialPage { /** * Provides output to the user for a result of UploadBase::verifyUpload * - * @param array $details result of UploadBase::verifyUpload + * @param array $details Result of UploadBase::verifyUpload * @throws MWException */ protected function processVerificationError( $details ) { - global $wgFileExtensions; - switch ( $details['status'] ) { /** Statuses that only require name changing **/ @@ -571,7 +603,7 @@ class SpecialUpload extends SpecialPage { } else { $msg->params( $details['finalExt'] ); } - $extensions = array_unique( $wgFileExtensions ); + $extensions = array_unique( $this->getConfig()->get( 'FileExtensions' ) ); $msg->params( $this->getLanguage()->commaList( $extensions ), count( $extensions ) ); @@ -610,7 +642,7 @@ class SpecialUpload extends SpecialPage { /** * Remove a temporarily kept file stashed by saveTempUploadedFile(). * - * @return Boolean: success + * @return bool Success */ protected function unsaveUploadedFile() { if ( !( $this->mUpload instanceof UploadFromStash ) ) { @@ -619,6 +651,7 @@ class SpecialUpload extends SpecialPage { $success = $this->mUpload->unsaveUploadedFile(); if ( !$success ) { $this->getOutput()->showFileDeleteError( $this->mUpload->getTempPath() ); + return false; } else { return true; @@ -631,8 +664,8 @@ class SpecialUpload extends SpecialPage { * Formats a result of UploadBase::getExistsWarning as HTML * This check is static and can be done pre-upload via AJAX * - * @param array $exists the result of UploadBase::getExistsWarning - * @return String: empty string if there is no warning or an HTML fragment + * @param array $exists The result of UploadBase::getExistsWarning + * @return string Empty string if there is no warning or an HTML fragment */ public static function getExistsWarning( $exists ) { if ( !$exists ) { @@ -683,7 +716,7 @@ class SpecialUpload extends SpecialPage { /** * Construct a warning and a gallery from an array of duplicate files. - * @param $dupes array + * @param array $dupes * @return string */ public function getDupeWarning( $dupes ) { @@ -691,12 +724,12 @@ class SpecialUpload extends SpecialPage { return ''; } - $gallery = ImageGalleryBase::factory(); - $gallery->setContext( $this->getContext() ); + $gallery = ImageGalleryBase::factory( false, $this->getContext() ); $gallery->setShowBytes( false ); foreach ( $dupes as $file ) { $gallery->add( $file->getTitle() ); } + return '
      • ' . wfMessage( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() . $gallery->toHtml() . "
      • \n"; @@ -705,6 +738,18 @@ class SpecialUpload extends SpecialPage { protected function getGroupName() { return 'media'; } + + /** + * Should we rotate images in the preview on Special:Upload. + * + * This controls js: mw.config.get( 'wgFileCanRotate' ) + * + * @todo What about non-BitmapHandler handled files? + */ + static public function rotationEnabled() { + $bitmapHandler = new BitmapHandler(); + return $bitmapHandler->autoRotateEnabled(); + } } /** @@ -731,8 +776,7 @@ class UploadForm extends HTMLForm { public function __construct( array $options = array(), IContextSource $context = null ) { $this->mWatch = !empty( $options['watch'] ); $this->mForReUpload = !empty( $options['forreupload'] ); - $this->mSessionKey = isset( $options['sessionkey'] ) - ? $options['sessionkey'] : ''; + $this->mSessionKey = isset( $options['sessionkey'] ) ? $options['sessionkey'] : ''; $this->mHideIgnoreWarning = !empty( $options['hideignorewarning'] ); $this->mDestWarningAck = !empty( $options['destwarningack'] ); $this->mDestFile = isset( $options['destfile'] ) ? $options['destfile'] : ''; @@ -754,6 +798,18 @@ class UploadForm extends HTMLForm { wfRunHooks( 'UploadFormInitDescriptor', array( &$descriptor ) ); parent::__construct( $descriptor, $context, 'upload' ); + # Add a link to edit MediaWik:Licenses + if ( $this->getUser()->isAllowed( 'editinterface' ) ) { + $licensesLink = Linker::link( + Title::makeTitle( NS_MEDIAWIKI, 'Licenses' ), + $this->msg( 'licenses-edit' )->escaped(), + array(), + array( 'action' => 'edit' ) + ); + $editLicenses = '

        ' . $licensesLink . '

        '; + $this->addFooterText( $editLicenses, 'description' ); + } + # Set some form properties $this->setSubmitText( $this->msg( 'uploadbtn' )->text() ); $this->setSubmitName( 'wpUpload' ); @@ -768,18 +824,15 @@ class UploadForm extends HTMLForm { $this->mSourceIds[] = $field['id']; } } - } /** * Get the descriptor of the fieldset that contains the file source * selection. The section is 'source' * - * @return Array: descriptor array + * @return array Descriptor array */ protected function getSourceSection() { - global $wgCopyUploadsFromSpecialUpload; - if ( $this->mSessionKey ) { return array( 'SessionKey' => array( @@ -794,8 +847,8 @@ class UploadForm extends HTMLForm { } $canUploadByUrl = UploadFromUrl::isEnabled() - && UploadFromUrl::isAllowed( $this->getUser() ) - && $wgCopyUploadsFromSpecialUpload; + && ( UploadFromUrl::isAllowed( $this->getUser() ) === true ) + && $this->getConfig()->get( 'CopyUploadsFromSpecialUpload' ); $radio = $canUploadByUrl; $selectedSourceType = strtolower( $this->getRequest()->getText( 'wpSourceType', 'File' ) ); @@ -812,7 +865,7 @@ class UploadForm extends HTMLForm { $this->mMaxUploadSize['file'] = UploadBase::getMaxUploadSize( 'file' ); # Limit to upload_max_filesize unless we are running under HipHop and # that setting doesn't exist - if ( !wfIsHipHop() ) { + if ( !wfIsHHVM() ) { $this->mMaxUploadSize['file'] = min( $this->mMaxUploadSize['file'], wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ), wfShorthandToInteger( ini_get( 'post_max_size' ) ) @@ -824,12 +877,13 @@ class UploadForm extends HTMLForm { 'section' => 'source', 'type' => 'file', 'id' => 'wpUploadFile', + 'radio-id' => 'wpSourceTypeFile', 'label-message' => 'sourcefilename', 'upload-type' => 'File', 'radio' => &$radio, 'help' => $this->msg( 'upload-maxfilesize', - $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] ) ) - ->parse() . + $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['file'] ) + )->parse() . $this->msg( 'word-separator' )->escaped() . $this->msg( 'upload_source_file' )->escaped(), 'checked' => $selectedSourceType == 'file', @@ -841,12 +895,13 @@ class UploadForm extends HTMLForm { 'class' => 'UploadSourceField', 'section' => 'source', 'id' => 'wpUploadFileURL', + 'radio-id' => 'wpSourceTypeurl', 'label-message' => 'sourceurl', 'upload-type' => 'url', 'radio' => &$radio, 'help' => $this->msg( 'upload-maxfilesize', - $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] ) ) - ->parse() . + $this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] ) + )->parse() . $this->msg( 'word-separator' )->escaped() . $this->msg( 'upload_source_url' )->escaped(), 'checked' => $selectedSourceType == 'url', @@ -860,41 +915,57 @@ class UploadForm extends HTMLForm { 'default' => $this->getExtensionsMessage(), 'raw' => true, ); + return $descriptor; } /** * Get the messages indicating which extensions are preferred and prohibitted. * - * @return String: HTML string containing the message + * @return string HTML string containing the message */ protected function getExtensionsMessage() { # Print a list of allowed file extensions, if so configured. We ignore # MIME type here, it's incomprehensible to most people and too long. - global $wgCheckFileExtensions, $wgStrictFileExtensions, - $wgFileExtensions, $wgFileBlacklist; + $config = $this->getConfig(); - if ( $wgCheckFileExtensions ) { - if ( $wgStrictFileExtensions ) { + if ( $config->get( 'CheckFileExtensions' ) ) { + if ( $config->get( 'StrictFileExtensions' ) ) { # Everything not permitted is banned $extensionsList = '
        ' . - $this->msg( 'upload-permitted', $this->getContext()->getLanguage()->commaList( array_unique( $wgFileExtensions ) ) )->parseAsBlock() . + $this->msg( + 'upload-permitted', + $this->getContext()->getLanguage()->commaList( + array_unique( $config->get( 'FileExtensions' ) ) + ) + )->parseAsBlock() . "
        \n"; } else { # We have to list both preferred and prohibited $extensionsList = '
        ' . - $this->msg( 'upload-preferred', $this->getContext()->getLanguage()->commaList( array_unique( $wgFileExtensions ) ) )->parseAsBlock() . + $this->msg( + 'upload-preferred', + $this->getContext()->getLanguage()->commaList( + array_unique( $config->get( 'FileExtensions' ) ) + ) + )->parseAsBlock() . "
        \n" . '
        ' . - $this->msg( 'upload-prohibited', $this->getContext()->getLanguage()->commaList( array_unique( $wgFileBlacklist ) ) )->parseAsBlock() . + $this->msg( + 'upload-prohibited', + $this->getContext()->getLanguage()->commaList( + array_unique( $config->get( 'FileBlacklist' ) ) + ) + )->parseAsBlock() . "
        \n"; } } else { # Everything is permitted. $extensionsList = ''; } + return $extensionsList; } @@ -902,9 +973,10 @@ class UploadForm extends HTMLForm { * Get the descriptor of the fieldset that contains the file description * input. The section is 'description' * - * @return Array: descriptor array + * @return array Descriptor array */ protected function getDescriptionSection() { + $config = $this->getConfig(); if ( $this->mSessionKey ) { $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash(); try { @@ -977,8 +1049,7 @@ class UploadForm extends HTMLForm { ); } - global $wgUseCopyrightUpload; - if ( $wgUseCopyrightUpload ) { + if ( $config->get( 'UseCopyrightUpload' ) ) { $descriptor['UploadCopyStatus'] = array( 'type' => 'text', 'section' => 'description', @@ -1000,7 +1071,7 @@ class UploadForm extends HTMLForm { * Get the descriptor of the fieldset that contains the upload options, * such as "watch this file". The section is 'options' * - * @return Array: descriptor array + * @return array Descriptor array */ protected function getOptionsSection() { $user = $this->getUser(); @@ -1011,7 +1082,7 @@ class UploadForm extends HTMLForm { 'id' => 'wpWatchthis', 'label-message' => 'watchthisupload', 'section' => 'options', - 'default' => $user->getOption( 'watchcreations' ), + 'default' => $this->mWatch, ) ); } @@ -1053,10 +1124,11 @@ class UploadForm extends HTMLForm { * Add upload JS to the OutputPage */ protected function addUploadJS() { - global $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview, $wgEnableAPI, $wgStrictFileExtensions; + $config = $this->getConfig(); - $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck; - $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview && $wgEnableAPI; + $useAjaxDestCheck = $config->get( 'UseAjax' ) && $config->get( 'AjaxUploadDestCheck' ); + $useAjaxLicensePreview = $config->get( 'UseAjax' ) && + $config->get( 'AjaxLicensePreview' ) && $config->get( 'EnableAPI' ); $this->mMaxUploadSize['*'] = UploadBase::getMaxUploadSize(); $scriptVars = array( @@ -1067,7 +1139,7 @@ class UploadForm extends HTMLForm { // the wpDestFile textbox $this->mDestFile === '', 'wgUploadSourceIds' => $this->mSourceIds, - 'wgStrictFileExtensions' => $wgStrictFileExtensions, + 'wgStrictFileExtensions' => $config->get( 'StrictFileExtensions' ), 'wgCapitalizeUploads' => MWNamespace::isCapitalized( NS_FILE ), 'wgMaxUploadSize' => $this->mMaxUploadSize, ); @@ -1077,20 +1149,18 @@ class UploadForm extends HTMLForm { $out->addModules( array( 'mediawiki.action.edit', // For support - 'mediawiki.legacy.upload', // Old form stuff... - 'mediawiki.special.upload', // Newer extras for thumbnail preview. + 'mediawiki.special.upload', // Extras for thumbnail and license preview. ) ); } /** * Empty function; submission is handled elsewhere. * - * @return bool false + * @return bool False */ function trySubmit() { return false; } - } /** @@ -1099,7 +1169,7 @@ class UploadForm extends HTMLForm { class UploadSourceField extends HTMLTextField { /** - * @param $cellAttributes array + * @param array $cellAttributes * @return string */ function getLabelHtml( $cellAttributes = array() ) { @@ -1107,15 +1177,25 @@ class UploadSourceField extends HTMLTextField { $label = Html::rawElement( 'label', array( 'for' => $id ), $this->mLabel ); if ( !empty( $this->mParams['radio'] ) ) { + if ( isset( $this->mParams['radio-id'] ) ) { + $radioId = $this->mParams['radio-id']; + } else { + // Old way. For the benefit of extensions that do not define + // the 'radio-id' key. + $radioId = 'wpSourceType' . $this->mParams['upload-type']; + } + $attribs = array( 'name' => 'wpSourceType', 'type' => 'radio', - 'id' => $id, + 'id' => $radioId, 'value' => $this->mParams['upload-type'], ); + if ( !empty( $this->mParams['checked'] ) ) { $attribs['checked'] = 'checked'; } + $label .= Html::element( 'input', $attribs ); } diff --git a/includes/specials/SpecialUploadStash.php b/includes/specials/SpecialUploadStash.php index 1373df1a..ddb435d9 100644 --- a/includes/specials/SpecialUploadStash.php +++ b/includes/specials/SpecialUploadStash.php @@ -57,8 +57,9 @@ class SpecialUploadStash extends UnlistedSpecialPage { /** * Execute page -- can output a file directly or show a listing of them. * - * @param string $subPage subpage, e.g. in http://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part - * @return Boolean: success + * @param string $subPage Subpage, e.g. in + * http://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part + * @return bool Success */ public function execute( $subPage ) { $this->checkPermissions(); @@ -66,6 +67,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { if ( $subPage === null || $subPage === '' ) { return $this->showUploads(); } + return $this->showUpload( $subPage ); } @@ -73,7 +75,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { * If file available in stash, cats it out to the client as a simple HTTP response. * n.b. Most sanity checking done in UploadStashLocalFile, so this is straightforward. * - * @param string $key the key of a particular requested file + * @param string $key The key of a particular requested file * @throws HttpError * @return bool */ @@ -99,7 +101,8 @@ class SpecialUploadStash extends UnlistedSpecialPage { $message = $e->getMessage(); } catch ( SpecialUploadStashTooLargeException $e ) { $code = 500; - $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES . ' bytes. ' . $e->getMessage(); + $message = 'Cannot serve a file larger than ' . self::MAX_SERVE_BYTES . + ' bytes. ' . $e->getMessage(); } catch ( Exception $e ) { $code = 500; $message = $e->getMessage(); @@ -136,10 +139,11 @@ class SpecialUploadStash extends UnlistedSpecialPage { $handler = $file->getHandler(); if ( $handler ) { $params = $handler->parseParamString( $paramString ); + return array( 'file' => $file, 'type' => $type, 'params' => $params ); } else { throw new UploadStashBadPathException( 'No handler found for ' . - "mime {$file->getMimeType()} of file {$file->getPath()}" ); + "mime {$file->getMimeType()} of file {$file->getPath()}" ); } } @@ -149,20 +153,19 @@ class SpecialUploadStash extends UnlistedSpecialPage { /** * Get a thumbnail for file, either generated locally or remotely, and stream it out * - * @param $file - * @param $params array + * @param File $file + * @param array $params * - * @return boolean success + * @return bool Success */ private function outputThumbFromStash( $file, $params ) { - - // this global, if it exists, points to a "scaler", as you might find in the Wikimedia Foundation cluster. See outputRemoteScaledThumb() - // this is part of our horrible NFS-based system, we create a file on a mount point here, but fetch the scaled file from somewhere else that - // happens to share it over NFS - global $wgUploadStashScalerBaseUrl; - $flags = 0; - if ( $wgUploadStashScalerBaseUrl ) { + // this config option, if it exists, points to a "scaler", as you might find in + // the Wikimedia Foundation cluster. See outputRemoteScaledThumb(). This + // is part of our horrible NFS-based system, we create a file on a mount + // point here, but fetch the scaled file from somewhere else that + // happens to share it over NFS. + if ( $this->getConfig()->get( 'UploadStashScalerBaseUrl' ) ) { $this->outputRemoteScaledThumb( $file, $params, $flags ); } else { $this->outputLocallyScaledThumb( $file, $params, $flags ); @@ -170,16 +173,15 @@ class SpecialUploadStash extends UnlistedSpecialPage { } /** - * Scale a file (probably with a locally installed imagemagick, or similar) and output it to STDOUT. - * @param $file File + * Scale a file (probably with a locally installed imagemagick, or similar) + * and output it to STDOUT. + * @param File $file * @param array $params Scaling parameters ( e.g. array( width => '50' ) ); * @param int $flags Scaling flags ( see File:: constants ) - * @throws MWException - * @throws UploadStashFileNotFoundException - * @return boolean success + * @throws MWException|UploadStashFileNotFoundException + * @return bool Success */ private function outputLocallyScaledThumb( $file, $params, $flags ) { - // n.b. this is stupid, we insist on re-transforming the file every time we are invoked. We rely // on HTTP caching to ensure this doesn't happen. @@ -195,8 +197,8 @@ class SpecialUploadStash extends UnlistedSpecialPage { throw new UploadStashFileNotFoundException( "no local path for scaled item" ); } - // now we should construct a File, so we can get mime and other such info in a standard way - // n.b. mimetype may be different from original (ogx original -> jpeg thumb) + // now we should construct a File, so we can get MIME and other such info in a standard way + // n.b. MIME type may be different from original (ogx original -> jpeg thumb) $thumbFile = new UnregisteredLocalFile( false, $this->stash->repo, $thumbnailImage->getStoragePath(), false ); if ( !$thumbFile ) { @@ -204,28 +206,32 @@ class SpecialUploadStash extends UnlistedSpecialPage { } return $this->outputLocalFile( $thumbFile ); - } /** - * Scale a file with a remote "scaler", as exists on the Wikimedia Foundation cluster, and output it to STDOUT. - * Note: unlike the usual thumbnail process, the web client never sees the cluster URL; we do the whole HTTP transaction to the scaler ourselves - * and cat the results out. - * Note: We rely on NFS to have propagated the file contents to the scaler. However, we do not rely on the thumbnail being created in NFS and then - * propagated back to our filesystem. Instead we take the results of the HTTP request instead. - * Note: no caching is being done here, although we are instructing the client to cache it forever. - * @param $file: File object - * @param $params: scaling parameters ( e.g. array( width => '50' ) ); - * @param $flags: scaling flags ( see File:: constants ) + * Scale a file with a remote "scaler", as exists on the Wikimedia Foundation + * cluster, and output it to STDOUT. + * Note: Unlike the usual thumbnail process, the web client never sees the + * cluster URL; we do the whole HTTP transaction to the scaler ourselves + * and cat the results out. + * Note: We rely on NFS to have propagated the file contents to the scaler. + * However, we do not rely on the thumbnail being created in NFS and then + * propagated back to our filesystem. Instead we take the results of the + * HTTP request instead. + * Note: No caching is being done here, although we are instructing the + * client to cache it forever. + * + * @param File $file + * @param array $params Scaling parameters ( e.g. array( width => '50' ) ); + * @param int $flags Scaling flags ( see File:: constants ) * @throws MWException - * @return boolean success + * @return bool Success */ private function outputRemoteScaledThumb( $file, $params, $flags ) { - - // this global probably looks something like 'http://upload.wikimedia.org/wikipedia/test/thumb/temp' - // do not use trailing slash - global $wgUploadStashScalerBaseUrl; - $scalerBaseUrl = $wgUploadStashScalerBaseUrl; + // This option probably looks something like + // 'http://upload.wikimedia.org/wikipedia/test/thumb/temp'. Do not use + // trailing slash. + $scalerBaseUrl = $this->getConfig()->get( 'UploadStashScalerBaseUrl' ); if ( preg_match( '/^\/\//', $scalerBaseUrl ) ) { // this is apparently a protocol-relative URL, which makes no sense in this context, @@ -248,16 +254,17 @@ class SpecialUploadStash extends UnlistedSpecialPage { ); $req = MWHttpRequest::factory( $scalerThumbUrl, $httpOptions ); $status = $req->execute(); - if ( ! $status->isOK() ) { + if ( !$status->isOK() ) { $errors = $status->getErrorsArray(); $errorStr = "Fetching thumbnail failed: " . print_r( $errors, 1 ); $errorStr .= "\nurl = $scalerThumbUrl\n"; throw new MWException( $errorStr ); } $contentType = $req->getResponseHeader( "content-type" ); - if ( ! $contentType ) { + if ( !$contentType ) { throw new MWException( "Missing content-type header" ); } + return $this->outputContents( $req->getContent(), $contentType ); } @@ -265,7 +272,8 @@ class SpecialUploadStash extends UnlistedSpecialPage { * Output HTTP response for file * Side effect: writes HTTP response to STDOUT. * - * @param $file File object with a local path (e.g. UnregisteredLocalFile, LocalFile. Oddly these don't share an ancestor!) + * @param File $file File object with a local path (e.g. UnregisteredLocalFile, + * LocalFile. Oddly these don't share an ancestor!) * @throws SpecialUploadStashTooLargeException * @return bool */ @@ -273,6 +281,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { if ( $file->getSize() > self::MAX_SERVE_BYTES ) { throw new SpecialUploadStashTooLargeException(); } + return $file->getRepo()->streamFile( $file->getPath(), array( 'Content-Transfer-Encoding: binary', 'Expires: Sun, 17-Jan-2038 19:14:07 GMT' ) @@ -282,8 +291,8 @@ class SpecialUploadStash extends UnlistedSpecialPage { /** * Output HTTP response of raw content * Side effect: writes HTTP response to STDOUT. - * @param string $content content - * @param string $contentType mime type + * @param string $content Content + * @param string $contentType MIME type * @throws SpecialUploadStashTooLargeException * @return bool */ @@ -294,15 +303,18 @@ class SpecialUploadStash extends UnlistedSpecialPage { } self::outputFileHeaders( $contentType, $size ); print $content; + return true; } /** * Output headers for streaming - * XXX unsure about encoding as binary; if we received from HTTP perhaps we should use that encoding, concatted with semicolon to mimeType as it usually is. + * @todo Unsure about encoding as binary; if we received from HTTP perhaps + * we should use that encoding, concatenated with semicolon to `$contentType` as it + * usually is. * Side effect: preps PHP to write headers to STDOUT. - * @param string $contentType : string suitable for content-type header - * @param string $size: length in bytes + * @param string $contentType String suitable for content-type header + * @param string $size Length in bytes */ private static function outputFileHeaders( $contentType, $size ) { header( "Content-Type: $contentType", true ); @@ -324,11 +336,13 @@ class SpecialUploadStash extends UnlistedSpecialPage { public static function tryClearStashedUploads( $formData ) { if ( isset( $formData['Clear'] ) ) { $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash(); - wfDebug( "stash has: " . print_r( $stash->listFiles(), true ) ); - if ( ! $stash->clear() ) { + wfDebug( 'stash has: ' . print_r( $stash->listFiles(), true ) . "\n" ); + + if ( !$stash->clear() ) { return Status::newFatal( 'uploadstash-errclear' ); } } + return Status::newGood(); } @@ -346,7 +360,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { // this design is extremely dubious, but supposedly HTMLForm is our standard now? $context = new DerivativeContext( $this->getContext() ); - $context->setTitle( $this->getTitle() ); // Remove subpage + $context->setTitle( $this->getPageTitle() ); // Remove subpage $form = new HTMLForm( array( 'Clear' => array( 'type' => 'hidden', @@ -362,7 +376,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { // show the files + form, if there are any, or just say there are none $refreshHtml = Html::element( 'a', - array( 'href' => $this->getTitle()->getLocalURL() ), + array( 'href' => $this->getPageTitle()->getLocalURL() ), $this->msg( 'uploadstash-refresh' )->text() ); $files = $this->stash->listFiles(); if ( $files && count( $files ) ) { @@ -372,7 +386,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { // TODO: Use Linker::link or even construct the list in plain wikitext $fileListItemsHtml .= Html::rawElement( 'li', array(), Html::element( 'a', array( 'href' => - $this->getTitle( "file/$file" )->getLocalURL() ), $file ) + $this->getPageTitle( "file/$file" )->getLocalURL() ), $file ) ); } $this->getOutput()->addHtml( Html::rawElement( 'ul', array(), $fileListItemsHtml ) ); @@ -390,4 +404,5 @@ class SpecialUploadStash extends UnlistedSpecialPage { } } -class SpecialUploadStashTooLargeException extends MWException {}; +class SpecialUploadStashTooLargeException extends MWException { +} diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php index 5ac3e654..6de7c90d 100644 --- a/includes/specials/SpecialUserlogin.php +++ b/includes/specials/SpecialUserlogin.php @@ -27,7 +27,6 @@ * @ingroup SpecialPage */ class LoginForm extends SpecialPage { - const SUCCESS = 0; const NO_NAME = 1; const ILLEGAL = 2; @@ -42,26 +41,64 @@ class LoginForm extends SpecialPage { const USER_BLOCKED = 11; const NEED_TOKEN = 12; const WRONG_TOKEN = 13; + const USER_MIGRATED = 14; - var $mUsername, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted; - var $mAction, $mCreateaccount, $mCreateaccountMail; - var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage; - var $mSkipCookieCheck, $mReturnToQuery, $mToken, $mStickHTTPS; - var $mType, $mReason, $mRealName; - var $mAbortLoginErrorMsg = null; + /** + * Valid error and warning messages + * + * Special:Userlogin can show an error or warning message on the form when + * coming from another page. This is done via the ?error= or ?warning= GET + * parameters. + * + * This array is the list of valid message keys. All other values will be + * ignored. + * + * @since 1.24 + * @var string[] + */ + public static $validErrorMessages = array( + 'exception-nologin-text', + 'watchlistanontext', + 'changeemail-no-info', + 'resetpass-no-info', + 'confirmemail_needlogin', + 'prefsnologintext2', + ); + + public $mAbortLoginErrorMsg = null; + + protected $mUsername; + protected $mPassword; + protected $mRetype; + protected $mReturnTo; + protected $mCookieCheck; + protected $mPosted; + protected $mAction; + protected $mCreateaccount; + protected $mCreateaccountMail; + protected $mLoginattempt; + protected $mRemember; + protected $mEmail; + protected $mDomain; + protected $mLanguage; + protected $mSkipCookieCheck; + protected $mReturnToQuery; + protected $mToken; + protected $mStickHTTPS; + protected $mType; + protected $mReason; + protected $mRealName; + protected $mEntryError = ''; + protected $mEntryErrorType = 'error'; + + private $mTempPasswordUsed; private $mLoaded = false; private $mSecureLoginUrl; - /** - * @ var WebRequest - */ + /** @var WebRequest */ private $mOverrideRequest = null; - /** - * Effective request; set at the beginning of load - * - * @var WebRequest $mRequest - */ + /** @var WebRequest Effective request; set at the beginning of load */ private $mRequest = null; /** @@ -100,19 +137,53 @@ class LoginForm extends SpecialPage { $this->mCookieCheck = $request->getVal( 'wpCookieCheck' ); $this->mPosted = $request->wasPosted(); $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' ) - && $wgEnableEmail; + && $wgEnableEmail; $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' ) && !$this->mCreateaccountMail; $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' ); $this->mAction = $request->getVal( 'action' ); $this->mRemember = $request->getCheck( 'wpRemember' ); $this->mFromHTTP = $request->getBool( 'fromhttp', false ); - $this->mStickHTTPS = ( !$this->mFromHTTP && $request->detectProtocol() === 'https' ) || $request->getBool( 'wpForceHttps', false ); + $this->mStickHTTPS = ( !$this->mFromHTTP && $request->getProtocol() === 'https' ) + || $request->getBool( 'wpForceHttps', false ); $this->mLanguage = $request->getText( 'uselang' ); $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' ); - $this->mToken = ( $this->mType == 'signup' ) ? $request->getVal( 'wpCreateaccountToken' ) : $request->getVal( 'wpLoginToken' ); + $this->mToken = $this->mType == 'signup' + ? $request->getVal( 'wpCreateaccountToken' ) + : $request->getVal( 'wpLoginToken' ); $this->mReturnTo = $request->getVal( 'returnto', '' ); $this->mReturnToQuery = $request->getVal( 'returntoquery', '' ); + // Show an error or warning passed on from a previous page + $entryError = $this->msg( $request->getVal( 'error', '' ) ); + $entryWarning = $this->msg( $request->getVal( 'warning', '' ) ); + // bc: provide login link as a parameter for messages where the translation + // was not updated + $loginreqlink = Linker::linkKnown( + $this->getPageTitle(), + $this->msg( 'loginreqlink' )->escaped(), + array(), + array( + 'returnto' => $this->mReturnTo, + 'returntoquery' => $this->mReturnToQuery, + 'uselang' => $this->mLanguage, + 'fromhttp' => $this->mFromHTTP ? '1' : '0', + ) + ); + + // Only show valid error or warning messages. + if ( $entryError->exists() + && in_array( $entryError->getKey(), self::$validErrorMessages ) + ) { + $this->mEntryErrorType = 'error'; + $this->mEntryError = $entryError->rawParams( $loginreqlink )->escaped(); + + } elseif ( $entryWarning->exists() + && in_array( $entryWarning->getKey(), self::$validErrorMessages ) + ) { + $this->mEntryErrorType = 'warning'; + $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->escaped(); + } + if ( $wgEnableEmail ) { $this->mEmail = $request->getText( 'wpEmail' ); } else { @@ -133,9 +204,10 @@ class LoginForm extends SpecialPage { # 2. Do not return to PasswordReset after a successful password change # but goto Wiki start page (Main_Page) instead ( bug 33997 ) $returnToTitle = Title::newFromText( $this->mReturnTo ); - if ( is_object( $returnToTitle ) && ( - $returnToTitle->isSpecial( 'Userlogout' ) - || $returnToTitle->isSpecial( 'PasswordReset' ) ) ) { + if ( is_object( $returnToTitle ) + && ( $returnToTitle->isSpecial( 'Userlogout' ) + || $returnToTitle->isSpecial( 'PasswordReset' ) ) + ) { $this->mReturnTo = ''; $this->mReturnToQuery = ''; } @@ -149,8 +221,8 @@ class LoginForm extends SpecialPage { } } - /* - * @param $subPage string|null + /** + * @param string|null $subPage */ public function execute( $subPage ) { if ( session_id() == '' ) { @@ -166,21 +238,43 @@ class LoginForm extends SpecialPage { } $this->setHeaders(); + // In the case where the user is already logged in, and was redirected to the login form from a + // page that requires login, do not show the login page. The use case scenario for this is when + // a user opens a large number of tabs, is redirected to the login page on all of them, and then + // logs in on one, expecting all the others to work properly. + // + // However, do show the form if it was visited intentionally (no 'returnto' is present). People + // who often switch between several accounts have grown accustomed to this behavior. + if ( + $this->mType !== 'signup' && + !$this->mPosted && + $this->getUser()->isLoggedIn() && + ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) + ) { + $this->successfulLogin(); + } + // If logging in and not on HTTPS, either redirect to it or offer a link. global $wgSecureLogin; - if ( WebRequest::detectProtocol() !== 'https' ) { + if ( $this->mRequest->getProtocol() !== 'https' ) { $title = $this->getFullTitle(); $query = array( - 'returnto' => $this->mReturnTo, - 'returntoquery' => $this->mReturnToQuery, + 'returnto' => $this->mReturnTo !== '' ? $this->mReturnTo : null, + 'returntoquery' => $this->mReturnToQuery !== '' ? + $this->mReturnToQuery : null, 'title' => null, + ( $this->mEntryErrorType === 'error' ? 'error' : 'warning' ) => $this->mEntryError, ) + $this->mRequest->getQueryValues(); $url = $title->getFullURL( $query, false, PROTO_HTTPS ); - if ( $wgSecureLogin && wfCanIPUseHTTPS( $this->getRequest()->getIP() ) ) { + if ( $wgSecureLogin + && wfCanIPUseHTTPS( $this->getRequest()->getIP() ) + && !$this->mFromHTTP ) // Avoid infinite redirect + { $url = wfAppendQuery( $url, 'fromhttp=1' ); $this->getOutput()->redirect( $url ); // Since we only do this redir to change proto, always vary $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' ); + return; } else { // A wiki without HTTPS login support should set $wgServer to @@ -194,20 +288,24 @@ class LoginForm extends SpecialPage { if ( !is_null( $this->mCookieCheck ) ) { $this->onCookieRedirectCheck( $this->mCookieCheck ); + return; } elseif ( $this->mPosted ) { if ( $this->mCreateaccount ) { $this->addNewAccount(); + return; } elseif ( $this->mCreateaccountMail ) { $this->addNewAccountMailPassword(); + return; } elseif ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) { $this->processLogin(); + return; } } - $this->mainLoginForm( '' ); + $this->mainLoginForm( $this->mEntryError, $this->mEntryErrorType ); } /** @@ -216,13 +314,15 @@ class LoginForm extends SpecialPage { function addNewAccountMailPassword() { if ( $this->mEmail == '' ) { $this->mainLoginForm( $this->msg( 'noemailcreate' )->escaped() ); + return; } - $status = $this->addNewaccountInternal(); + $status = $this->addNewAccountInternal(); if ( !$status->isGood() ) { $error = $status->getMessage(); $this->mainLoginForm( $error->toString() ); + return; } @@ -259,6 +359,7 @@ class LoginForm extends SpecialPage { if ( !$status->isGood() ) { $error = $status->getMessage(); $this->mainLoginForm( $error->toString() ); + return false; } @@ -318,10 +419,11 @@ class LoginForm extends SpecialPage { # Confirm that the account was created $out->setPageTitle( $this->msg( 'accountcreated' ) ); $out->addWikiMsg( 'accountcreatedtext', $u->getName() ); - $out->addReturnTo( $this->getTitle() ); + $out->addReturnTo( $this->getPageTitle() ); wfRunHooks( 'AddNewAccount', array( $u, false ) ); $u->addNewUserLogEntry( 'create2', $this->mReason ); } + return true; } @@ -364,6 +466,7 @@ class LoginForm extends SpecialPage { # Request forgery checks. if ( !self::getCreateaccountToken() ) { self::setCreateaccountToken(); + return Status::newFatal( 'nocookiesfornew' ); } @@ -385,14 +488,20 @@ class LoginForm extends SpecialPage { } elseif ( $creationBlock instanceof Block ) { // Throws an ErrorPageError. $this->userBlockedMessage( $creationBlock ); + // This should never be reached. return false; } # Include checks that will include GlobalBlocking (Bug 38333) - $permErrors = $this->getTitle()->getUserPermissionsErrors( 'createaccount', $currentUser, true ); + $permErrors = $this->getPageTitle()->getUserPermissionsErrors( + 'createaccount', + $currentUser, + true + ); + if ( count( $permErrors ) ) { - throw new PermissionsError( 'createaccount', $permErrors ); + throw new PermissionsError( 'createaccount', $permErrors ); } $ip = $this->getRequest()->getIP(); @@ -400,9 +509,20 @@ class LoginForm extends SpecialPage { return Status::newFatal( 'sorbs_create_account_reason' ); } + // Normalize the name so that silly things don't cause "invalid username" + // errors. User::newFromName does some rather strict checking, rejecting + // e.g. leading/trailing/multiple spaces. But first we need to reject + // usernames that would be treated as titles with a fragment part. + if ( strpos( $this->mUsername, '#' ) !== false ) { + return Status::newFatal( 'noname' ); + } + $title = Title::makeTitleSafe( NS_USER, $this->mUsername ); + if ( !is_object( $title ) ) { + return Status::newFatal( 'noname' ); + } + # Now create a dummy user ($u) and check if it is valid - $name = trim( $this->mUsername ); - $u = User::newFromName( $name, 'creatable' ); + $u = User::newFromName( $title->getText(), 'creatable' ); if ( !is_object( $u ) ) { return Status::newFatal( 'noname' ); } elseif ( 0 != $u->idForName() ) { @@ -424,6 +544,7 @@ class LoginForm extends SpecialPage { if ( !is_array( $valid ) ) { $valid = array( $valid, $wgMinimalPasswordLength ); } + return call_user_func_array( 'Status::newFatal', $valid ); } } @@ -444,17 +565,30 @@ class LoginForm extends SpecialPage { $u->setRealName( $this->mRealName ); $abortError = ''; - if ( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) { + $abortStatus = null; + if ( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError, &$abortStatus ) ) ) { // Hook point to add extra creation throttles and blocks wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" ); - $abortError = new RawMessage( $abortError ); - $abortError->text(); - return Status::newFatal( $abortError ); + if ( $abortStatus === null ) { + // Report back the old string as a raw message status. + // This will report the error back as 'createaccount-hook-aborted' + // with the given string as the message. + // To return a different error code, return a Status object. + $abortError = new Message( 'createaccount-hook-aborted', array( $abortError ) ); + $abortError->text(); + + return Status::newFatal( $abortError ); + } else { + // For MediaWiki 1.23+ and updated hooks, return the Status object + // returned from the hook. + return $abortStatus; + } } // Hook point to check for exempt from account creation throttle if ( !wfRunHooks( 'ExemptFromAccountCreationThrottle', array( $ip ) ) ) { - wfDebug( "LoginForm::exemptFromAccountCreationThrottle: a hook allowed account creation w/o throttle\n" ); + wfDebug( "LoginForm::exemptFromAccountCreationThrottle: a hook " . + "allowed account creation w/o throttle\n" ); } else { if ( ( $wgAccountCreationThrottle && $currentUser->isPingLimitable() ) ) { $key = wfMemcKey( 'acctcreate', 'ip', $ip ); @@ -474,6 +608,7 @@ class LoginForm extends SpecialPage { } self::clearCreateaccountToken(); + return $this->initUser( $u, false ); } @@ -481,9 +616,9 @@ class LoginForm extends SpecialPage { * Actually add a user to the database. * Give it a User object that has been initialised with a name. * - * @param $u User object. - * @param $autocreate boolean -- true if this is an autocreation via auth plugin - * @return Status object, with the User object in the value member on success + * @param User $u + * @param bool $autocreate True if this is an autocreation via auth plugin + * @return Status Status object, with the User object in the value member on success * @private */ function initUser( $u, $autocreate ) { @@ -504,12 +639,14 @@ class LoginForm extends SpecialPage { $wgAuth->initUser( $u, $autocreate ); - $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 ); $u->saveSettings(); - # Update user count + // Update user count DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 0, 0, 0, 1 ) ); + // Watch user's userpage and talk page + $u->addWatch( $u->getUserPage(), WatchedItem::IGNORE_USER_RIGHTS ); + return Status::newGood( $u ); } @@ -538,6 +675,7 @@ class LoginForm extends SpecialPage { // If the user doesn't have a login token yet, set one. if ( !self::getLoginToken() ) { self::setLoginToken(); + return self::NEED_TOKEN; } // If the user didn't pass a login token, tell them we need one @@ -563,10 +701,19 @@ class LoginForm extends SpecialPage { // will effectively be using stale data. if ( $this->getUser()->getName() === $this->mUsername ) { wfDebug( __METHOD__ . ": already logged in as {$this->mUsername}\n" ); + return self::SUCCESS; } $u = User::newFromName( $this->mUsername ); + + // Give extensions a way to indicate the username has been updated, + // rather than telling the user the account doesn't exist. + if ( !wfRunHooks( 'LoginUserMigrated', array( $u, &$msg ) ) ) { + $this->mAbortLoginErrorMsg = $msg; + return self::USER_MIGRATED; + } + if ( !( $u instanceof User ) || !User::isUsableName( $u->getName() ) ) { return self::ILLEGAL; } @@ -588,6 +735,7 @@ class LoginForm extends SpecialPage { $msg = null; if ( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort, &$msg ) ) ) { $this->mAbortLoginErrorMsg = $msg; + return $abort; } @@ -618,6 +766,8 @@ class LoginForm extends SpecialPage { // At this point we just return an appropriate code/ indicating // that the UI should show a password reset form; bot inter- // faces etc will probably just fail cleanly here. + $this->mAbortLoginErrorMsg = 'resetpass-temp-emailed'; + $this->mTempPasswordUsed = true; $retval = self::RESET_PASS; } else { $retval = ( $this->mPassword == '' ) ? self::EMPTY_PASS : self::WRONG_PASS; @@ -625,6 +775,10 @@ class LoginForm extends SpecialPage { } elseif ( $wgBlockDisablesLogin && $u->isBlocked() ) { // If we've enabled it, make it so that a blocked user cannot login $retval = self::USER_BLOCKED; + } elseif ( $u->getPasswordExpired() == 'hard' ) { + // Force reset now, without logging in + $retval = self::RESET_PASS; + $this->mAbortLoginErrorMsg = 'resetpass-expired'; } else { $wgAuth->updateUser( $u ); $wgUser = $u; @@ -646,6 +800,7 @@ class LoginForm extends SpecialPage { $retval = self::SUCCESS; } wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) ); + return $retval; } @@ -653,7 +808,7 @@ class LoginForm extends SpecialPage { * Increment the login attempt throttle hit count for the (username,current IP) * tuple unless the throttle was already reached. * @param string $username The user name - * @return Bool|Integer The integer hit count or True if it is already at the limit + * @return bool|int The integer hit count or True if it is already at the limit */ public static function incLoginThrottle( $username ) { global $wgPasswordAttemptThrottle, $wgMemc, $wgRequest; @@ -695,26 +850,32 @@ class LoginForm extends SpecialPage { * Attempt to automatically create a user on login. Only succeeds if there * is an external authentication method which allows it. * - * @param $user User + * @param User $user * - * @return integer Status code + * @return int Status code */ function attemptAutoCreate( $user ) { global $wgAuth; if ( $this->getUser()->isBlockedFromCreateAccount() ) { wfDebug( __METHOD__ . ": user is blocked from account creation\n" ); + return self::CREATE_BLOCKED; } + if ( !$wgAuth->autoCreate() ) { return self::NOT_EXISTS; } + if ( !$wgAuth->userExists( $user->getName() ) ) { wfDebug( __METHOD__ . ": user does not exist\n" ); + return self::NOT_EXISTS; } + if ( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) { wfDebug( __METHOD__ . ": \$wgAuth->authenticate() returned false, aborting\n" ); + return self::WRONG_PLUGIN_PASS; } @@ -723,6 +884,7 @@ class LoginForm extends SpecialPage { // Hook point to add extra creation throttles and blocks wfDebug( "LoginForm::attemptAutoCreate: a hook blocked creation: $abortError\n" ); $this->mAbortLoginErrorMsg = $abortError; + return self::ABORTED; } @@ -732,6 +894,7 @@ class LoginForm extends SpecialPage { if ( !$status->isOK() ) { $errors = $status->getErrorsByType( 'error' ); $this->mAbortLoginErrorMsg = $errors[0]['message']; + return self::ABORTED; } @@ -739,27 +902,23 @@ class LoginForm extends SpecialPage { } function processLogin() { - global $wgMemc, $wgLang, $wgSecureLogin, $wgPasswordAttemptThrottle; + global $wgMemc, $wgLang, $wgSecureLogin, $wgPasswordAttemptThrottle, + $wgInvalidPasswordReset; switch ( $this->authenticateUserData() ) { case self::SUCCESS: # We've verified now, update the real record $user = $this->getUser(); - if ( (bool)$this->mRemember != $user->getBoolOption( 'rememberpassword' ) ) { - $user->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 ); - $user->saveSettings(); - } else { - $user->invalidateCache(); - } + $user->invalidateCache(); if ( $user->requiresHTTPS() ) { $this->mStickHTTPS = true; } if ( $wgSecureLogin && !$this->mStickHTTPS ) { - $user->setCookies( null, false ); + $user->setCookies( $this->mRequest, false, $this->mRemember ); } else { - $user->setCookies(); + $user->setCookies( $this->mRequest, null, $this->mRemember ); } self::clearLoginToken(); @@ -778,7 +937,18 @@ class LoginForm extends SpecialPage { $this->getContext()->setLanguage( $userLang ); // Reset SessionID on Successful login (bug 40995) $this->renewSessionId(); - $this->successfulLogin(); + if ( $this->getUser()->getPasswordExpired() == 'soft' ) { + $this->resetLoginForm( $this->msg( 'resetpass-expired-soft' ) ); + } elseif ( $wgInvalidPasswordReset + && !$user->isValidPassword( $this->mPassword ) + ) { + $status = $user->checkPasswordValidity( $this->mPassword ); + $this->resetLoginForm( + $status->getMessage( 'resetpass-validity-soft' ) + ); + } else { + $this->successfulLogin(); + } } else { $this->cookieRedirectCheck( 'login' ); } @@ -822,7 +992,7 @@ class LoginForm extends SpecialPage { break; case self::RESET_PASS: $error = $this->mAbortLoginErrorMsg ?: 'resetpass_announce'; - $this->resetLoginForm( $this->msg( $error )->text() ); + $this->resetLoginForm( $this->msg( $error ) ); break; case self::CREATE_BLOCKED: $this->userBlockedMessage( $this->getUser()->isBlockedFromCreateAccount() ); @@ -830,8 +1000,8 @@ class LoginForm extends SpecialPage { case self::THROTTLED: $error = $this->mAbortLoginErrorMsg ?: 'login-throttled'; $this->mainLoginForm( $this->msg( $error ) - ->params ( $this->getLanguage()->formatDuration( $wgPasswordAttemptThrottle['seconds'] ) ) - ->text() + ->params( $this->getLanguage()->formatDuration( $wgPasswordAttemptThrottle['seconds'] ) ) + ->text() ); break; case self::USER_BLOCKED: @@ -840,7 +1010,17 @@ class LoginForm extends SpecialPage { break; case self::ABORTED: $error = $this->mAbortLoginErrorMsg ?: 'login-abort-generic'; - $this->mainLoginForm( $this->msg( $error )->text() ); + $this->mainLoginForm( $this->msg( $error, + wfEscapeWikiText( $this->mUsername ) )->text() ); + break; + case self::USER_MIGRATED: + $error = $this->mAbortLoginErrorMsg ?: 'login-migrated-generic'; + $params = array(); + if ( is_array( $error ) ) { + $error = array_shift( $this->mAbortLoginErrorMsg ); + $params = $this->mAbortLoginErrorMsg; + } + $this->mainLoginForm( $this->msg( $error, $params )->text() ); break; default: throw new MWException( 'Unhandled case value' ); @@ -848,24 +1028,34 @@ class LoginForm extends SpecialPage { } /** - * @param $error string + * Show the Special:ChangePassword form, with custom message + * @param Message $msg */ - function resetLoginForm( $error ) { - $this->getOutput()->addHTML( Xml::element( 'p', array( 'class' => 'error' ), $error ) ); + protected function resetLoginForm( Message $msg ) { + // Allow hooks to explain this password reset in more detail + wfRunHooks( 'LoginPasswordResetMessage', array( &$msg, $this->mUsername ) ); $reset = new SpecialChangePassword(); - $reset->setContext( $this->getContext() ); + $derivative = new DerivativeContext( $this->getContext() ); + $derivative->setTitle( $reset->getPageTitle() ); + $reset->setContext( $derivative ); + if ( !$this->mTempPasswordUsed ) { + $reset->setOldPasswordMessage( 'oldpassword' ); + } + $reset->setChangeMessage( $msg ); $reset->execute( null ); } /** - * @param $u User object - * @param $throttle Boolean - * @param string $emailTitle message name of email title - * @param string $emailText message name of email text - * @return Status object + * @param User $u + * @param bool $throttle + * @param string $emailTitle Message name of email title + * @param string $emailText Message name of email text + * @return Status */ - function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) { - global $wgCanonicalServer, $wgScript, $wgNewPasswordExpiry; + function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', + $emailText = 'passwordremindertext' + ) { + global $wgNewPasswordExpiry; if ( $u->getEmail() == '' ) { return Status::newFatal( 'noemail', $u->getName() ); @@ -882,7 +1072,11 @@ class LoginForm extends SpecialPage { $u->setNewpassword( $np, $throttle ); $u->saveSettings(); $userLanguage = $u->getOption( 'language' ); - $m = $this->msg( $emailText, $ip, $u->getName(), $np, '<' . $wgCanonicalServer . $wgScript . '>', + + $mainPage = Title::newMainPage(); + $mainPageUrl = $mainPage->getCanonicalURL(); + + $m = $this->msg( $emailText, $ip, $u->getName(), $np, '<' . $mainPageUrl . '>', round( $wgNewPasswordExpiry / 86400 ) )->inLanguage( $userLanguage )->text(); $result = $u->sendMail( $this->msg( $emailTitle )->inLanguage( $userLanguage )->text(), $m ); @@ -906,7 +1100,7 @@ class LoginForm extends SpecialPage { wfRunHooks( 'UserLoginComplete', array( &$currentUser, &$injected_html ) ); if ( $injected_html !== '' ) { - $this->displaySuccessfulAction( $this->msg( 'loginsuccesstitle' ), + $this->displaySuccessfulAction( 'success', $this->msg( 'loginsuccesstitle' ), 'loginsuccess', $injected_html ); } else { $this->executeReturnTo( 'successredirect' ); @@ -934,18 +1128,22 @@ class LoginForm extends SpecialPage { */ wfRunHooks( 'BeforeWelcomeCreation', array( &$welcome_creation_msg, &$injected_html ) ); - $this->displaySuccessfulAction( $this->msg( 'welcomeuser', $this->getUser()->getName() ), - $welcome_creation_msg, $injected_html ); + $this->displaySuccessfulAction( + 'signup', + $this->msg( 'welcomeuser', $this->getUser()->getName() ), + $welcome_creation_msg, $injected_html + ); } /** - * Display an "successful action" page. + * Display a "successful action" page. * - * @param string|Message $title page's title - * @param $msgname string - * @param $injected_html string + * @param string $type Condition of return to; see `executeReturnTo` + * @param string|Message $title Page's title + * @param string $msgname + * @param string $injected_html */ - private function displaySuccessfulAction( $title, $msgname, $injected_html ) { + private function displaySuccessfulAction( $type, $title, $msgname, $injected_html ) { $out = $this->getOutput(); $out->setPageTitle( $title ); if ( $msgname ) { @@ -954,7 +1152,7 @@ class LoginForm extends SpecialPage { $out->addHTML( $injected_html ); - $this->executeReturnTo( 'success' ); + $this->executeReturnTo( $type ); } /** @@ -962,7 +1160,7 @@ class LoginForm extends SpecialPage { * there is a block on them or their IP which prevents account creation. Note that * User::isBlockedFromCreateAccount(), which gets this block, ignores the 'hardblock' * setting on blocks (bug 13611). - * @param $block Block the block causing this error + * @param Block $block The block causing this error * @throws ErrorPageError */ function userBlockedMessage( Block $block ) { @@ -973,14 +1171,23 @@ class LoginForm extends SpecialPage { # haven't bothered to log out before trying to create an account to # evade it, but we'll leave that to their guilty conscience to figure # out. + $errorParams = array( + $block->getTarget(), + $block->mReason ? $block->mReason : $this->msg( 'blockednoreason' )->text(), + $block->getByName() + ); + + if ( $block->getType() === Block::TYPE_RANGE ) { + $errorMessage = 'cantcreateaccount-range-text'; + $errorParams[] = $this->getRequest()->getIP(); + } else { + $errorMessage = 'cantcreateaccount-text'; + } + throw new ErrorPageError( 'cantcreateaccounttitle', - 'cantcreateaccount-text', - array( - $block->getTarget(), - $block->mReason ? $block->mReason : $this->msg( 'blockednoreason' )->text(), - $block->getByName() - ) + $errorMessage, + $errorParams ); } @@ -989,8 +1196,9 @@ class LoginForm extends SpecialPage { * Extensions can use this to reuse the "return to" logic after * inject steps (such as redirection) into the login process. * - * @param $type string, one of the following: + * @param string $type One of the following: * - error: display a return to link ignoring $wgRedirectOnLogin + * - signup: display a return to link using $wgRedirectOnLogin if needed * - success: display a return to link using $wgRedirectOnLogin if needed * - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed * @param string $returnTo @@ -1010,8 +1218,9 @@ class LoginForm extends SpecialPage { /** * Add a "return to" link or redirect to it. * - * @param $type string, one of the following: + * @param string $type One of the following: * - error: display a return to link ignoring $wgRedirectOnLogin + * - signup: display a return to link using $wgRedirectOnLogin if needed * - success: display a return to link using $wgRedirectOnLogin if needed * - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed */ @@ -1026,6 +1235,9 @@ class LoginForm extends SpecialPage { $returnToQuery = wfCgiToArray( $this->mReturnToQuery ); } + // Allow modification of redirect behavior + wfRunHooks( 'PostLoginRedirect', array( &$returnTo, &$returnToQuery, &$type ) ); + $returnToTitle = Title::newFromText( $returnTo ); if ( !$returnToTitle ) { $returnToTitle = Title::newMainPage(); @@ -1051,6 +1263,8 @@ class LoginForm extends SpecialPage { } /** + * @param string $msg + * @param string $msgtype * @private */ function mainLoginForm( $msg, $msgtype = 'error' ) { @@ -1059,7 +1273,7 @@ class LoginForm extends SpecialPage { global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration; global $wgSecureLogin, $wgPasswordResetRoutes; - $titleObj = $this->getTitle(); + $titleObj = $this->getPageTitle(); $user = $this->getUser(); $out = $this->getOutput(); @@ -1072,6 +1286,7 @@ class LoginForm extends SpecialPage { throw new PermissionsError( 'createaccount', $permErrors ); } elseif ( $user->isBlockedFromCreateAccount() ) { $this->userBlockedMessage( $user->isBlockedFromCreateAccount() ); + return; } elseif ( wfReadOnly() ) { throw new ReadOnlyError; @@ -1087,33 +1302,47 @@ class LoginForm extends SpecialPage { } } - if ( $this->mType == 'signup' ) { - $template = new UsercreateTemplate(); + // Generic styles and scripts for both login and signup form + $out->addModuleStyles( array( + 'mediawiki.ui', + 'mediawiki.ui.button', + 'mediawiki.ui.checkbox', + 'mediawiki.ui.input', + 'mediawiki.special.userlogin.common.styles' + ) ); + $out->addModules( array( + 'mediawiki.special.userlogin.common.js' + ) ); - $out->addModuleStyles( array( - 'mediawiki.ui', - 'mediawiki.special.createaccount' - ) ); + if ( $this->mType == 'signup' ) { // XXX hack pending RL or JS parse() support for complex content messages // https://bugzilla.wikimedia.org/show_bug.cgi?id=25349 $out->addJsConfigVars( 'wgCreateacctImgcaptchaHelp', $this->msg( 'createacct-imgcaptcha-help' )->parse() ); + + // Additional styles and scripts for signup form $out->addModules( array( - 'mediawiki.special.createaccount.js' + 'mediawiki.special.userlogin.signup.js' ) ); + $out->addModuleStyles( array( + 'mediawiki.special.userlogin.signup.styles' + ) ); + + $template = new UsercreateTemplate(); + // Must match number of benefits defined in messages $template->set( 'benefitCount', 3 ); $q = 'action=submitlogin&type=signup'; $linkq = 'type=login'; } else { - $template = new UserloginTemplate(); - + // Additional styles for login form $out->addModuleStyles( array( - 'mediawiki.ui', - 'mediawiki.special.userlogin' + 'mediawiki.special.userlogin.login.styles' ) ); + $template = new UserloginTemplate(); + $q = 'action=submitlogin&type=login'; $linkq = 'type=signup'; } @@ -1167,7 +1396,7 @@ class LoginForm extends SpecialPage { $template->set( 'resetlink', $resetLink ); $template->set( 'canremember', ( $wgCookieExpiration > 0 ) ); $template->set( 'usereason', $user->isLoggedIn() ); - $template->set( 'remember', $user->getOption( 'rememberpassword' ) || $this->mRemember ); + $template->set( 'remember', $this->mRemember ); $template->set( 'cansecurelogin', ( $wgSecureLogin === true ) ); $template->set( 'stickhttps', (int)$this->mStickHTTPS ); $template->set( 'loggedin', $user->isLoggedIn() ); @@ -1196,7 +1425,7 @@ class LoginForm extends SpecialPage { $template->set( 'secureLoginUrl', $this->mSecureLoginUrl ); // Use loginend-https for HTTPS requests if it's not blank, loginend otherwise // Ditto for signupend. New forms use neither. - $usingHTTPS = WebRequest::detectProtocol() == 'https'; + $usingHTTPS = $this->mRequest->getProtocol() == 'https'; $loginendHTTPS = $this->msg( 'loginend-https' ); $signupendHTTPS = $this->msg( 'signupend-https' ); if ( $usingHTTPS && !$loginendHTTPS->isBlank() ) { @@ -1226,7 +1455,7 @@ class LoginForm extends SpecialPage { * Whether the login/create account form should display a link to the * other form (in addition to whatever the skin provides). * - * @param $user User + * @param User $user * @return bool */ private function showCreateOrLoginLink( &$user ) { @@ -1251,15 +1480,17 @@ class LoginForm extends SpecialPage { */ function hasSessionCookie() { global $wgDisableCookieCheck; + return $wgDisableCookieCheck ? true : $this->getRequest()->checkSessionCookie(); } /** * Get the login token from the current session - * @return Mixed + * @return mixed */ public static function getLoginToken() { global $wgRequest; + return $wgRequest->getSessionData( 'wsLoginToken' ); } @@ -1268,7 +1499,7 @@ class LoginForm extends SpecialPage { */ public static function setLoginToken() { global $wgRequest; - // Generate a token directly instead of using $user->editToken() + // Generate a token directly instead of using $user->getEditToken() // because the latter reuses $_SESSION['wsEditToken'] $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32 ) ); } @@ -1283,10 +1514,11 @@ class LoginForm extends SpecialPage { /** * Get the createaccount token from the current session - * @return Mixed + * @return mixed */ public static function getCreateaccountToken() { global $wgRequest; + return $wgRequest->getSessionData( 'wsCreateaccountToken' ); } @@ -1319,6 +1551,7 @@ class LoginForm extends SpecialPage { } /** + * @param string $type * @private */ function cookieRedirectCheck( $type ) { @@ -1334,6 +1567,7 @@ class LoginForm extends SpecialPage { } /** + * @param string $type * @private */ function onCookieRedirectCheck( $type ) { @@ -1369,6 +1603,7 @@ class LoginForm extends SpecialPage { $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) ); } } + return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams( $this->getLanguage()->pipeList( $links ) )->escaped() : ''; } else { @@ -1403,7 +1638,7 @@ class LoginForm extends SpecialPage { $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode(); return Linker::linkKnown( - $this->getTitle(), + $this->getPageTitle(), htmlspecialchars( $text ), $attr, $query diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php index d957e875..d65ac852 100644 --- a/includes/specials/SpecialUserlogout.php +++ b/includes/specials/SpecialUserlogout.php @@ -27,7 +27,6 @@ * @ingroup SpecialPage */ class SpecialUserlogout extends UnlistedSpecialPage { - function __construct() { parent::__construct( 'Userlogout' ); } diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index 4501736f..cefdad07 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -55,6 +55,7 @@ class UserrightsPage extends SpecialPage { if ( $user->getId() == 0 ) { return false; } + return !empty( $available['add'] ) || !empty( $available['remove'] ) || ( ( $this->isself || !$checkIfSelf ) && @@ -66,7 +67,7 @@ class UserrightsPage extends SpecialPage { * Manage forms to be shown according to posted data. * Depending on the submit button used, call a form or a save function. * - * @param $par Mixed: string if any subpage provided, else null + * @param string|null $par String if any subpage provided, else null * @throws UserBlockedError|PermissionsError */ public function execute( $par ) { @@ -118,6 +119,7 @@ class UserrightsPage extends SpecialPage { $out = $this->getOutput(); $out->wrapWikiMsg( "
        \n$1\n
        ", 'userrights-removed-self' ); $out->returnToMain(); + return; } @@ -148,12 +150,18 @@ class UserrightsPage extends SpecialPage { $status = $this->fetchUser( $this->mTarget ); if ( !$status->isOK() ) { $this->getOutput()->addWikiText( $status->getWikiText() ); + return; } $targetUser = $status->value; + if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (bug 61252) + $targetUser->clearInstanceCache(); // bug 38989 + } - if ( $request->getVal( 'conflictcheck-originalgroups' ) !== implode( ',', $targetUser->getGroups() ) ) { + if ( $request->getVal( 'conflictcheck-originalgroups' ) + !== implode( ',', $targetUser->getGroups() ) + ) { $out->addWikiMsg( 'userrights-conflict' ); } else { $this->saveUserGroups( @@ -163,6 +171,7 @@ class UserrightsPage extends SpecialPage { ); $out->redirect( $this->getSuccessURL() ); + return; } } @@ -174,15 +183,15 @@ class UserrightsPage extends SpecialPage { } function getSuccessURL() { - return $this->getTitle( $this->mTarget )->getFullURL( array( 'success' => 1 ) ); + return $this->getPageTitle( $this->mTarget )->getFullURL( array( 'success' => 1 ) ); } /** * Save user groups changes in the database. * Data comes from the editUserGroupsForm() form function * - * @param string $username username to apply changes to. - * @param string $reason reason for group change + * @param string $username Username to apply changes to. + * @param string $reason Reason for group change * @param User|UserRightsProxy $user Target user object. * @return null */ @@ -209,11 +218,11 @@ class UserrightsPage extends SpecialPage { /** * Save user groups changes in the database. * - * @param $user User object - * @param array $add of groups to add - * @param array $remove of groups to remove - * @param string $reason reason for group change - * @return Array: Tuple of added, then removed groups + * @param User $user + * @param array $add Array of groups to add + * @param array $remove Array of groups to remove + * @param string $reason Reason for group change + * @return array Tuple of added, then removed groups */ function doSaveUserGroups( $user, $add, $remove, $reason = '' ) { global $wgAuth; @@ -256,18 +265,23 @@ class UserrightsPage extends SpecialPage { // update groups in external authentication database $wgAuth->updateExternalDBGroups( $user, $add, $remove ); - wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) ); - wfDebug( 'newGroups: ' . print_r( $newGroups, true ) ); + wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) . "\n" ); + wfDebug( 'newGroups: ' . print_r( $newGroups, true ) . "\n" ); wfRunHooks( 'UserRights', array( &$user, $add, $remove ) ); if ( $newGroups != $oldGroups ) { $this->addLogEntry( $user, $oldGroups, $newGroups, $reason ); } + return array( $add, $remove ); } /** * Add a rights log entry for an action. + * @param User $user + * @param array $oldGroups + * @param array $newGroups + * @param array $reason */ function addLogEntry( $user, $oldGroups, $newGroups, $reason ) { $logEntry = new ManualLogEntry( 'rights', 'rights' ); @@ -284,12 +298,13 @@ class UserrightsPage extends SpecialPage { /** * Edit user groups membership - * @param string $username name of the user. + * @param string $username Name of the user. */ function editUserGroupsForm( $username ) { $status = $this->fetchUser( $username ); if ( !$status->isOK() ) { $this->getOutput()->addWikiText( $status->getWikiText() ); + return; } else { $user = $status->value; @@ -310,12 +325,10 @@ class UserrightsPage extends SpecialPage { * * Side effects: error output for invalid access * @param string $username - * @return Status object + * @return Status */ public function fetchUser( $username ) { - global $wgUserrightsInterwikiDelimiter; - - $parts = explode( $wgUserrightsInterwikiDelimiter, $username ); + $parts = explode( $this->getConfig()->get( 'UserrightsInterwikiDelimiter' ), $username ); if ( count( $parts ) < 2 ) { $name = trim( $username ); $database = ''; @@ -384,8 +397,8 @@ class UserrightsPage extends SpecialPage { /** * Make a list of group names to be stored as parameter for log entries * - * @deprecated in 1.21; use LogFormatter instead. - * @param $ids array + * @deprecated since 1.21; use LogFormatter instead. + * @param array $ids * @return string */ function makeGroupNameListForLog( $ids ) { @@ -402,12 +415,26 @@ class UserrightsPage extends SpecialPage { * Output a form to allow searching for a user */ function switchForm() { - global $wgScript; $this->getOutput()->addHTML( - Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser', 'id' => 'mw-userrights-form1' ) ) . - Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . + Html::openElement( + 'form', + array( + 'method' => 'get', + 'action' => wfScript(), + 'name' => 'uluser', + 'id' => 'mw-userrights-form1' + ) + ) . + Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) . Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) . - Xml::inputLabel( $this->msg( 'userrights-user-editname' )->text(), 'user', 'username', 30, str_replace( '_', ' ', $this->mTarget ), array( 'autofocus' => true ) ) . ' ' . + Xml::inputLabel( + $this->msg( 'userrights-user-editname' )->text(), + 'user', + 'username', + 30, + str_replace( '_', ' ', $this->mTarget ), + array( 'autofocus' => true ) + ) . ' ' . Xml::submitButton( $this->msg( 'editusergroup' )->text() ) . Html::closeElement( 'fieldset' ) . Html::closeElement( 'form' ) . "\n" @@ -419,8 +446,8 @@ class UserrightsPage extends SpecialPage { * form will be able to manipulate based on the current user's system * permissions. * - * @param array $groups list of groups the given user is in - * @return Array: Tuple of addable, then removable groups + * @param array $groups List of groups the given user is in + * @return array Tuple of addable, then removable groups */ protected function splitGroups( $groups ) { list( $addable, $removable, $addself, $removeself ) = array_values( $this->changeableGroups() ); @@ -440,8 +467,8 @@ class UserrightsPage extends SpecialPage { /** * Show the form to edit group memberships. * - * @param $user User or UserRightsProxy you're editing - * @param $groups Array: Array of groups the user is in + * @param User|UserRightsProxy $user User or UserRightsProxy you're editing + * @param array $groups Array of groups the user is in */ protected function showEditUserGroupsForm( $user, $groups ) { $list = array(); @@ -476,30 +503,48 @@ class UserrightsPage extends SpecialPage { $grouplist = $this->msg( 'userrights-groupsmember', $count, $user->getName() )->parse(); $grouplist = '

        ' . $grouplist . ' ' . $displayedList . "

        \n"; } + $count = count( $autoList ); if ( $count > 0 ) { - $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto', $count, $user->getName() )->parse(); + $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto', $count, $user->getName() ) + ->parse(); $grouplist .= '

        ' . $autogrouplistintro . ' ' . $displayedAutolist . "

        \n"; } $userToolLinks = Linker::userToolLinks( - $user->getId(), - $user->getName(), - false, /* default for redContribsWhenNoEdits */ - Linker::TOOL_LINKS_EMAIL /* Add "send e-mail" link */ + $user->getId(), + $user->getName(), + false, /* default for redContribsWhenNoEdits */ + Linker::TOOL_LINKS_EMAIL /* Add "send e-mail" link */ ); $this->getOutput()->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL(), 'name' => 'editGroup', 'id' => 'mw-userrights-form2' ) ) . + Xml::openElement( + 'form', + array( + 'method' => 'post', + 'action' => $this->getPageTitle()->getLocalURL(), + 'name' => 'editGroup', + 'id' => 'mw-userrights-form2' + ) + ) . Html::hidden( 'user', $this->mTarget ) . Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) . - Html::hidden( 'conflictcheck-originalgroups', implode( ',', $user->getGroups() ) ) . // Conflict detection + Html::hidden( + 'conflictcheck-originalgroups', + implode( ',', $user->getGroups() ) + ) . // Conflict detection Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', array(), $this->msg( 'userrights-editusergroup', $user->getName() )->text() ) . - $this->msg( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )->rawParams( $userToolLinks )->parse() . + Xml::element( + 'legend', + array(), + $this->msg( 'userrights-editusergroup', $user->getName() )->text() + ) . + $this->msg( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) ) + ->rawParams( $userToolLinks )->parse() . $this->msg( 'userrights-groups-help', $user->getName() )->parse() . $grouplist . - Xml::tags( 'p', null, $this->groupCheckboxes( $groups, $user ) ) . + $this->groupCheckboxes( $groups, $user ) . Xml::openElement( 'table', array( 'id' => 'mw-userrights-table-outer' ) ) . " " . @@ -514,7 +559,9 @@ class UserrightsPage extends SpecialPage { " . Xml::submitButton( $this->msg( 'saveusergroups' )->text(), - array( 'name' => 'saveusergroups' ) + Linker::tooltipAndAccesskeyAttribs( 'userrights-set' ) ) . + array( 'name' => 'saveusergroups' ) + + Linker::tooltipAndAccesskeyAttribs( 'userrights-set' ) + ) . " " . Xml::closeElement( 'table' ) . "\n" . @@ -526,7 +573,7 @@ class UserrightsPage extends SpecialPage { /** * Format a link to a group description page * - * @param $group string + * @param string $group * @return string */ private static function buildGroupLink( $group ) { @@ -536,7 +583,7 @@ class UserrightsPage extends SpecialPage { /** * Format a link to a group member description page * - * @param $group string + * @param string $group * @return string */ private static function buildGroupMemberLink( $group ) { @@ -555,8 +602,8 @@ class UserrightsPage extends SpecialPage { * Adds a table with checkboxes where you can select what groups to add/remove * * @todo Just pass the username string? - * @param array $usergroups groups the user belongs to - * @param $user User a user object + * @param array $usergroups Groups the user belongs to + * @param User $user * @return string XHTML table element with checkboxes */ private function groupCheckboxes( $usergroups, $user ) { @@ -599,8 +646,13 @@ class UserrightsPage extends SpecialPage { continue; } // Messages: userrights-changeable-col, userrights-unchangeable-col - $ret .= Xml::element( 'th', null, $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text() ); + $ret .= Xml::element( + 'th', + null, + $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text() + ); } + $ret .= "\n\n"; foreach ( $columns as $column ) { if ( $column === array() ) { @@ -631,28 +683,41 @@ class UserrightsPage extends SpecialPage { } /** - * @param $group String: the name of the group to check + * @param string $group The name of the group to check * @return bool Can we remove the group? */ private function canRemove( $group ) { // $this->changeableGroups()['remove'] doesn't work, of course. Thanks, PHP. $groups = $this->changeableGroups(); - return in_array( $group, $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] ) ); + + return in_array( + $group, + $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] ) + ); } /** - * @param string $group the name of the group to check + * @param string $group The name of the group to check * @return bool Can we add the group? */ private function canAdd( $group ) { $groups = $this->changeableGroups(); - return in_array( $group, $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] ) ); + + return in_array( + $group, + $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] ) + ); } /** * Returns $this->getUser()->changeableGroups() * - * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ), 'add-self' => array( addablegroups to self ), 'remove-self' => array( removable groups from self ) ) + * @return array Array( + * 'add' => array( addablegroups ), + * 'remove' => array( removablegroups ), + * 'add-self' => array( addablegroups to self ), + * 'remove-self' => array( removable groups from self ) + * ) */ function changeableGroups() { return $this->getUser()->changeableGroups(); @@ -661,8 +726,8 @@ class UserrightsPage extends SpecialPage { /** * Show a rights log fragment for the specified user * - * @param $user User to show log for - * @param $output OutputPage to use + * @param User $user User to show log for + * @param OutputPage $output OutputPage to use */ protected function showLogFragment( $user, $output ) { $rightsLogPage = new LogPage( 'rights' ); diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php index 5ba785f5..cb3fc118 100644 --- a/includes/specials/SpecialVersion.php +++ b/includes/specials/SpecialVersion.php @@ -29,9 +29,13 @@ * @ingroup SpecialPage */ class SpecialVersion extends SpecialPage { - protected $firstExtOpened = false; + /** + * Stores the current rev id/SHA hash of MediaWiki core + */ + protected $coreId = ''; + protected static $extensionTypes = false; protected static $viewvcUrls = array( @@ -46,43 +50,95 @@ class SpecialVersion extends SpecialPage { /** * main() + * @param string|null $par */ public function execute( $par ) { - global $wgSpecialVersionShowHooks, $IP; + global $IP, $wgExtensionCredits; $this->setHeaders(); $this->outputHeader(); $out = $this->getOutput(); $out->allowClickjacking(); - if ( $par !== 'Credits' ) { - $text = - $this->getMediaWikiCredits() . - $this->softwareInformation() . - $this->getEntryPointInfo() . - $this->getExtensionCredits(); - if ( $wgSpecialVersionShowHooks ) { - $text .= $this->getWgHooks(); + // Explode the sub page information into useful bits + $parts = explode( '/', (string)$par ); + $extNode = null; + if ( isset( $parts[1] ) ) { + $extName = str_replace( '_', ' ', $parts[1] ); + // Find it! + foreach ( $wgExtensionCredits as $group => $extensions ) { + foreach ( $extensions as $ext ) { + if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) { + $extNode = &$ext; + break 2; + } + } } - - $out->addWikiText( $text ); - $out->addHTML( $this->IPInfo() ); - - if ( $this->getRequest()->getVal( 'easteregg' ) ) { - // TODO: put something interesting here + if ( !$extNode ) { + $out->setStatusCode( 404 ); } } else { - // Credits sub page - - // Header - $out->addHTML( wfMessage( 'version-credits-summary' )->parseAsBlock() ); + $extName = 'MediaWiki'; + } - $wikiText = file_get_contents( $IP . '/CREDITS' ); + // Now figure out what to do + switch ( strtolower( $parts[0] ) ) { + case 'credits': + $wikiText = '{{int:version-credits-not-found}}'; + if ( $extName === 'MediaWiki' ) { + $wikiText = file_get_contents( $IP . '/CREDITS' ); + } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) { + $file = $this->getExtAuthorsFileName( dirname( $extNode['path'] ) ); + if ( $file ) { + $wikiText = file_get_contents( $file ); + if ( substr( $file, -4 ) === '.txt' ) { + $wikiText = Html::element( 'pre', array(), $wikiText ); + } + } + } - // Take everything from the first section onwards, to remove the (not localized) header - $wikiText = substr( $wikiText, strpos( $wikiText, '==' ) ); + $out->setPageTitle( $this->msg( 'version-credits-title', $extName ) ); + $out->addWikiText( $wikiText ); + break; + + case 'license': + $wikiText = '{{int:version-license-not-found}}'; + if ( $extName === 'MediaWiki' ) { + $wikiText = file_get_contents( $IP . '/COPYING' ); + } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) { + $file = $this->getExtLicenseFileName( dirname( $extNode['path'] ) ); + if ( $file ) { + $wikiText = file_get_contents( $file ); + if ( !isset( $extNode['license-name'] ) ) { + // If the developer did not explicitly set license-name they probably + // are unaware that we're now sucking this file in and thus it's probably + // not wikitext friendly. + $wikiText = "
        $wikiText
        "; + } + } + } - $out->addWikiText( $wikiText ); + $out->setPageTitle( $this->msg( 'version-license-title', $extName ) ); + $out->addWikiText( $wikiText ); + break; + + default: + $out->addModules( 'mediawiki.special.version' ); + $out->addWikiText( + $this->getMediaWikiCredits() . + $this->softwareInformation() . + $this->getEntryPointInfo() + ); + $out->addHtml( + $this->getSkinCredits() . + $this->getExtensionCredits() . + $this->getParserTags() . + $this->getParserFunctionHooks() + ); + $out->addWikiText( $this->getWgHooks() ); + $out->addHTML( $this->IPInfo() ); + + break; } } @@ -92,7 +148,11 @@ class SpecialVersion extends SpecialPage { * @return string */ private static function getMediaWikiCredits() { - $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMessage( 'version-license' )->text() ); + $ret = Xml::element( + 'h2', + array( 'id' => 'mw-version-license' ), + wfMessage( 'version-license' )->text() + ); // This text is always left-to-right. $ret .= '