diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2010-07-28 11:52:48 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2010-07-28 11:52:48 +0200 |
commit | 222b01f5169f1c7e69762e0e8904c24f78f71882 (patch) | |
tree | 8e932e12546bb991357ec48eb1638d1770be7a35 /includes/specials | |
parent | 00ab76a6b686e98a914afc1975812d2b1aaa7016 (diff) |
update to MediaWiki 1.16.0
Diffstat (limited to 'includes/specials')
76 files changed, 6182 insertions, 7439 deletions
diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php new file mode 100644 index 00000000..7d907fb5 --- /dev/null +++ b/includes/specials/SpecialActiveusers.php @@ -0,0 +1,195 @@ +<?php +# Copyright (C) 2008 Aaron Schulz +# +# http://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 +# 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 + +/** + * This class is used to get a list of active users. The ones with specials + * rights (sysop, bureaucrat, developer) will have them displayed + * next to their names. + * + * @file + * @ingroup SpecialPage + */ +class ActiveUsersPager extends UsersPager { + + function __construct( $group = null ) { + global $wgRequest, $wgRCMaxAge; + $this->RCMaxAge = ceil( $wgRCMaxAge / ( 3600 * 24 ) ); // Constant + + $un = $wgRequest->getText( 'username' ); + $this->requestedUser = ''; + if ( $un != '' ) { + $username = Title::makeTitleSafe( NS_USER, $un ); + if( !is_null( $username ) ) { + $this->requestedUser = $username->getText(); + } + } + + $this->setupOptions(); + + parent::__construct(); + } + + public function setupOptions() { + global $wgRequest; + + $this->opts = new FormOptions(); + + $this->opts->add( 'hidebots', false, FormOptions::BOOL ); + $this->opts->add( 'hidesysops', false, FormOptions::BOOL ); + + $this->opts->fetchValuesFromRequest( $wgRequest ); + + $this->groups = array(); + if ($this->opts->getValue('hidebots') == 1) + $this->groups['bot'] = true; + if ($this->opts->getValue('hidesysops') == 1) + $this->groups['sysop'] = true; + } + + function getIndexField() { + return 'rc_user_text'; + } + + function getQueryInfo() { + $dbr = wfGetDB( DB_SLAVE ); + $conds = array( 'rc_user > 0' ); // Users - no anons + $conds[] = 'ipb_deleted IS NULL'; // don't show hidden names + $conds[] = "rc_log_type IS NULL OR rc_log_type != 'newusers'"; + + if( $this->requestedUser != '' ) { + $conds[] = 'rc_user_text >= ' . $dbr->addQuotes( $this->requestedUser ); + } + + $query = array( + 'tables' => array( 'recentchanges', 'user', 'ipblocks' ), + 'fields' => array( 'rc_user_text AS user_name', // inheritance + 'rc_user_text', // for Pager + 'user_id', + 'COUNT(*) AS recentedits', + 'MAX(ipb_user) AS blocked' + ), + 'options' => array( + 'GROUP BY' => 'rc_user_text, user_id', + 'USE INDEX' => array( 'recentchanges' => 'rc_user_text' ) + ), + 'join_conds' => array( + 'user' => array( 'INNER JOIN', 'rc_user_text=user_name' ), + 'ipblocks' => array( 'LEFT JOIN', 'user_id=ipb_user AND ipb_auto=0 AND ipb_deleted=1' ), + ), + 'conds' => $conds + ); + return $query; + } + + function formatRow( $row ) { + global $wgLang; + $userName = $row->user_name; + + $ulinks = $this->getSkin()->userLink( $row->user_id, $userName ); + $ulinks .= $this->getSkin()->userToolLinks( $row->user_id, $userName ); + + $list = array(); + foreach( self::getGroups( $row->user_id ) as $group ) { + if (isset($this->groups[$group])) + return; + $list[] = self::buildGroupLink( $group ); + } + $groups = $wgLang->commaList( $list ); + + $item = wfSpecialList( $ulinks, $groups ); + $count = wfMsgExt( 'activeusers-count', + array( 'parsemag' ), + $wgLang->formatNum( $row->recentedits ), + $userName, + $wgLang->formatNum ( $this->RCMaxAge ) + ); + $blocked = $row->blocked ? ' ' . wfMsgExt( 'listusers-blocked', array( 'parsemag' ), $userName ) : ''; + + return Html::rawElement( 'li', array(), "{$item} [{$count}]{$blocked}" ); + } + + function getPageHeader() { + global $wgScript, $wgRequest; + + $self = $this->getTitle(); + $limit = $this->mLimit ? Xml::hidden( 'limit', $this->mLimit ) : ''; + + $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); # Form tag + $out .= Xml::fieldset( wfMsg( 'activeusers' ) ) . "\n"; + $out .= Xml::hidden( 'title', $self->getPrefixedDBkey() ) . $limit . "\n"; + + $out .= Xml::inputLabel( wfMsg( 'activeusers-from' ), 'username', 'offset', 20, $this->requestedUser ) . '<br />';# Username field + + $out .= Xml::checkLabel( wfMsg('activeusers-hidebots'), 'hidebots', 'hidebots', $this->opts->getValue( 'hidebots' ) ); + + $out .= Xml::checkLabel( wfMsg('activeusers-hidesysops'), 'hidesysops', 'hidesysops', $this->opts->getValue( 'hidesysops' ) ) . '<br />'; + + $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n";# Submit button and form bottom + $out .= Xml::closeElement( 'fieldset' ); + $out .= Xml::closeElement( 'form' ); + + return $out; + } +} + +/** + * @ingroup SpecialPage + */ +class SpecialActiveUsers extends SpecialPage { + + /** + * Constructor + */ + public function __construct() { + parent::__construct( 'Activeusers' ); + } + + /** + * Show the special page + * + * @param $par Mixed: parameter passed to the page or null + */ + public function execute( $par ) { + global $wgOut, $wgLang, $wgRCMaxAge; + + $this->setHeaders(); + + $up = new ActiveUsersPager(); + + # getBody() first to check, if empty + $usersbody = $up->getBody(); + + $s = Html::rawElement( 'div', array( 'class' => 'mw-activeusers-intro' ), + wfMsgExt( 'activeusers-intro', array( 'parsemag', 'escape' ), $wgLang->formatNum( ceil( $wgRCMaxAge / 86400 ) ) ) + ); + + $s .= $up->getPageHeader(); + if( $usersbody ) { + $s .= $up->getNavigationBar(); + $s .= Html::rawElement( 'ul', array(), $usersbody ); + $s .= $up->getNavigationBar(); + } else { + $s .= Html::element( 'p', array(), wfMsg( 'activeusers-noresult' ) ); + } + + $wgOut->addHTML( $s ); + } + +} diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php index 38181c08..1745bf6c 100644 --- a/includes/specials/SpecialAllmessages.php +++ b/includes/specials/SpecialAllmessages.php @@ -4,233 +4,414 @@ * @file * @ingroup SpecialPage */ +class SpecialAllmessages extends SpecialPage { -/** - * Constructor. - */ -function wfSpecialAllmessages() { - global $wgOut, $wgRequest, $wgMessageCache, $wgTitle; - global $wgUseDatabaseMessages, $wgLang; - - # The page isn't much use if the MediaWiki namespace is not being used - if( !$wgUseDatabaseMessages ) { - $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' ); - return; + /** + * Constructor + */ + public function __construct() { + parent::__construct( 'Allmessages' ); } - wfProfileIn( __METHOD__ ); + /** + * Show the special page + * + * @param $par Mixed: parameter passed to the page or null + */ + public function execute( $par ) { + global $wgOut, $wgRequest; - wfProfileIn( __METHOD__ . '-setup' ); - $ot = $wgRequest->getText( 'ot' ); + $this->setHeaders(); - $navText = wfMsg( 'allmessagestext' ); + global $wgUseDatabaseMessages; + if( !$wgUseDatabaseMessages ) { + $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' ); + return; + } else { + $this->outputHeader( 'allmessagestext' ); + } - # Make sure all extension messages are available + $this->filter = $wgRequest->getVal( 'filter', 'all' ); + $this->prefix = $wgRequest->getVal( 'prefix', '' ); - $wgMessageCache->loadAllMessages(); + $this->table = new AllmessagesTablePager( + $this, + $conds = array(), + wfGetLangObj( $wgRequest->getVal( 'lang', $par ) ) + ); - $sortedArray = array_merge( Language::getMessagesFor( 'en' ), - $wgMessageCache->getExtensionMessagesFor( 'en' ) ); - ksort( $sortedArray ); + $this->langCode = $this->table->lang->getCode(); + + $wgOut->addHTML( $this->buildForm() . + $this->table->getNavigationBar() . + $this->table->getLimitForm() . + $this->table->getBody() . + $this->table->getNavigationBar() ); - $messages = array(); - foreach( $sortedArray as $key => $value ) { - $messages[$key]['enmsg'] = $value; - $messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false ); - $messages[$key]['msg'] = wfMsgNoTrans( $key ); - $sortedArray[$key] = NULL; // trade bytes from $sortedArray to this - } - unset($sortedArray); // trade bytes from $sortedArray to this - - wfProfileOut( __METHOD__ . '-setup' ); - - wfProfileIn( __METHOD__ . '-output' ); - $wgOut->addScriptFile( 'allmessages.js' ); - if ( $ot == 'php' ) { - $navText .= wfAllMessagesMakePhp( $messages ); - $wgOut->addHTML( $wgLang->pipeList( array( - 'PHP', - '<a href="' . $wgTitle->escapeLocalUrl( 'ot=html' ) . '">HTML</a>', - '<a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' . - '<pre>' . htmlspecialchars( $navText ) . '</pre>' - ) ) ); - } else if ( $ot == 'xml' ) { - $wgOut->disable(); - header( 'Content-type: text/xml' ); - echo wfAllMessagesMakeXml( $messages ); - } else { - $wgOut->addHTML( $wgLang->pipeList( array( - '<a href="' . $wgTitle->escapeLocalUrl( 'ot=php' ) . '">PHP</a>', - 'HTML', - '<a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' - ) ) ); - $wgOut->addWikiText( $navText ); - $wgOut->addHTML( wfAllMessagesMakeHTMLText( $messages ) ); - } - wfProfileOut( __METHOD__ . '-output' ); - - wfProfileOut( __METHOD__ ); -} -function wfAllMessagesMakeXml( &$messages ) { - global $wgLang; - $lang = $wgLang->getCode(); - $txt = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"; - $txt .= "<messages lang=\"$lang\">\n"; - foreach( $messages as $key => $m ) { - $txt .= "\t" . Xml::element( 'message', array( 'name' => $key ), $m['msg'] ) . "\n"; - $messages[$key] = NULL; // trade bytes - } - $txt .= "</messages>"; - return $txt; -} + function buildForm() { + global $wgScript; -/** - * Create the messages array, formatted in PHP to copy to language files. - * @param $messages Messages array. - * @return The PHP messages array. - * @todo Make suitable for language files. - */ -function wfAllMessagesMakePhp( &$messages ) { - global $wgLang; - $txt = "\n\n\$messages = array(\n"; - foreach( $messages as $key => $m ) { - if( $wgLang->getCode() != 'en' && $m['msg'] == $m['enmsg'] ) { - continue; - } else if ( wfEmptyMsg( $key, $m['msg'] ) ) { - $m['msg'] = ''; - $comment = ' #empty'; - } else { - $comment = ''; + $languages = Language::getLanguageNames( false ); + ksort( $languages ); + + $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-allmessages-form' ) ) . + Xml::fieldset( wfMsg( 'allmessages-filter-legend' ) ) . + Xml::hidden( 'title', $this->getTitle() ) . + Xml::openElement( 'table', array( 'class' => 'mw-allmessages-table' ) ) . "\n" . + '<tr> + <td class="mw-label">' . + Xml::label( wfMsg( 'allmessages-prefix' ), 'mw-allmessages-form-prefix' ) . + "</td>\n + <td class=\"mw-input\">" . + Xml::input( 'prefix', 20, str_replace( '_', ' ', $this->prefix ), array( 'id' => 'mw-allmessages-form-prefix' ) ) . + "</td>\n + </tr> + <tr>\n + <td class='mw-label'>" . + wfMsg( 'allmessages-filter' ) . + "</td>\n + <td class='mw-input'>" . + Xml::radioLabel( wfMsg( 'allmessages-filter-unmodified' ), + 'filter', + 'unmodified', + 'mw-allmessages-form-filter-unmodified', + ( $this->filter == 'unmodified' ? true : false ) + ) . + Xml::radioLabel( wfMsg( 'allmessages-filter-all' ), + 'filter', + 'all', + 'mw-allmessages-form-filter-all', + ( $this->filter == 'all' ? true : false ) + ) . + Xml::radioLabel( wfMsg( 'allmessages-filter-modified' ), + 'filter', + 'modified', + 'mw-allmessages-form-filter-modified', + ( $this->filter == 'modified' ? true : false ) + ) . + "</td>\n + </tr> + <tr>\n + <td class=\"mw-label\">" . + Xml::label( wfMsg( 'allmessages-language' ), 'mw-allmessages-form-lang' ) . + "</td>\n + <td class=\"mw-input\">" . + Xml::openElement( 'select', array( 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' ) ); + + foreach( $languages as $lang => $name ) { + $selected = $lang == $this->langCode ? true : false; + $out .= Xml::option( $lang . ' - ' . $name, $lang, $selected ) . "\n"; } - $txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n"; - $messages[$key] = NULL; // trade bytes + $out .= Xml::closeElement( 'select' ) . + "</td>\n + </tr> + <tr>\n + <td></td> + <td>" . + Xml::submitButton( wfMsg( 'allmessages-filter-submit' ) ) . + "</td>\n + </tr>" . + Xml::closeElement( 'table' ) . + $this->table->getHiddenFields( array( 'title', 'prefix', 'filter', 'lang' ) ) . + Xml::closeElement( 'fieldset' ) . + Xml::closeElement( 'form' ); + return $out; } - $txt .= ');'; - return $txt; } -/** - * Create a list of messages, formatted in HTML as a list of messages and values and showing differences between the default language file message and the message in MediaWiki: namespace. - * @param $messages Messages array. - * @return The HTML list of messages. +/* 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. */ -function wfAllMessagesMakeHTMLText( &$messages ) { - global $wgLang, $wgContLang, $wgUser; - wfProfileIn( __METHOD__ ); - - $sk = $wgUser->getSkin(); - $talk = wfMsg( 'talkpagelinktext' ); - - $input = Xml::element( 'input', array( - 'type' => 'text', - 'id' => 'allmessagesinput', - 'onkeyup' => 'allmessagesfilter()' - ), '' ); - $checkbox = Xml::element( 'input', array( - 'type' => 'button', - 'value' => wfMsgHtml( 'allmessagesmodified' ), - 'id' => 'allmessagescheckbox', - 'onclick' => 'allmessagesmodified()' - ), '' ); - - $txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) . - " {$input}{$checkbox} " . '</span>'; - - $txt .= ' -<table border="1" cellspacing="0" width="100%" id="allmessagestable"> - <tr> - <th rowspan="2">' . wfMsgHtml( 'allmessagesname' ) . '</th> - <th>' . wfMsgHtml( 'allmessagesdefault' ) . '</th> - </tr> - <tr> - <th>' . wfMsgHtml( 'allmessagescurrent' ) . '</th> - </tr>'; - - wfProfileIn( __METHOD__ . "-check" ); - - # This is a nasty hack to avoid doing independent existence checks - # without sending the links and table through the slow wiki parser. - $pageExists = array( - NS_MEDIAWIKI => array(), - NS_MEDIAWIKI_TALK => array() - ); - $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' ) - ); - while( $s = $dbr->fetchObject( $res ) ) { - $pageExists[$s->page_namespace][$s->page_title] = 1; - } - $dbr->freeResult( $res ); - wfProfileOut( __METHOD__ . "-check" ); - - wfProfileIn( __METHOD__ . "-output" ); - - $i = 0; - - foreach( $messages as $key => $m ) { - $title = $wgLang->ucfirst( $key ); - if( $wgLang->getCode() != $wgContLang->getCode() ) { - $title .= '/' . $wgLang->getCode(); - } +class AllmessagesTablePager extends TablePager { - $titleObj = Title::makeTitle( NS_MEDIAWIKI, $title ); - $talkPage = Title::makeTitle( NS_MEDIAWIKI_TALK, $title ); + public $mLimitsShown; - $changed = ( $m['statmsg'] != $m['msg'] ); - $message = htmlspecialchars( $m['statmsg'] ); - $mw = htmlspecialchars( $m['msg'] ); + function __construct( $page, $conds, $langObj = null ) { + parent::__construct(); + $this->mIndexField = 'am_title'; + $this->mPage = $page; + $this->mConds = $conds; + $this->mDefaultDirection = true; // always sort ascending + // We want to have an option for people to view *all* the messages, + // so they can use Ctrl+F to search them. 5000 is the maximum that + // will get through WebRequest::getLimitOffset(). + $this->mLimitsShown = array( 20, 50, 100, 250, 500, 5000 => wfMsg('limitall') ); - if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI] ) ) { - $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . - htmlspecialchars( $key ) . '</span>' ); + global $wgLang, $wgContLang, $wgRequest; + + $this->talk = htmlspecialchars( wfMsg( 'talkpagelinktext' ) ); + + $this->lang = ( $langObj ? $langObj : $wgContLang ); + $this->langcode = $this->lang->getCode(); + $this->foreign = $this->langcode != $wgContLang->getCode(); + + if( $wgRequest->getVal( 'filter', 'all' ) === 'all' ){ + $this->custom = null; // So won't match in either case } else { - $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . - htmlspecialchars( $key ) . '</span>' ); + $this->custom = ($wgRequest->getVal( 'filter' ) == 'unmodified'); } - if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI_TALK] ) ) { - $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) ); + + $prefix = $wgLang->ucfirst( $wgRequest->getVal( 'prefix', '' ) ); + $prefix = $prefix != '' ? Title::makeTitleSafe( NS_MEDIAWIKI, $wgRequest->getVal( 'prefix', null ) ) : null; + if( $prefix !== null ){ + $this->prefix = '/^' . preg_quote( $prefix->getDBkey() ) . '/i'; } else { - $talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) ); + $this->prefix = false; } + $this->getSkin(); - $anchor = 'msg_' . htmlspecialchars( strtolower( $title ) ); - $anchor = "<a id=\"$anchor\" name=\"$anchor\"></a>"; - - if( $changed ) { - $txt .= " - <tr class=\"orig\" id=\"sp-allmessages-r1-$i\"> - <td rowspan=\"2\"> - $anchor$pageLink<br />$talkLink - </td><td> - $message - </td> - </tr><tr class=\"new\" id=\"sp-allmessages-r2-$i\"> - <td> - $mw - </td> - </tr>"; + // 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 { - $txt .= " - <tr class=\"def\" id=\"sp-allmessages-r1-$i\"> - <td> - $anchor$pageLink<br />$talkLink - </td><td> - $mw - </td> - </tr>"; + $this->suffix = ''; } - $messages[$key] = NULL; // trade bytes - $i++; } - $txt .= '</table>'; - wfProfileOut( __METHOD__ . '-output' ); - wfProfileOut( __METHOD__ ); - return $txt; + 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 ); + wfProfileIn( __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. + */ + function getCustomisedStatuses( $messageNames ) { + 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(); + + while( $s = $dbr->fetchObject( $res ) ) { + if( $s->page_namespace == NS_MEDIAWIKI ) { + if( $this->foreign ) { + $title = explode( '/', $s->page_title ); + if( count( $title ) === 2 && $this->langcode == $title[1] + && isset( $xNames[$title[0]] ) ) + { + $pageFlags["{$title[0]}"] = true; + } + } elseif( isset( $xNames[$s->page_title] ) ) { + $pageFlags[$s->page_title] = true; + } + } else if( $s->page_namespace == NS_MEDIAWIKI_TALK ){ + $talkFlags[$s->page_title] = true; + } + } + $dbr->freeResult( $res ); + + 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. + */ + function reallyDoQuery( $offset, $limit, $descending ) { + $result = new FakeResultWrapper( array() ); + + $messageNames = $this->getAllMessages( $descending ); + $statuses = $this->getCustomisedStatuses( $messageNames ); + + $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 ) + ){ + $result->result[] = array( + 'am_title' => $key, + 'am_actual' => wfMsgGetKey( $key, /*useDB*/true, $this->langcode, false ), + 'am_default' => wfMsgGetKey( $key, /*useDB*/false, $this->langcode, false ), + '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' => 'TablePager', 'id' => 'mw-allmessagestable' ) ) . "\n" . + "<thead><tr> + <th rowspan=\"2\">" . + wfMsg( 'allmessagesname' ) . " + </th> + <th>" . + wfMsg( 'allmessagesdefault' ) . + "</th> + </tr>\n + <tr> + <th>" . + wfMsg( 'allmessagescurrent' ) . + "</th> + </tr></thead><tbody>\n"; + } + + function formatValue( $field, $value ){ + global $wgLang; + 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 = $this->mSkin->linkKnown( $title, $wgLang->lcfirst( $value ) ); + } else { + $title = $this->mSkin->link( + $title, + $wgLang->lcfirst( $value ), + array(), + array(), + array( 'broken' ) + ); + } + if ( $this->mCurrentRow->am_talk_exists ) { + $talk = $this->mSkin->linkKnown( $talk , $this->talk ); + } else { + $talk = $this->mSkin->link( + $talk, + $this->talk, + array(), + array(), + array( 'broken' ) + ); + } + return $title . ' (' . $talk . ')'; + + case 'am_default' : + return Sanitizer::escapeHtmlAllowEntities( $value, ENT_QUOTES ); + 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 ) + . "</tr>\n"; + } + return $s; + } + + function getRowAttrs( $row, $isSecond = false ){ + $arr = array(); + global $wgLang; + if( $row->am_customised ){ + $arr['class'] = 'allmessages-customised'; + } + if( !$isSecond ){ + $arr['id'] = Sanitizer::escapeId( 'msg_' . $wgLang->lcfirst( $row->am_title ) ); + } + return $arr; + } + + function getCellAttrs( $field, $value ){ + if( $this->mCurrentRow->am_customised && $field == 'am_title' ){ + return array( 'rowspan' => '2', 'class' => $field ); + } else { + return array( 'class' => $field ); + } + } + + // This is not actually used, as getStartBody is overridden above + function getFieldNames() { + return array( + 'am_title' => wfMsg( 'allmessagesname' ), + 'am_default' => wfMsg( 'allmessagesdefault' ) + ); + } + function getTitle() { + return SpecialPage::getTitleFor( 'Allmessages', false ); + } + function isFieldSortable( $x ){ + return false; + } + function getDefaultSort(){ + return ''; + } + function getQueryInfo(){ + return ''; + } +} +/* Overloads the relevant methods of the real ResultsWrapper so it + * doesn't go anywhere near an actual database. + */ +class FakeResultWrapper extends ResultWrapper { + + var $result = array(); + var $db = null; // And it's going to stay that way :D + var $pos = 0; + var $currentRow = null; + + function __construct( $array ){ + $this->result = $array; + } + + function numRows() { + return count( $this->result ); + } + + function fetchRow() { + $this->currentRow = $this->result[$this->pos++]; + return $this->currentRow; + } + + function seek( $row ) { + $this->pos = $row; + } + + function free() {} + + // Callers want to be able to access fields with $this->fieldName + function fetchObject(){ + $this->currentRow = $this->result[$this->pos++]; + return (object)$this->currentRow; + } + + function rewind() { + $this->pos = 0; + $this->currentRow = null; + } } diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php index bded8835..a36cdca7 100644 --- a/includes/specials/SpecialAllpages.php +++ b/includes/specials/SpecialAllpages.php @@ -14,7 +14,7 @@ class SpecialAllpages extends IncludableSpecialPage { /** * Maximum number of pages to show on single index subpage. */ - protected $maxLineCount = 200; + protected $maxLineCount = 100; /** * Maximum number of chars to show for an entry. @@ -48,7 +48,8 @@ class SpecialAllpages extends IncludableSpecialPage { $namespaces = $wgContLang->getNamespaces(); - $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ? + $wgOut->setPagetitle( + ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : wfMsg( 'allarticles' ) ); @@ -69,53 +70,52 @@ class SpecialAllpages extends IncludableSpecialPage { * @param string $to dbKey we are ending listing at. */ function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '' ) { - global $wgScript; - $t = $this->getTitle(); - - $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); - $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); - $out .= Xml::hidden( 'title', $t->getPrefixedText() ); - $out .= Xml::openElement( 'fieldset' ); - $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) ); - $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); - $out .= "<tr> - <td class='mw-label'>" . - Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) . - "</td> - <td class='mw-input'>" . - Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . - "</td> - </tr> - <tr> - <td class='mw-label'>" . - Xml::label( wfMsg( 'allpagesto' ), 'nsto' ) . - "</td> - <td class='mw-input'>" . - Xml::input( 'to', 30, str_replace('_',' ',$to), array( 'id' => 'nsto' ) ) . - "</td> - </tr> - <tr> - <td class='mw-label'>" . - Xml::label( wfMsg( 'namespace' ), 'namespace' ) . - "</td> - <td class='mw-input'>" . - Xml::namespaceSelector( $namespace, null ) . ' ' . - Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . - "</td> - </tr>"; - $out .= Xml::closeElement( 'table' ); - $out .= Xml::closeElement( 'fieldset' ); - $out .= Xml::closeElement( 'form' ); - $out .= Xml::closeElement( 'div' ); - return $out; + global $wgScript; + $t = $this->getTitle(); + + $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); + $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $out .= Xml::hidden( 'title', $t->getPrefixedText() ); + $out .= Xml::openElement( 'fieldset' ); + $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) ); + $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); + $out .= "<tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) . + " </td> + <td class='mw-input'>" . + Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . + " </td> +</tr> +<tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'allpagesto' ), 'nsto' ) . + " </td> + <td class='mw-input'>" . + Xml::input( 'to', 30, str_replace('_',' ',$to), array( 'id' => 'nsto' ) ) . + " </td> +</tr> +<tr> + <td class='mw-label'>" . + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + " </td> + <td class='mw-input'>" . + Xml::namespaceSelector( $namespace, null ) . ' ' . + Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + " </td> +</tr>"; + $out .= Xml::closeElement( 'table' ); + $out .= Xml::closeElement( 'fieldset' ); + $out .= Xml::closeElement( 'form' ); + $out .= Xml::closeElement( 'div' ); + return $out; } /** * @param integer $namespace (default NS_MAIN) */ function showToplevel( $namespace = NS_MAIN, $from = '', $to = '' ) { - global $wgOut, $wgContLang; - $align = $wgContLang->isRtl() ? 'left' : 'right'; + global $wgOut; # TODO: Either make this *much* faster or cache the title index points # in the querycache table. @@ -126,8 +126,8 @@ class SpecialAllpages extends IncludableSpecialPage { $from = Title::makeTitleSafe( $namespace, $from ); $to = Title::makeTitleSafe( $namespace, $to ); - $from = ( $from && $from->isLocal() ) ? $from->getDBKey() : null; - $to = ( $to && $to->isLocal() ) ? $to->getDBKey() : null; + $from = ( $from && $from->isLocal() ) ? $from->getDBkey() : null; + $to = ( $to && $to->isLocal() ) ? $to->getDBkey() : null; if( isset($from) ) $where[] = 'page_title >= '.$dbr->addQuotes( $from ); @@ -190,7 +190,7 @@ class SpecialAllpages extends IncludableSpecialPage { // Instead, display the first section directly. if( count( $lines ) <= 2 ) { if( !empty($lines) ) { - $this->showChunk( $namespace, $lines[0], $lines[count($lines)-1] ); + $this->showChunk( $namespace, $from, $to ); } else { $wgOut->addHTML( $this->namespaceForm( $namespace, $from, $to ) ); } @@ -198,13 +198,13 @@ class SpecialAllpages extends IncludableSpecialPage { } # At this point, $lines should contain an even number of elements. - $out .= "<table class='allpageslist' style='background: inherit;'>"; + $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 ); } - $out .= '</table>'; + $out .= Xml::closeElement( 'table' ); $nsForm = $this->namespaceForm( $namespace, $from, $to ); # Is there more? @@ -213,11 +213,17 @@ class SpecialAllpages extends IncludableSpecialPage { } else { if( isset($from) || isset($to) ) { global $wgUser; - $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">'; - $out2 .= '<tr valign="top"><td>' . $nsForm; - $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' . - $wgUser->getSkin()->makeKnownLinkObj( $this->getTitle(), wfMsgHtml ( 'allpages' ) ); - $out2 .= "</td></tr></table>"; + $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ). + '<tr> + <td>' . + $nsForm . + '</td> + <td class="mw-allpages-nav">' . + $wgUser->getSkin()->link( $this->getTitle(), wfMsgHtml ( 'allpages' ), + array(), array(), 'known' ) . + "</td> + </tr>" . + Xml::closeElement( 'table' ); } else { $out2 = $nsForm; } @@ -233,7 +239,6 @@ class SpecialAllpages extends IncludableSpecialPage { */ function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) { global $wgContLang; - $align = $wgContLang->isRtl() ? 'left' : 'right'; $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) ); $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) ); // Don't let the length runaway @@ -248,7 +253,7 @@ class SpecialAllpages extends IncludableSpecialPage { "<a href=\"$link\">$inpointf</a></td><td>", "</td><td><a href=\"$link\">$outpointf</a>" ); - return '<tr><td align="' . $align . '">'.$out.'</td></tr>'; + return '<tr><td class="mw-allpages-alphaindexline">' . $out . '</td></tr>'; } /** @@ -264,8 +269,6 @@ class SpecialAllpages extends IncludableSpecialPage { $fromList = $this->getNamespaceKeyAndText($namespace, $from); $toList = $this->getNamespaceKeyAndText( $namespace, $to ); $namespaces = $wgContLang->getNamespaces(); - $align = $wgContLang->isRtl() ? 'left' : 'right'; - $n = 0; if ( !$fromList || !$toList ) { @@ -299,13 +302,12 @@ class SpecialAllpages extends IncludableSpecialPage { ); if( $res->numRows() > 0 ) { - $out = '<table style="background: inherit;" border="0" width="100%">'; - + $out = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-chunk' ) ); while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { $t = Title::makeTitle( $s->page_namespace, $s->page_title ); if( $t ) { $link = ( $s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) . - $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) . + $sk->linkKnown( $t, htmlspecialchars( $t->getText() ) ) . ($s->page_is_redirect ? '</div>' : '' ); } else { $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; @@ -316,13 +318,13 @@ class SpecialAllpages extends IncludableSpecialPage { $out .= "<td width=\"33%\">$link</td>"; $n++; if( $n % 3 == 0 ) { - $out .= '</tr>'; + $out .= "</tr>\n"; } } if( ($n % 3) != 0 ) { - $out .= '</tr>'; + $out .= "</tr>\n"; } - $out .= '</table>'; + $out .= Xml::closeElement( 'table' ); } else { $out = ''; } @@ -342,7 +344,9 @@ class SpecialAllpages extends IncludableSpecialPage { '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 ) ) + array( 'ORDER BY' => 'page_title DESC', + 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) + ) ); # Get first title of previous complete chunk @@ -370,28 +374,44 @@ class SpecialAllpages extends IncludableSpecialPage { $self = $this->getTitle(); $nsForm = $this->namespaceForm( $namespace, $from, $to ); - $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">'; - $out2 .= '<tr valign="top"><td>' . $nsForm; - $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' . - $sk->makeKnownLinkObj( $self, - wfMsgHtml ( 'allpages' ) ); + $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ). + '<tr> + <td>' . + $nsForm . + '</td> + <td class="mw-allpages-nav">' . + $sk->link( $self, wfMsgHtml ( 'allpages' ), array(), array(), 'known' ); # Do we put a previous link ? if( isset( $prevTitle ) && $pt = $prevTitle->getText() ) { - $q = 'from=' . $prevTitle->getPartialUrl() - . ( $namespace ? '&namespace=' . $namespace : '' ); - $prevLink = $sk->makeKnownLinkObj( $self, - wfMsgHTML( 'prevpage', htmlspecialchars( $pt ) ), $q ); + $query = array( 'from' => $prevTitle->getText() ); + + if( $namespace ) + $query['namespace'] = $namespace; + + $prevLink = $sk->linkKnown( + $self, + htmlspecialchars( wfMsg( 'prevpage', $pt ) ), + array(), + $query + ); $out2 = $wgLang->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); - $q = 'from=' . $t->getPartialUrl() - . ( $namespace ? '&namespace=' . $namespace : '' ); - $nextLink = $sk->makeKnownLinkObj( $self, - wfMsgHtml( 'nextpage', htmlspecialchars( $t->getText() ) ), $q ); + $query = array( 'from' => $t->getText() ); + + if( $namespace ) + $query['namespace'] = $namespace; + + $nextLink = $sk->linkKnown( + $self, + htmlspecialchars( wfMsg( 'nextpage', $t->getText() ) ), + array(), + $query + ); $out2 = $wgLang->pipeList( array( $out2, $nextLink ) ); } $out2 .= "</td></tr></table>"; @@ -399,7 +419,7 @@ class SpecialAllpages extends IncludableSpecialPage { $wgOut->addHTML( $out2 . $out ); if( isset($prevLink) or isset($nextLink) ) { - $wgOut->addHTML( '<hr /><p style="font-size: smaller; float: ' . $align . '">' ); + $wgOut->addHTML( '<hr /><p class="mw-allpages-nav">' ); if( isset( $prevLink ) ) { $wgOut->addHTML( $prevLink ); } @@ -430,7 +450,7 @@ class SpecialAllpages extends IncludableSpecialPage { if ( $t && $t->isLocal() ) { return array( $t->getNamespace(), $t->getDBkey(), $t->getText() ); } else if ( $t ) { - return NULL; + return null; } # try again, in case the problem was an empty pagename @@ -439,7 +459,7 @@ class SpecialAllpages extends IncludableSpecialPage { if ( $t && $t->isLocal() ) { return array( $t->getNamespace(), '', '' ); } else { - return NULL; + return null; } } } diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php index 188ad914..92192435 100644 --- a/includes/specials/SpecialAncientpages.php +++ b/includes/specials/SpecialAncientpages.php @@ -25,8 +25,25 @@ class AncientPagesPage extends QueryPage { $db = wfGetDB( DB_SLAVE ); $page = $db->tableName( 'page' ); $revision = $db->tableName( 'revision' ); - $epoch = $wgDBtype == 'mysql' ? 'UNIX_TIMESTAMP(rev_timestamp)' : - 'EXTRACT(epoch FROM rev_timestamp)'; + + switch ($wgDBtype) { + case 'mysql': + $epoch = 'UNIX_TIMESTAMP(rev_timestamp)'; + break; + case 'ibm_db2': + // TODO implement proper conversion to a Unix epoch + $epoch = 'rev_timestamp'; + break; + case 'oracle': + $epoch = '((trunc(rev_timestamp) - to_date(\'19700101\',\'YYYYMMDD\')) * 86400)'; + break; + case 'sqlite': + $epoch = 'rev_timestamp'; + break; + default: + $epoch = 'EXTRACT(epoch FROM rev_timestamp)'; + } + return "SELECT 'Ancientpages' as type, page_namespace as namespace, @@ -46,8 +63,11 @@ class AncientPagesPage extends QueryPage { $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $result->value ), true ); $title = Title::makeTitle( $result->namespace, $result->title ); - $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) ); - return wfSpecialList($link, $d); + $link = $skin->linkKnown( + $title, + htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) + ); + return wfSpecialList($link, htmlspecialchars($d) ); } } diff --git a/includes/specials/SpecialBlankpage.php b/includes/specials/SpecialBlankpage.php index 29d6b96c..e1fadd02 100644 --- a/includes/specials/SpecialBlankpage.php +++ b/includes/specials/SpecialBlankpage.php @@ -1,6 +1,17 @@ <?php - -function wfSpecialBlankpage() { - global $wgOut; - $wgOut->addWikiMsg('intentionallyblankpage'); +/** + * Special page designed for basic benchmarking of + * MediaWiki since it doesn't really do much. + * + * @ingroup SpecialPage + */ +class SpecialBlankpage extends UnlistedSpecialPage { + public function __construct() { + parent::__construct( 'Blankpage' ); + } + public function execute( $par ) { + global $wgOut; + $this->setHeaders(); + $wgOut->addWikiMsg('intentionallyblankpage'); + } } diff --git a/includes/specials/SpecialBlockip.php b/includes/specials/SpecialBlockip.php index f002e570..16720dd1 100644 --- a/includes/specials/SpecialBlockip.php +++ b/includes/specials/SpecialBlockip.php @@ -17,7 +17,6 @@ function wfSpecialBlockip( $par ) { $wgOut->readOnlyPage(); return; } - # Permission check if( !$wgUser->isAllowed( 'block' ) ) { $wgOut->permissionRequired( 'block' ); @@ -27,9 +26,9 @@ function wfSpecialBlockip( $par ) { $ipb = new IPBlockForm( $par ); $action = $wgRequest->getVal( 'action' ); - if ( 'success' == $action ) { + if( 'success' == $action ) { $ipb->showSuccess(); - } else if ( $wgRequest->wasPosted() && 'submit' == $action && + } elseif( $wgRequest->wasPosted() && 'submit' == $action && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { $ipb->doSubmit(); } else { @@ -44,18 +43,17 @@ function wfSpecialBlockip( $par ) { */ class IPBlockForm { var $BlockAddress, $BlockExpiry, $BlockReason; -# var $BlockEmail; // The maximum number of edits a user can have and still be hidden const HIDEUSER_CONTRIBLIMIT = 1000; - function IPBlockForm( $par ) { + public function __construct( $par ) { global $wgRequest, $wgUser, $wgBlockAllowsUTEdit; $this->BlockAddress = $wgRequest->getVal( 'wpBlockAddress', $wgRequest->getVal( 'ip', $par ) ); $this->BlockAddress = strtr( $this->BlockAddress, '_', ' ' ); $this->BlockReason = $wgRequest->getText( 'wpBlockReason' ); $this->BlockReasonList = $wgRequest->getText( 'wpBlockReasonList' ); - $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg('ipbotheroption') ); + $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg( 'ipbotheroption' ) ); $this->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' ); # Unchecked checkboxes are not included in the form data at all, so having one @@ -64,21 +62,30 @@ class IPBlockForm { $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly', $byDefault ); $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', $byDefault ); $this->BlockEnableAutoblock = $wgRequest->getBool( 'wpEnableAutoblock', $byDefault ); - $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false ); - $this->BlockWatchUser = $wgRequest->getBool( 'wpWatchUser', false ); - # Re-check user's rights to hide names, very serious, defaults to 0 - $this->BlockHideName = ( $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ) ) ? 1 : 0; + $this->BlockEmail = false; + if( self::canBlockEmail( $wgUser ) ) { + $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false ); + } + $this->BlockWatchUser = $wgRequest->getBool( 'wpWatchUser', false ) && $wgUser->isLoggedIn(); + # Re-check user's rights to hide names, very serious, defaults to null + if( $wgUser->isAllowed( 'hideuser' ) ) { + $this->BlockHideName = $wgRequest->getBool( 'wpHideName', null ); + } else { + $this->BlockHideName = false; + } $this->BlockAllowUsertalk = ( $wgRequest->getBool( 'wpAllowUsertalk', $byDefault ) && $wgBlockAllowsUTEdit ); $this->BlockReblock = $wgRequest->getBool( 'wpChangeBlock', false ); + + $this->wasPosted = $wgRequest->wasPosted(); } - function showForm( $err ) { + public function showForm( $err ) { global $wgOut, $wgUser, $wgSysopUserBans; - $wgOut->setPagetitle( wfMsg( 'blockip' ) ); + $wgOut->setPageTitle( wfMsg( 'blockip-title' ) ); $wgOut->addWikiMsg( 'blockiptext' ); - if($wgSysopUserBans) { + if( $wgSysopUserBans ) { $mIpaddress = Xml::label( wfMsg( 'ipadressorusername' ), 'mw-bi-target' ); } else { $mIpaddress = Xml::label( wfMsg( 'ipaddress' ), 'mw-bi-target' ); @@ -90,25 +97,28 @@ class IPBlockForm { $titleObj = SpecialPage::getTitleFor( 'Blockip' ); $user = User::newFromName( $this->BlockAddress ); - + $alreadyBlocked = false; - if ( $err && $err[0] != 'ipb_already_blocked' ) { - $key = array_shift($err); - $msg = wfMsgReal($key, $err); + $otherBlockedMsgs = array(); + if( $err && $err[0] != 'ipb_already_blocked' ) { + $key = array_shift( $err ); + $msg = wfMsgReal( $key, $err ); $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) ); $wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $msg ) ); - } elseif ( $this->BlockAddress ) { - $userId = 0; - if ( is_object( $user ) ) - $userId = $user->getId(); + } elseif( $this->BlockAddress ) { + # Get other blocks, i.e. from GlobalBlocking or TorBlock extension + wfRunHooks( 'OtherBlockLogLink', array( &$otherBlockedMsgs, $this->BlockAddress ) ); + + $userId = is_object( $user ) ? $user->getId() : 0; $currentBlock = Block::newFromDB( $this->BlockAddress, $userId ); - if ( !is_null($currentBlock) && !$currentBlock->mAuto && # The block exists and isn't an autoblock + if( !is_null( $currentBlock ) && !$currentBlock->mAuto && # The block exists and isn't an autoblock ( $currentBlock->mRangeStart == $currentBlock->mRangeEnd || # The block isn't a rangeblock # or if it is, the range is what we're about to block - ( $currentBlock->mAddress == $this->BlockAddress ) ) ) { - $wgOut->addWikiMsg( 'ipb-needreblock', $this->BlockAddress ); - $alreadyBlocked = true; - # Set the block form settings to the existing block + ( $currentBlock->mAddress == $this->BlockAddress ) ) + ) { + $alreadyBlocked = true; + # Set the block form settings to the existing block + if( !$this->wasPosted ) { $this->BlockAnonOnly = $currentBlock->mAnonOnly; $this->BlockCreateAccount = $currentBlock->mCreateAccount; $this->BlockEnableAutoblock = $currentBlock->mEnableAutoblock; @@ -121,21 +131,38 @@ class IPBlockForm { $this->BlockOther = wfTimestamp( TS_ISO_8601, $currentBlock->mExpiry ); } $this->BlockReason = $currentBlock->mReason; + } + } + } + + # Show other blocks from extensions, i.e. GlockBlocking and TorBlock + if( count( $otherBlockedMsgs ) ) { + $wgOut->addHTML( + Html::rawElement( 'h2', array(), wfMsgExt( 'ipb-otherblocks-header', 'parseinline', count( $otherBlockedMsgs ) ) ) . "\n" + ); + $list = ''; + foreach( $otherBlockedMsgs as $link ) { + $list .= Html::rawElement( 'li', array(), $link ) . "\n"; } + $wgOut->addHTML( Html::rawElement( 'ul', array( 'class' => 'mw-blockip-alreadyblocked' ), $list ) . "\n" ); + } + + # Username/IP is blocked already locally + if( $alreadyBlocked ) { + $wgOut->addWikiMsg( 'ipb-needreblock', $this->BlockAddress ); } $scBlockExpiryOptions = wfMsgForContent( 'ipboptions' ); $showblockoptions = $scBlockExpiryOptions != '-'; - if (!$showblockoptions) - $mIpbother = $mIpbexpiry; + if( !$showblockoptions ) $mIpbother = $mIpbexpiry; $blockExpiryFormOptions = Xml::option( wfMsg( 'ipbotheroption' ), 'other' ); - foreach (explode(',', $scBlockExpiryOptions) as $option) { - if ( strpos($option, ":") === false ) $option = "$option:$option"; - list($show, $value) = explode(":", $option); - $show = htmlspecialchars($show); - $value = htmlspecialchars($value); + foreach( explode( ',', $scBlockExpiryOptions ) as $option ) { + if( strpos( $option, ':' ) === false ) $option = "$option:$option"; + list( $show, $value ) = explode( ':', $option ); + $show = htmlspecialchars( $show ); + $value = htmlspecialchars( $value ); $blockExpiryFormOptions .= Xml::option( $show, $value, $this->BlockExpiry === $value ? true : false ) . "\n"; } @@ -146,25 +173,27 @@ class IPBlockForm { global $wgStylePath, $wgStyleVersion; $wgOut->addHTML( Xml::tags( 'script', array( 'type' => 'text/javascript', 'src' => "$wgStylePath/common/block.js?$wgStyleVersion" ), '' ) . - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( "action=submit" ), 'id' => 'blockip' ) ) . + Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), 'id' => 'blockip' ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'blockip-legend' ) ) . - Xml::openElement( 'table', array ( 'border' => '0', 'id' => 'mw-blockip-table' ) ) . + Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-blockip-table' ) ) . "<tr> <td class='mw-label'> {$mIpaddress} </td> <td class='mw-input'>" . - Xml::input( 'wpBlockAddress', 45, $this->BlockAddress, - array( - 'tabindex' => '1', - 'id' => 'mw-bi-target', - 'onchange' => 'updateBlockOptions()' ) ). " + Html::input( 'wpBlockAddress', $this->BlockAddress, 'text', array( + 'tabindex' => '1', + 'id' => 'mw-bi-target', + 'onchange' => 'updateBlockOptions()', + 'size' => '45', + 'required' => '' + ) + ( $this->BlockAddress ? array() : array( 'autofocus' ) ) ). " </td> </tr> <tr>" ); - if ( $showblockoptions ) { + if( $showblockoptions ) { $wgOut->addHTML(" <td class='mw-label'> {$mIpbexpiry} @@ -204,8 +233,12 @@ class IPBlockForm { {$mIpbreason} </td> <td class='mw-input'>" . - Xml::input( 'wpBlockReason', 45, $this->BlockReason, - array( 'tabindex' => '5', 'id' => 'mw-bi-reason', 'maxlength'=> '200' ) ) . " + Html::input( 'wpBlockReason', $this->BlockReason, 'text', array( + 'tabindex' => '5', + 'id' => 'mw-bi-reason', + 'maxlength' => '200', + 'size' => '45' + ) + ( $this->BlockAddress ? array( 'autofocus' ) : array() ) ) . " </td> </tr> <tr id='wpAnonOnlyRow'> @@ -234,36 +267,37 @@ class IPBlockForm { </tr>" ); - global $wgSysopEmailBans, $wgBlockAllowsUTEdit; - if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) { + if( self::canBlockEmail( $wgUser ) ) { $wgOut->addHTML(" <tr id='wpEnableEmailBan'> <td> </td> <td class='mw-input'>" . Xml::checkLabel( wfMsg( 'ipbemailban' ), 'wpEmailBan', 'wpEmailBan', $this->BlockEmail, - array( 'tabindex' => '9' )) . " + array( 'tabindex' => '9' ) ) . " </td> </tr>" ); } // Allow some users to hide name from block log, blocklist and listusers - if ( $wgUser->isAllowed( 'hideuser' ) ) { + if( $wgUser->isAllowed( 'hideuser' ) ) { $wgOut->addHTML(" <tr id='wpEnableHideUser'> <td> </td> <td class='mw-input'><strong>" . Xml::checkLabel( wfMsg( 'ipbhidename' ), 'wpHideName', 'wpHideName', $this->BlockHideName, - array( 'tabindex' => '10' ) ) . " + array( 'tabindex' => '10' ) + ) . " </strong></td> </tr>" ); } - - # Watchlist their user page? - $wgOut->addHTML(" + + # Watchlist their user page? (Only if user is logged in) + if( $wgUser->isLoggedIn() ) { + $wgOut->addHTML(" <tr id='wpEnableWatchUser'> <td> </td> <td class='mw-input'>" . @@ -272,7 +306,11 @@ class IPBlockForm { array( 'tabindex' => '11' ) ) . " </td> </tr>" - ); + ); + } + + # Can we explicitly disallow the use of user_talk? + global $wgBlockAllowsUTEdit; if( $wgBlockAllowsUTEdit ){ $wgOut->addHTML(" <tr id='wpAllowUsertalkRow'> @@ -314,12 +352,22 @@ class IPBlockForm { } /** + * Can we do an email block? + * @param User $user The sysop wanting to make a block + * @return boolean + */ + public static function canBlockEmail( $user ) { + global $wgEnableUserEmail, $wgSysopEmailBans; + return ( $wgEnableUserEmail && $wgSysopEmailBans && $user->isAllowed( 'blockemail' ) ); + } + + /** * Backend block code. * $userID and $expiry will be filled accordingly * @return array(message key, arguments) on failure, empty array on success */ function doBlock( &$userId = null, &$expiry = null ) { - global $wgUser, $wgSysopUserBans, $wgSysopRangeBans, $wgBlockAllowsUTEdit; + global $wgUser, $wgSysopUserBans, $wgSysopRangeBans, $wgBlockAllowsUTEdit, $wgBlockCIDRLimit; $userId = 0; # Expand valid IPv6 addresses, usernames are left as is @@ -330,24 +378,28 @@ class IPBlockForm { $rxIP = "($rxIP4|$rxIP6)"; # Check for invalid specifications - if ( !preg_match( "/^$rxIP$/", $this->BlockAddress ) ) { + if( !preg_match( "/^$rxIP$/", $this->BlockAddress ) ) { $matches = array(); - if ( preg_match( "/^($rxIP4)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) { + if( preg_match( "/^($rxIP4)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) { # IPv4 - if ( $wgSysopRangeBans ) { - if ( !IP::isIPv4( $this->BlockAddress ) || $matches[2] < 16 || $matches[2] > 32 ) { - return array('ip_range_invalid'); + if( $wgSysopRangeBans ) { + if( !IP::isIPv4( $this->BlockAddress ) || $matches[2] > 32 ) { + return array( 'ip_range_invalid' ); + } elseif ( $matches[2] < $wgBlockCIDRLimit['IPv4'] ) { + return array( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv4'] ); } $this->BlockAddress = Block::normaliseRange( $this->BlockAddress ); } else { # Range block illegal - return array('range_block_disabled'); + return array( 'range_block_disabled' ); } - } else if ( preg_match( "/^($rxIP6)\\/(\\d{1,3})$/", $this->BlockAddress, $matches ) ) { + } elseif( preg_match( "/^($rxIP6)\\/(\\d{1,3})$/", $this->BlockAddress, $matches ) ) { # IPv6 - if ( $wgSysopRangeBans ) { - if ( !IP::isIPv6( $this->BlockAddress ) || $matches[2] < 64 || $matches[2] > 128 ) { - return array('ip_range_invalid'); + if( $wgSysopRangeBans ) { + if( !IP::isIPv6( $this->BlockAddress ) || $matches[2] > 128 ) { + return array( 'ip_range_invalid' ); + } elseif( $matches[2] < $wgBlockCIDRLimit['IPv6'] ) { + return array( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv6'] ); } $this->BlockAddress = Block::normaliseRange( $this->BlockAddress ); } else { @@ -356,30 +408,30 @@ class IPBlockForm { } } else { # Username block - if ( $wgSysopUserBans ) { + if( $wgSysopUserBans ) { $user = User::newFromName( $this->BlockAddress ); if( !is_null( $user ) && $user->getId() ) { # Use canonical name $userId = $user->getId(); $this->BlockAddress = $user->getName(); } else { - return array('nosuchusershort', htmlspecialchars( $user ? $user->getName() : $this->BlockAddress ) ); + return array( 'nosuchusershort', htmlspecialchars( $user ? $user->getName() : $this->BlockAddress ) ); } } else { - return array('badipaddress'); + return array( 'badipaddress' ); } } } - if ( $wgUser->isBlocked() && ( $wgUser->getId() !== $userId ) ) { + if( $wgUser->isBlocked() && ( $wgUser->getId() !== $userId ) ) { return array( 'cant-block-while-blocked' ); } $reasonstr = $this->BlockReasonList; - if ( $reasonstr != 'other' && $this->BlockReason != '' ) { + if( $reasonstr != 'other' && $this->BlockReason != '' ) { // Entry from drop down menu + additional comment $reasonstr .= wfMsgForContent( 'colon-separator' ) . $this->BlockReason; - } elseif ( $reasonstr == 'other' ) { + } elseif( $reasonstr == 'other' ) { $reasonstr = $this->BlockReason; } @@ -387,44 +439,45 @@ class IPBlockForm { if( $expirestr == 'other' ) $expirestr = $this->BlockOther; - if ( ( strlen( $expirestr ) == 0) || ( strlen( $expirestr ) > 50) ) { - return array('ipb_expiry_invalid'); + if( ( strlen( $expirestr ) == 0) || ( strlen( $expirestr ) > 50 ) ) { + return array( 'ipb_expiry_invalid' ); } - if ( false === ($expiry = Block::parseExpiryInput( $expirestr )) ) { + if( false === ( $expiry = Block::parseExpiryInput( $expirestr ) ) ) { // Bad expiry. - return array('ipb_expiry_invalid'); + return array( 'ipb_expiry_invalid' ); } - + if( $this->BlockHideName ) { - if( !$userId ) { - // IP users should not be hidden - $this->BlockHideName = false; - } else if( $expiry !== 'infinity' ) { + // Recheck params here... + if( !$userId || !$wgUser->isAllowed('hideuser') ) { + $this->BlockHideName = false; // IP users should not be hidden + } elseif( $expiry !== 'infinity' ) { // Bad expiry. - return array('ipb_expiry_temp'); - } else if( User::edits($userId) > self::HIDEUSER_CONTRIBLIMIT ) { + return array( 'ipb_expiry_temp' ); + } elseif( User::edits( $userId ) > self::HIDEUSER_CONTRIBLIMIT ) { // Typically, the user should have a handful of edits. // Disallow hiding users with many edits for performance. - return array('ipb_hide_invalid'); + return array( 'ipb_hide_invalid' ); } } - # Create block + # Create block object # Note: for a user block, ipb_address is only for display purposes $block = new Block( $this->BlockAddress, $userId, $wgUser->getId(), $reasonstr, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly, $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName, - $this->BlockEmail, isset( $this->BlockAllowUsertalk ) ? $this->BlockAllowUsertalk : $wgBlockAllowsUTEdit + $this->BlockEmail, + isset( $this->BlockAllowUsertalk ) ? $this->BlockAllowUsertalk : $wgBlockAllowsUTEdit ); # Should this be privately logged? $suppressLog = (bool)$this->BlockHideName; - if ( wfRunHooks('BlockIp', array(&$block, &$wgUser)) ) { + if( wfRunHooks( 'BlockIp', array( &$block, &$wgUser ) ) ) { # Try to insert block. Is there a conflicting block? - if ( !$block->insert() ) { + if( !$block->insert() ) { # Show form unless the user is already aware of this... - if ( !$this->BlockReblock ) { + if( !$this->BlockReblock ) { return array( 'ipb_already_blocked' ); # Otherwise, try to update the block... } else { @@ -436,8 +489,8 @@ class IPBlockForm { } # If the name was hidden and the blocking user cannot hide # names, then don't allow any block changes... - if( $currentBlock->mHideName && !$wgUser->isAllowed('hideuser') ) { - return array( 'hookaborted' ); + if( $currentBlock->mHideName && !$wgUser->isAllowed( 'hideuser' ) ) { + return array( 'cant-see-hidden-user' ); } $currentBlock->delete(); $block->insert(); @@ -452,19 +505,18 @@ class IPBlockForm { } else { $log_action = 'block'; } - wfRunHooks('BlockIpComplete', array($block, $wgUser)); + wfRunHooks( 'BlockIpComplete', array( $block, $wgUser ) ); # Set *_deleted fields if requested if( $this->BlockHideName ) { self::suppressUserName( $this->BlockAddress, $userId ); } - if ( $this->BlockWatchUser && - # Only show watch link when this is no range block - $block->mRangeStart == $block->mRangeEnd) { - $wgUser->addWatch ( Title::makeTitle( NS_USER, $this->BlockAddress ) ); + # Only show watch link when this is no range block + if( $this->BlockWatchUser && $block->mRangeStart == $block->mRangeEnd ) { + $wgUser->addWatch( Title::makeTitle( NS_USER, $this->BlockAddress ) ); } - + # Block constructor sanitizes certain block options on insert $this->BlockEmail = $block->mBlockEmail; $this->BlockEnableAutoblock = $block->mEnableAutoblock; @@ -478,34 +530,34 @@ class IPBlockForm { $log_type = $suppressLog ? 'suppress' : 'block'; $log = new LogPage( $log_type ); $log->addEntry( $log_action, Title::makeTitle( NS_USER, $this->BlockAddress ), - $reasonstr, $logParams ); + $reasonstr, $logParams ); # Report to the user return array(); } else { - return array('hookaborted'); + return array( 'hookaborted' ); } } - - public static function suppressUserName( $name, $userId ) { + + public static function suppressUserName( $name, $userId, $dbw = null ) { $op = '|'; // bitwise OR - return self::setUsernameBitfields( $name, $userId, $op ); + return self::setUsernameBitfields( $name, $userId, $op, $dbw ); } - - public static function unsuppressUserName( $name, $userId ) { + + public static function unsuppressUserName( $name, $userId, $dbw = null ) { $op = '&'; // bitwise AND - return self::setUsernameBitfields( $name, $userId, $op ); + return self::setUsernameBitfields( $name, $userId, $op, $dbw ); } - - private static function setUsernameBitfields( $name, $userId, $op ) { - if( $op !== '|' && $op !== '&' ) - return false; // sanity check - $dbw = wfGetDB( DB_MASTER ); + + private static function setUsernameBitfields( $name, $userId, $op, $dbw ) { + if( $op !== '|' && $op !== '&' ) return false; // sanity check + if( !$dbw ) + $dbw = wfGetDB( DB_MASTER ); $delUser = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; $delAction = LogPage::DELETED_ACTION | Revision::DELETED_RESTRICTED; # Normalize user name $userTitle = Title::makeTitleSafe( NS_USER, $name ); - $userDbKey = $userTitle->getDBKey(); + $userDbKey = $userTitle->getDBkey(); # To suppress, we OR the current bitfields with Revision::DELETED_USER # to put a 1 in the username *_deleted bit. To unsuppress we AND the # current bitfields with the inverse of Revision::DELETED_USER. The @@ -516,27 +568,29 @@ class IPBlockForm { $delAction = "~{$delAction}"; } # Hide name from live edits - $dbw->update( 'revision', array("rev_deleted = rev_deleted $op $delUser"), - array('rev_user' => $userId), __METHOD__ ); + $dbw->update( 'revision', array( "rev_deleted = rev_deleted $op $delUser" ), + array( 'rev_user' => $userId ), __METHOD__ ); # Hide name from deleted edits - $dbw->update( 'archive', array("ar_deleted = ar_deleted $op $delUser"), - array('ar_user_text' => $name), __METHOD__ ); + $dbw->update( 'archive', array( "ar_deleted = ar_deleted $op $delUser" ), + array( 'ar_user_text' => $name ), __METHOD__ ); # Hide name from logs - $dbw->update( 'logging', array("log_deleted = log_deleted $op $delUser"), - array('log_user' => $userId, "log_type != 'suppress'"), __METHOD__ ); - $dbw->update( 'logging', array("log_deleted = log_deleted $op $delAction"), - array('log_namespace' => NS_USER, 'log_title' => $userDbKey, - "log_type != 'suppress'"), __METHOD__ ); + $dbw->update( 'logging', array( "log_deleted = log_deleted $op $delUser" ), + array( 'log_user' => $userId, "log_type != 'suppress'" ), __METHOD__ ); + $dbw->update( 'logging', array( "log_deleted = log_deleted $op $delAction" ), + array( 'log_namespace' => NS_USER, 'log_title' => $userDbKey, + "log_type != 'suppress'" ), __METHOD__ ); # Hide name from RC - $dbw->update( 'recentchanges', array("rc_deleted = rc_deleted $op $delUser"), - array('rc_user_text' => $name), __METHOD__ ); + $dbw->update( 'recentchanges', array( "rc_deleted = rc_deleted $op $delUser" ), + array( 'rc_user_text' => $name ), __METHOD__ ); + $dbw->update( 'recentchanges', array( "rc_deleted = rc_deleted $op $delAction" ), + array( 'rc_namespace' => NS_USER, 'rc_title' => $userDbKey, 'rc_logid > 0' ), __METHOD__ ); # Hide name from live images - $dbw->update( 'oldimage', array("oi_deleted = oi_deleted $op $delUser"), - array('oi_user_text' => $name), __METHOD__ ); + $dbw->update( 'oldimage', array( "oi_deleted = oi_deleted $op $delUser" ), + array( 'oi_user_text' => $name ), __METHOD__ ); # Hide name from deleted images # WMF - schema change pending - # $dbw->update( 'filearchive', array("fa_deleted = fa_deleted $op $delUser"), - # array('fa_user_text' => $name), __METHOD__ ); + # $dbw->update( 'filearchive', array( "fa_deleted = fa_deleted $op $delUser" ), + # array( 'fa_user_text' => $name ), __METHOD__ ); # Done! return true; } @@ -545,11 +599,10 @@ class IPBlockForm { * UI entry point for blocking * Wraps around doBlock() */ - function doSubmit() - { + public function doSubmit() { global $wgOut; $retval = $this->doBlock(); - if(empty($retval)) { + if( empty( $retval ) ) { $titleObj = SpecialPage::getTitleFor( 'Blockip' ); $wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' . urlencode( $this->BlockAddress ) ) ); @@ -558,27 +611,55 @@ class IPBlockForm { $this->showForm( $retval ); } - function showSuccess() { + public function showSuccess() { global $wgOut; - $wgOut->setPagetitle( wfMsg( 'blockip' ) ); + $wgOut->setPageTitle( wfMsg( 'blockip-title' ) ); $wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) ); $text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress ); $wgOut->addHTML( $text ); } - function showLogFragment( $out, $title ) { + private function showLogFragment( $out, $title ) { global $wgUser; - $out->addHTML( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) ); - $count = LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText(), '', 10 ); - if($count > 10){ - $out->addHTML( $wgUser->getSkin()->link( - SpecialPage::getTitleFor( 'Log' ), - wfMsgHtml( 'blocklog-fulllog' ), - array(), + + // Used to support GENDER in 'blocklog-showlog' and 'blocklog-showsuppresslog' + $userBlocked = $title->getText(); + + LogEventsList::showLogExtract( + $out, + 'block', + $title->getPrefixedText(), + '', + array( + 'lim' => 10, + 'msgKey' => array( + 'blocklog-showlog', + $userBlocked + ), + 'showIfEmpty' => false + ) + ); + + // Add suppression block entries if allowed + if( $wgUser->isAllowed( 'hideuser' ) ) { + LogEventsList::showLogExtract( $out, 'suppress', $title->getPrefixedText(), '', array( - 'type' => 'block', - 'page' => $title->getPrefixedText() ) ) ); + 'lim' => 10, + 'conds' => array( + 'log_action' => array( + 'block', + 'reblock', + 'unblock' + ) + ), + 'msgKey' => array( + 'blocklog-showsuppresslog', + $userBlocked + ), + 'showIfEmpty' => false + ) + ); } } @@ -596,13 +677,14 @@ class IPBlockForm { $flags[] = 'anononly'; if( $this->BlockCreateAccount ) $flags[] = 'nocreate'; - if( !$this->BlockEnableAutoblock ) + if( !$this->BlockEnableAutoblock && !IP::isIPAddress( $this->BlockAddress ) ) + // Same as anononly, this is not displayed when blocking an IP address $flags[] = 'noautoblock'; - if ( $this->BlockEmail ) + if( $this->BlockEmail ) $flags[] = 'noemail'; - if ( !$this->BlockAllowUsertalk && $wgBlockAllowsUTEdit ) + if( !$this->BlockAllowUsertalk && $wgBlockAllowsUTEdit ) $flags[] = 'nousertalk'; - if ( $this->BlockHideName ) + if( $this->BlockHideName ) $flags[] = 'hiddenname'; return implode( ',', $flags ); } @@ -619,10 +701,18 @@ class IPBlockForm { $links[] = $this->getContribsLink( $skin ); $links[] = $this->getUnblockLink( $skin ); $links[] = $this->getBlockListLink( $skin ); - $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) ); + if ( $wgUser->isAllowed( 'editinterface' ) ) { + $title = Title::makeTitle( NS_MEDIAWIKI, 'Ipbreason-dropdown' ); + $links[] = $skin->link( + $title, + wfMsgHtml( 'ipb-edit-dropdown' ), + array(), + array( 'action' => 'edit' ) + ); + } return '<p class="mw-ipb-conveniencelinks">' . $wgLang->pipeList( $links ) . '</p>'; } - + /** * Build a convenient link to a user or IP's contribs * form @@ -645,13 +735,21 @@ class IPBlockForm { */ private function getUnblockLink( $skin ) { $list = SpecialPage::getTitleFor( 'Ipblocklist' ); + $query = array( 'action' => 'unblock' ); + if( $this->BlockAddress ) { - $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) ); - return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock-addr', $addr ), - 'action=unblock&ip=' . urlencode( $this->BlockAddress ) ); + $addr = strtr( $this->BlockAddress, '_', ' ' ); + $message = wfMsg( 'ipb-unblock-addr', $addr ); + $query['ip'] = $this->BlockAddress; } else { - return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock' ), 'action=unblock' ); + $message = wfMsg( 'ipb-unblock' ); } + return $skin->linkKnown( + $list, + htmlspecialchars( $message ), + array(), + $query + ); } /** @@ -662,23 +760,32 @@ class IPBlockForm { */ private function getBlockListLink( $skin ) { $list = SpecialPage::getTitleFor( 'Ipblocklist' ); + $query = array(); + if( $this->BlockAddress ) { - $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) ); - return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist-addr', $addr ), - 'ip=' . urlencode( $this->BlockAddress ) ); + $addr = strtr( $this->BlockAddress, '_', ' ' ); + $message = wfMsg( 'ipb-blocklist-addr', $addr ); + $query['ip'] = $this->BlockAddress; } else { - return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist' ) ); + $message = wfMsg( 'ipb-blocklist' ); } + + return $skin->linkKnown( + $list, + htmlspecialchars( $message ), + array(), + $query + ); } - + /** - * Block a list of selected users - * @param array $users - * @param string $reason - * @param string $tag replaces user pages - * @param string $talkTag replaces user talk pages - * @returns array, list of html-safe usernames - */ + * Block a list of selected users + * @param array $users + * @param string $reason + * @param string $tag replaces user pages + * @param string $talkTag replaces user talk pages + * @returns array, list of html-safe usernames + */ public static function doMassUserBlock( $users, $reason = '', $tag = '', $talkTag = '' ) { global $wgUser; $counter = $blockSize = 0; @@ -695,7 +802,7 @@ class IPBlockForm { } $u = User::newFromName( $name, false ); // If user doesn't exist, it ought to be an IP then - if( is_null($u) || (!$u->getId() && !IP::isIPAddress( $u->getName() )) ) { + if( is_null( $u ) || ( !$u->getId() && !IP::isIPAddress( $u->getName() ) ) ) { continue; } $userTitle = $u->getUserPage(); @@ -734,10 +841,10 @@ class IPBlockForm { $log->addEntry( 'block', $userTitle, $reason, $logParams ); } # Tag userpage! (check length to avoid mistakes) - if( strlen($tag) > 2 ) { + if( strlen( $tag ) > 2 ) { $userpage->doEdit( $tag, $reason, EDIT_MINOR ); } - if( strlen($talkTag) > 2 ) { + if( strlen( $talkTag ) > 2 ) { $usertalk->doEdit( $talkTag, $reason, EDIT_MINOR ); } } diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php index db466c14..8ee5467a 100644 --- a/includes/specials/SpecialBooksources.php +++ b/includes/specials/SpecialBooksources.php @@ -6,7 +6,7 @@ * * @author Rob Church <robchur@gmail.com> * @todo Validate ISBNs using the standard check-digit method - * @ingroup SpecialPages + * @ingroup SpecialPage */ class SpecialBookSources extends SpecialPage { @@ -35,7 +35,7 @@ class SpecialBookSources extends SpecialPage { $wgOut->addHTML( $this->makeForm() ); if( strlen( $this->isbn ) > 0 ) { if( !self::isValidISBN( $this->isbn ) ) { - $wgOut->wrapWikiMsg( '<div class="error">$1</div>', 'booksources-invalid-isbn' ); + $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1</div>", 'booksources-invalid-isbn' ); } $this->showList(); } diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php index 0a16e6de..b6ae2ada 100644 --- a/includes/specials/SpecialBrokenRedirects.php +++ b/includes/specials/SpecialBrokenRedirects.php @@ -33,9 +33,9 @@ class BrokenRedirectsPage extends PageQueryPage { rd_namespace, rd_title FROM $redirect AS rd - JOIN $page p1 ON (rd.rd_from=p1.page_id) + JOIN $page p1 ON (rd.rd_from=p1.page_id) LEFT JOIN $page AS p2 ON (rd_namespace=p2.page_namespace AND rd_title=p2.page_title ) - WHERE rd_namespace >= 0 + WHERE rd_namespace >= 0 AND p2.page_namespace IS NULL"; return $sql; } @@ -45,7 +45,7 @@ class BrokenRedirectsPage extends PageQueryPage { } function formatResult( $skin, $result ) { - global $wgUser, $wgContLang; + global $wgUser, $wgContLang, $wgLang; $fromObj = Title::makeTitle( $result->namespace, $result->title ); if ( isset( $result->rd_title ) ) { @@ -61,21 +61,43 @@ class BrokenRedirectsPage extends PageQueryPage { // $toObj may very easily be false if the $result list is cached if ( !is_object( $toObj ) ) { - return '<s>' . $skin->makeLinkObj( $fromObj ) . '</s>'; + return '<s>' . $skin->link( $fromObj ) . '</s>'; } - $from = $skin->makeKnownLinkObj( $fromObj ,'', 'redirect=no' ); - $edit = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-edit' ), 'action=edit' ); - $to = $skin->makeBrokenLinkObj( $toObj ); + $from = $skin->linkKnown( + $fromObj, + null, + array(), + array( 'redirect' => 'no' ) + ); + $links = array(); + $links[] = $skin->linkKnown( + $fromObj, + wfMsgHtml( 'brokenredirects-edit' ), + array(), + array( 'action' => 'edit' ) + ); + $to = $skin->link( + $toObj, + null, + array(), + array(), + array( 'broken' ) + ); $arr = $wgContLang->getArrow(); - $out = "{$from} {$edit}"; + $out = $from . wfMsg( 'word-separator' ); if( $wgUser->isAllowed( 'delete' ) ) { - $delete = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-delete' ), 'action=delete' ); - $out .= " {$delete}"; + $links[] = $skin->linkKnown( + $fromObj, + wfMsgHtml( 'brokenredirects-delete' ), + array(), + array( 'action' => 'delete' ) + ); } + $out .= wfMsg( 'parentheses', $wgLang->pipeList( $links ) ); $out .= " {$arr} {$to}"; return $out; } diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php index c6e73f2b..a649eafd 100644 --- a/includes/specials/SpecialCategories.php +++ b/includes/specials/SpecialCategories.php @@ -13,9 +13,10 @@ function wfSpecialCategories( $par=null ) { $from = $par; } $cap = new CategoryPager( $from ); + $cap->doQuery(); $wgOut->addHTML( XML::openElement( 'div', array('class' => 'mw-spcontent') ) . - wfMsgExt( 'categoriespagetext', array( 'parse' ) ) . + wfMsgExt( 'categoriespagetext', array( 'parse' ), $cap->getNumRows() ) . $cap->getStartForm( $from ) . $cap->getNavigationBar() . '<ul>' . $cap->getBody() . '</ul>' . @@ -35,10 +36,7 @@ class CategoryPager extends AlphabeticPager { parent::__construct(); $from = str_replace( ' ', '_', $from ); if( $from !== '' ) { - global $wgCapitalLinks, $wgContLang; - if( $wgCapitalLinks ) { - $from = $wgContLang->ucfirst( $from ); - } + $from = Title::capitalize( $from, NS_CATEGORY ); $this->mOffset = $from; } } @@ -74,9 +72,6 @@ class CategoryPager extends AlphabeticPager { /* Override getBody to apply LinksBatch on resultset before actually outputting anything. */ public function getBody() { - if (!$this->mQueryDone) { - $this->doQuery(); - } $batch = new LinkBatch; $this->mResult->rewind(); @@ -92,7 +87,7 @@ class CategoryPager extends AlphabeticPager { function formatRow($result) { global $wgLang; $title = Title::makeTitle( NS_CATEGORY, $result->cat_title ); - $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) ); + $titleText = $this->getSkin()->link( $title, htmlspecialchars( $title->getText() ) ); $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->cat_pages ) ); return Xml::tags('li', null, "$titleText ($count)" ) . "\n"; diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php index 9c6f857d..372a574c 100644 --- a/includes/specials/SpecialConfirmemail.php +++ b/includes/specials/SpecialConfirmemail.php @@ -34,10 +34,13 @@ class EmailConfirmation extends UnlistedSpecialPage { } } else { $title = SpecialPage::getTitleFor( 'Userlogin' ); - $self = SpecialPage::getTitleFor( 'Confirmemail' ); $skin = $wgUser->getSkin(); - $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), - 'returnto=' . $self->getPrefixedUrl() ); + $llink = $skin->linkKnown( + $title, + wfMsgHtml( 'loginreqlink' ), + array(), + array( 'returnto' => $this->getTitle()->getPrefixedText() ) + ); $wgOut->addHTML( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) ); } } else { @@ -68,11 +71,10 @@ class EmailConfirmation extends UnlistedSpecialPage { $wgOut->addWikiMsg( 'emailauthenticated', $time, $d, $t ); } if( $wgUser->isEmailConfirmationPending() ) { - $wgOut->wrapWikiMsg( "<div class=\"error mw-confirmemail-pending\">$1</div>", 'confirmemail_pending' ); + $wgOut->wrapWikiMsg( "<div class=\"error mw-confirmemail-pending\">\n$1</div>", 'confirmemail_pending' ); } $wgOut->addWikiMsg( 'confirmemail_text' ); - $self = SpecialPage::getTitleFor( 'Confirmemail' ); - $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) ); + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalUrl() ) ); $form .= Xml::hidden( 'token', $wgUser->editToken() ); $form .= Xml::submitButton( wfMsg( 'confirmemail_send' ) ); $form .= Xml::closeElement( 'form' ); diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 9263336e..392f4332 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -39,7 +39,7 @@ class SpecialContributions extends SpecialPage { return; } - $this->opts['limit'] = $wgRequest->getInt( 'limit', 50 ); + $this->opts['limit'] = $wgRequest->getInt( 'limit', $wgUser->getOption('rclimit') ); $this->opts['target'] = $target; $nt = Title::makeTitleSafe( NS_USER, $target ); @@ -89,104 +89,161 @@ class SpecialContributions extends SpecialPage { return $this->feed( $feedType ); } - wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id ); + if ( wfRunHooks( 'SpecialContributionsBeforeMainOutput', array( $id ) ) ) { - $wgOut->addHTML( $this->getForm() ); + $wgOut->addHTML( $this->getForm() ); - $pager = new ContribsPager( $target, $this->opts['namespace'], $this->opts['year'], $this->opts['month'] ); - if( !$pager->getNumRows() ) { - $wgOut->addWikiMsg( 'nocontribs', $target ); - return; - } + $pager = new ContribsPager( $target, $this->opts['namespace'], $this->opts['year'], $this->opts['month'] ); + if( !$pager->getNumRows() ) { + $wgOut->addWikiMsg( 'nocontribs', $target ); + } else { + # Show a message about slave lag, if applicable + if( ( $lag = $pager->getDatabase()->getLag() ) > 0 ) + $wgOut->showLagWarning( $lag ); + + $wgOut->addHTML( + '<p>' . $pager->getNavigationBar() . '</p>' . + $pager->getBody() . + '<p>' . $pager->getNavigationBar() . '</p>' + ); + } - # Show a message about slave lag, if applicable - if( ( $lag = $pager->getDatabase()->getLag() ) > 0 ) - $wgOut->showLagWarning( $lag ); - $wgOut->addHTML( - '<p>' . $pager->getNavigationBar() . '</p>' . - $pager->getBody() . - '<p>' . $pager->getNavigationBar() . '</p>' - ); + # Show the appropriate "footer" message - WHOIS tools, etc. + if( $target != 'newbies' ) { + $message = 'sp-contributions-footer'; + if ( IP::isIPAddress( $target ) ) { + $message = 'sp-contributions-footer-anon'; + } else { + $user = User::newFromName( $target ); + if ( !$user || $user->isAnon() ) { + // No message for non-existing users + return; + } + } - # If there were contributions, and it was a valid user or IP, show - # the appropriate "footer" message - WHOIS tools, etc. - if( $target != 'newbies' ) { - $message = IP::isIPAddress( $target ) ? - 'sp-contributions-footer-anon' : 'sp-contributions-footer'; - - $text = wfMsgNoTrans( $message, $target ); - if( !wfEmptyMsg( $message, $text ) && $text != '-' ) { - $wgOut->addHTML( '<div class="mw-contributions-footer">' ); - $wgOut->addWikiText( $text ); - $wgOut->addHTML( '</div>' ); + $text = wfMsgNoTrans( $message, $target ); + if( !wfEmptyMsg( $message, $text ) && $text != '-' ) { + $wgOut->wrapWikiMsg( + "<div class='mw-contributions-footer'>\n$1\n</div>", + array( $message, $target ) ); + } } } } protected function setSyndicated() { global $wgOut; - $queryParams = array( - 'namespace' => $this->opts['namespace'], - 'target' => $this->opts['target'] - ); $wgOut->setSyndicated( true ); - $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) ); + $wgOut->setFeedAppendQuery( wfArrayToCGI( $this->opts ) ); } /** - * Generates the subheading with links - * @param Title $nt Title object for the target - * @param integer $id User ID for the target - * @return String: appropriately-escaped HTML to be output literally - */ + * Generates the subheading with links + * @param Title $nt @see Title object for the target + * @param integer $id User ID 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( $nt, $id ) { - global $wgSysopUserBans, $wgLang, $wgUser; + global $wgSysopUserBans, $wgLang, $wgUser, $wgOut; $sk = $wgUser->getSkin(); - if( 0 == $id ) { - $user = $nt->getText(); + if ( $id === null ) { + $user = htmlspecialchars( $nt->getText() ); } else { - $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) ); + $user = $sk->link( $nt, htmlspecialchars( $nt->getText() ) ); } + $userObj = User::newFromName( $nt->getText(), /* check for username validity not needed */ false ); $talk = $nt->getTalkPage(); if( $talk ) { # Talk page link - $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) ); - if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && IP::isIPAddress( $nt->getText() ) ) ) { - # Block link - if( $wgUser->isAllowed( 'block' ) ) - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', - $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) ); + $tools[] = $sk->link( $talk, wfMsgHtml( 'sp-contributions-talk' ) ); + if( ( $id !== null && $wgSysopUserBans ) || ( $id === null && IP::isIPAddress( $nt->getText() ) ) ) { + if( $wgUser->isAllowed( 'block' ) ) { # Block / Change block / Unblock links + if ( $userObj->isBlocked() ) { + $tools[] = $sk->linkKnown( # Change block link + SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), + wfMsgHtml( 'change-blocklink' ) + ); + $tools[] = $sk->linkKnown( # Unblock link + SpecialPage::getTitleFor( 'BlockList' ), + wfMsgHtml( 'unblocklink' ), + array(), + array( + 'action' => 'unblock', + 'ip' => $nt->getDBkey() + ) + ); + } + else { # User is not blocked + $tools[] = $sk->linkKnown( # Block link + SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), + wfMsgHtml( 'blocklink' ) + ); + } + } # Block log link - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), - wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() ); + $tools[] = $sk->linkKnown( + SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'sp-contributions-blocklog' ), + array(), + array( + 'type' => 'block', + 'page' => $nt->getPrefixedText() + ) + ); } # Other logs link - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsg( 'sp-contributions-logs' ), - 'user=' . $nt->getPartialUrl() ); + $tools[] = $sk->linkKnown( + SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'sp-contributions-logs' ), + array(), + array( 'user' => $nt->getText() ) + ); # Add link to deleted user contributions for priviledged users if( $wgUser->isAllowed( 'deletedhistory' ) ) { - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'DeletedContributions', - $nt->getDBkey() ), wfMsgHtml( 'deletedcontributions' ) ); + $tools[] = $sk->linkKnown( + SpecialPage::getTitleFor( 'DeletedContributions', $nt->getDBkey() ), + wfMsgHtml( 'sp-contributions-deleted' ) + ); } # Add a link to change user rights for privileged users $userrightsPage = new UserrightsPage(); - if( 0 !== $id && $userrightsPage->userCanChangeRights( User::newFromId( $id ) ) ) { - $tools[] = $sk->makeKnownLinkObj( + if( $id !== null && $userrightsPage->userCanChangeRights( User::newFromId( $id ) ) ) { + $tools[] = $sk->linkKnown( SpecialPage::getTitleFor( 'Userrights', $nt->getDBkey() ), - wfMsgHtml( 'userrights' ) + wfMsgHtml( 'sp-contributions-userrights' ) ); } wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) ); - + $links = $wgLang->pipeList( $tools ); + + // Show a note if the user is blocked and display the last block log entry. + if ( $userObj->isBlocked() ) { + LogEventsList::showLogExtract( + $wgOut, + 'block', + $nt->getPrefixedText(), + '', + array( + 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => array( + 'sp-contributions-blocked-notice', + $nt->getText() # Support GENDER in 'sp-contributions-blocked-notice' + ), + 'offset' => '' # don't use $wgRequest parameter offset + ) + ); + } } - + // Old message 'contribsub' had one parameter, but that doesn't work for // languages that want to put the "for" bit right after $user but before // $links. If 'contribsub' is around, use it for reverse compatibility, @@ -203,9 +260,9 @@ class SpecialContributions extends SpecialPage { * @param $this->opts Array: the options to be included. */ protected function getForm() { - global $wgScript, $wgTitle; + global $wgScript; - $this->opts['title'] = $wgTitle->getPrefixedText(); + $this->opts['title'] = $this->getTitle()->getPrefixedText(); if( !isset( $this->opts['target'] ) ) { $this->opts['target'] = ''; } else { @@ -249,11 +306,14 @@ class SpecialContributions extends SpecialPage { $f .= '<fieldset>' . Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) . - Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), + Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parsemag' ) ), 'contribs', 'newbie' , 'newbie', $this->opts['contribs'] == 'newbie' ? true : false ) . '<br />' . - Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), + Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parsemag' ) ), 'contribs' , 'user', 'user', $this->opts['contribs'] == 'user' ? true : false ) . ' ' . - Xml::input( 'target', 20, $this->opts['target']) . ' '. + Html::input( 'target', $this->opts['target'], 'text', array( + 'size' => '20', + 'required' => '' + ) + ( $this->opts['target'] ? array() : array( 'autofocus' ) ) ) . ' '. '<span style="white-space: nowrap">' . Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' . Xml::namespaceSelector( $this->opts['namespace'], '' ) . @@ -268,7 +328,7 @@ class SpecialContributions extends SpecialPage { $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' ); if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) ) - $f .= "<p>{$explain}</p>"; + $f .= "<p id='mw-sp-contributions-explain'>{$explain}</p>"; $f .= '</fieldset>' . Xml::closeElement( 'form' ); @@ -341,7 +401,7 @@ class SpecialContributions extends SpecialPage { $comments ); } else { - return NULL; + return null; } } @@ -371,9 +431,13 @@ class ContribsPager extends ReverseChronologicalPager { function __construct( $target, $namespace = false, $year = false, $month = false, $tagFilter = false ) { parent::__construct(); - foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist newpageletter minoreditletter' ) as $msg ) { - $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') ); + + $msgs = array( 'uctop', 'diff', 'newarticle', 'rollbacklink', 'diff', 'hist', 'rev-delundel', 'pipe-separator' ); + + foreach( $msgs as $msg ) { + $this->messages[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) ); } + $this->target = $target; $this->namespace = $namespace; $this->tagFilter = $tagFilter; @@ -395,8 +459,11 @@ class ContribsPager extends ReverseChronologicalPager { $conds = array_merge( $userCond, $this->getNamespaceCond() ); // Paranoia: avoid brute force searches (bug 17342) - if( !$wgUser->isAllowed( 'suppressrevision' ) ) { - $conds[] = 'rev_deleted & ' . Revision::DELETED_USER . ' = 0'; + if( !$wgUser->isAllowed( 'deletedhistory' ) ) { + $conds[] = $this->mDb->bitAnd('rev_deleted',Revision::DELETED_USER) . ' = 0'; + } else if( !$wgUser->isAllowed( 'suppressrevision' ) ) { + $conds[] = $this->mDb->bitAnd('rev_deleted',Revision::SUPPRESSED_USER) . + ' != ' . Revision::SUPPRESSED_USER; } $join_cond['page'] = array( 'INNER JOIN', 'page_id=rev_page' ); @@ -411,14 +478,16 @@ class ContribsPager extends ReverseChronologicalPager { 'options' => array( 'USE INDEX' => array('revision' => $index) ), 'join_conds' => $join_cond ); - - ChangeTags::modifyDisplayQuery( $queryInfo['tables'], - $queryInfo['fields'], - $queryInfo['conds'], - $queryInfo['join_conds'], - $queryInfo['options'], - $this->tagFilter ); - + + ChangeTags::modifyDisplayQuery( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + $queryInfo['join_conds'], + $queryInfo['options'], + $this->tagFilter + ); + wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) ); return $queryInfo; } @@ -473,7 +542,7 @@ class ContribsPager extends ReverseChronologicalPager { * @todo This would probably look a lot nicer in a table. */ function formatRow( $row ) { - global $wgLang, $wgUser, $wgContLang; + global $wgUser, $wgLang, $wgContLang; wfProfileIn( __METHOD__ ); $sk = $this->getSkin(); @@ -482,60 +551,101 @@ class ContribsPager extends ReverseChronologicalPager { $page = Title::newFromRow( $row ); $page->resetArticleId( $row->rev_page ); // use process cache - $link = $sk->makeLinkObj( $page, $page->getPrefixedText(), $page->isRedirect() ? 'redirect=no' : '' ); + $link = $sk->link( + $page, + htmlspecialchars( $page->getPrefixedText() ), + array(), + $page->isRedirect() ? array( 'redirect' => 'no' ) : array() + ); # Mark current revisions $difftext = $topmarktext = ''; if( $row->rev_id == $row->page_latest ) { - $topmarktext .= '<strong>' . $this->messages['uctop'] . '</strong>'; - if( !$row->page_is_new ) { - $difftext .= '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=0' ) . ')'; - # Add rollback link - if( $page->quickUserCan( 'rollback') && $page->quickUserCan( 'edit' ) ) { - $topmarktext .= ' '.$sk->generateRollback( $rev ); - } - } else { - $difftext .= $this->messages['newarticle']; + $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; + # Add rollback link + if( !$row->page_is_new && $page->quickUserCan( 'rollback' ) + && $page->quickUserCan( 'edit' ) ) + { + $topmarktext .= ' '.$sk->generateRollback( $rev ); } } # Is there a visible previous revision? - if( $rev->userCan(Revision::DELETED_TEXT) ) { - $difftext = '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], - 'diff=prev&oldid='.$row->rev_id ) . ')'; + if( $rev->userCan( Revision::DELETED_TEXT ) && $rev->getParentId() !== 0 ) { + $difftext = $sk->linkKnown( + $page, + $this->messages['diff'], + array(), + array( + 'diff' => 'prev', + 'oldid' => $row->rev_id + ) + ); } else { - $difftext = '(' . $this->messages['diff'] . ')'; + $difftext = $this->messages['diff']; } - $histlink = '('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')'; + $histlink = $sk->linkKnown( + $page, + $this->messages['hist'], + array(), + array( 'action' => 'history' ) + ); $comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true ); $date = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true ); - $d = $sk->makeKnownLinkObj( $page, $date, 'oldid='.intval($row->rev_id) ); + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $d = '<span class="history-deleted">' . $date . '</span>'; + } else { + $d = $sk->linkKnown( + $page, + htmlspecialchars($date), + array(), + array( 'oldid' => intval( $row->rev_id ) ) + ); + } if( $this->target == 'newbies' ) { $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text ); - $userlink .= ' (' . $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) . ') '; + $userlink .= ' ' . wfMsg( 'parentheses', $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) ) . ' '; } else { $userlink = ''; } - if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - $d = '<span class="history-deleted">' . $d . '</span>'; - } - if( $rev->getParentId() === 0 ) { - $nflag = '<span class="newpage">' . $this->messages['newpageletter'] . '</span>'; + $nflag = ChangesList::flag( 'newpage' ); } else { $nflag = ''; } if( $rev->isMinor() ) { - $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> '; + $mflag = ChangesList::flag( 'minor' ); } else { $mflag = ''; } - $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}"; - if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - $ret .= ' ' . wfMsgHtml( 'deletedrev' ); + // Don't show useless link to people who cannot hide revisions + $canHide = $wgUser->isAllowed( 'deleterevision' ); + if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) { + if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { + $del = $this->mSkin->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops + } else { + $query = array( + 'type' => 'revision', + 'target' => $page->getPrefixedDbkey(), + 'ids' => $rev->getId() + ); + $del = $this->mSkin->revDeleteLink( $query, + $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide ); + } + $del .= ' '; + } else { + $del = ''; + } + + $diffHistLinks = '(' . $difftext . $this->messages['pipe-separator'] . $histlink . ')'; + $ret = "{$del}{$d} {$diffHistLinks} {$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}"; + + # Denote if username is redacted for this edit + if( $rev->isDeleted( Revision::DELETED_USER ) ) { + $ret .= " <strong>" . wfMsgHtml('rev-deleted-user-contribs') . "</strong>"; } # Tags, if any. diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php index 67b05ca1..8884bb22 100644 --- a/includes/specials/SpecialDeletedContributions.php +++ b/includes/specials/SpecialDeletedContributions.php @@ -11,8 +11,9 @@ class DeletedContribsPager extends IndexPager { function __construct( $target, $namespace = false ) { parent::__construct(); - foreach( explode( ' ', 'deletionlog undeletebtn minoreditletter diff' ) as $msg ) { - $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') ); + $msgs = array( 'deletionlog', 'undeleteviewlink', 'diff' ); + foreach( $msgs as $msg ) { + $this->messages[$msg] = wfMsgExt( $msg, array( 'escapenoentities') ); } $this->target = $target; $this->namespace = $namespace; @@ -30,8 +31,11 @@ class DeletedContribsPager extends IndexPager { list( $index, $userCond ) = $this->getUserCond(); $conds = array_merge( $userCond, $this->getNamespaceCond() ); // Paranoia: avoid brute force searches (bug 17792) - if( !$wgUser->isAllowed( 'suppressrevision' ) ) { - $conds[] = 'ar_deleted & ' . Revision::DELETED_USER . ' = 0'; + if( !$wgUser->isAllowed( 'deletedhistory' ) ) { + $conds[] = $this->mDb->bitAnd('ar_deleted',Revision::DELETED_USER) . ' = 0'; + } else if( !$wgUser->isAllowed( 'suppressrevision' ) ) { + $conds[] = $this->mDb->bitAnd('ar_deleted',Revision::SUPPRESSED_USER) . + ' != ' . Revision::SUPPRESSED_USER; } return array( 'tables' => array( 'archive' ), @@ -71,9 +75,10 @@ class DeletedContribsPager extends IndexPager { if ( isset( $this->mNavigationBar ) ) { return $this->mNavigationBar; } + $fmtLimit = $wgLang->formatNum( $this->mLimit ); $linkTexts = array( - 'prev' => wfMsgHtml( 'pager-newer-n', $this->mLimit ), - 'next' => wfMsgHtml( 'pager-older-n', $this->mLimit ), + 'prev' => wfMsgExt( 'pager-newer-n', array( 'escape', 'parsemag' ), $fmtLimit ), + 'next' => wfMsgExt( 'pager-older-n', array( 'escape', 'parsemag' ), $fmtLimit ), 'first' => wfMsgHtml( 'histlast' ), 'last' => wfMsgHtml( 'histfirst' ) ); @@ -83,7 +88,7 @@ class DeletedContribsPager extends IndexPager { $limits = $wgLang->pipeList( $limitLinks ); $this->mNavigationBar = "(" . $wgLang->pipeList( array( $pagingLinks['first'], $pagingLinks['last'] ) ) . ") " . - wfMsgExt( 'viewprevnext', array( 'parsemag' ), $pagingLinks['prev'], $pagingLinks['next'], $limits ); + wfMsgExt( 'viewprevnext', array( 'parsemag', 'escape', 'replaceafter' ), $pagingLinks['prev'], $pagingLinks['next'], $limits ); return $this->mNavigationBar; } @@ -106,10 +111,9 @@ class DeletedContribsPager extends IndexPager { * @todo This would probably look a lot nicer in a table. */ function formatRow( $row ) { + global $wgUser, $wgLang; wfProfileIn( __METHOD__ ); - global $wgLang, $wgUser; - $sk = $this->getSkin(); $rev = new Revision( array( @@ -119,7 +123,7 @@ class DeletedContribsPager extends IndexPager { 'user_text' => $row->ar_user_text, 'timestamp' => $row->ar_timestamp, 'minor_edit' => $row->ar_minor_edit, - 'deleted' => $row->ar_deleted, + 'deleted' => $row->ar_deleted, ) ); $page = Title::makeTitle( $row->ar_namespace, $row->ar_title ); @@ -127,50 +131,96 @@ class DeletedContribsPager extends IndexPager { $undelete = SpecialPage::getTitleFor( 'Undelete' ); $logs = SpecialPage::getTitleFor( 'Log' ); - $dellog = $sk->makeKnownLinkObj( $logs, + $dellog = $sk->linkKnown( + $logs, $this->messages['deletionlog'], - 'type=delete&page=' . $page->getPrefixedUrl() ); - - $reviewlink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ), - $this->messages['undeletebtn'] ); + array(), + array( + 'type' => 'delete', + 'page' => $page->getPrefixedText() + ) + ); - $link = $sk->makeKnownLinkObj( $undelete, - htmlspecialchars( $page->getPrefixedText() ), - 'target=' . $page->getPrefixedUrl() . - '×tamp=' . $rev->getTimestamp() ); + $reviewlink = $sk->linkKnown( + SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ), + $this->messages['undeleteviewlink'] + ); - $last = $sk->makeKnownLinkObj( $undelete, - $this->messages['diff'], - "target=" . $page->getPrefixedUrl() . - "×tamp=" . $rev->getTimestamp() . - "&diff=prev" ); + if( $wgUser->isAllowed('deletedtext') ) { + $last = $sk->linkKnown( + $undelete, + $this->messages['diff'], + array(), + array( + 'target' => $page->getPrefixedText(), + 'timestamp' => $rev->getTimestamp(), + 'diff' => 'prev' + ) + ); + } else { + $last = $this->messages['diff']; + } $comment = $sk->revComment( $rev ); - $d = $wgLang->timeanddate( $rev->getTimestamp(), true ); + $date = htmlspecialchars( $wgLang->timeanddate( $rev->getTimestamp(), true ) ); - if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - $d = '<span class="history-deleted">' . $d . '</span>'; + if( !$wgUser->isAllowed('undelete') || !$rev->userCan(Revision::DELETED_TEXT) ) { + $link = $date; // unusable link } else { - $link = $sk->makeKnownLinkObj( $undelete, $d, - 'target=' . $page->getPrefixedUrl() . - '×tamp=' . $rev->getTimestamp() ); + $link = $sk->linkKnown( + $undelete, + $date, + array(), + array( + 'target' => $page->getPrefixedText(), + 'timestamp' => $rev->getTimestamp() + ) + ); + } + // Style deleted items + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $link = '<span class="history-deleted">' . $link . '</span>'; } - $pagelink = $sk->makeLinkObj( $page ); + $pagelink = $sk->link( $page ); if( $rev->isMinor() ) { - $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> '; + $mflag = ChangesList::flag( 'minor' ); } else { $mflag = ''; } + + // Revision delete link + $canHide = $wgUser->isAllowed( 'deleterevision' ); + if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) { + if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { + $del = $this->mSkin->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops + } else { + $query = array( + 'type' => 'archive', + 'target' => $page->getPrefixedDbkey(), + 'ids' => $rev->getTimestamp() ); + $del = $this->mSkin->revDeleteLink( $query, + $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide ) . ' '; + } + } else { + $del = ''; + } - - $ret = "{$link} ($last) ({$dellog}) ({$reviewlink}) . . {$mflag} {$pagelink} {$comment}"; - if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - $ret .= ' ' . wfMsgHtml( 'deletedrev' ); + $tools = Html::rawElement( + 'span', + array( 'class' => 'mw-deletedcontribs-tools' ), + wfMsg( 'parentheses', $wgLang->pipeList( array( $last, $dellog, $reviewlink ) ) ) + ); + + $ret = "{$del}{$link} {$tools} . . {$mflag} {$pagelink} {$comment}"; + + # Denote if username is redacted for this edit + if( $rev->isDeleted( Revision::DELETED_USER ) ) { + $ret .= " <strong>" . wfMsgHtml('rev-deleted-user-contribs') . "</strong>"; } - $ret = "<li>$ret</li>\n"; + $ret = Html::rawElement( 'li', array(), $ret ) . "\n"; wfProfileOut( __METHOD__ ); return $ret; @@ -208,7 +258,7 @@ class DeletedContributionsPage extends SpecialPage { return; } - global $wgUser, $wgOut, $wgLang, $wgRequest; + global $wgOut, $wgLang, $wgRequest; $wgOut->setPageTitle( wfMsgExt( 'deletedcontributions-title', array( 'parsemag' ) ) ); @@ -248,7 +298,7 @@ class DeletedContributionsPage extends SpecialPage { $pager = new DeletedContribsPager( $target, $options['namespace'] ); if ( !$pager->getNumRows() ) { - $wgOut->addWikiText( wfMsg( 'nocontribs' ) ); + $wgOut->addWikiMsg( 'nocontribs' ); return; } @@ -271,50 +321,112 @@ class DeletedContributionsPage extends SpecialPage { $text = wfMsgNoTrans( $message, $target ); if( !wfEmptyMsg( $message, $text ) && $text != '-' ) { - $wgOut->addHTML( '<div class="mw-contributions-footer">' ); - $wgOut->addWikiText( $text ); - $wgOut->addHTML( '</div>' ); + $wgOut->wrapWikiMsg( "<div class='mw-contributions-footer'>\n$1\n</div>", array( $message, $target ) ); } } } /** * Generates the subheading with links - * @param $nt @see Title object for the target + * @param Title $nt @see Title object for the target + * @param integer $id User ID 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( $nt, $id ) { - global $wgSysopUserBans, $wgLang, $wgUser; + global $wgSysopUserBans, $wgLang, $wgUser, $wgOut; $sk = $wgUser->getSkin(); - if ( 0 == $id ) { - $user = $nt->getText(); + if ( $id === null ) { + $user = htmlspecialchars( $nt->getText() ); } else { - $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) ); + $user = $sk->link( $nt, htmlspecialchars( $nt->getText() ) ); } + $userObj = User::newFromName( $nt->getText(), /* check for username validity not needed */ false ); $talk = $nt->getTalkPage(); if( $talk ) { # Talk page link - $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) ); - if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) { - # Block link - if( $wgUser->isAllowed( 'block' ) ) - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), - wfMsgHtml( 'blocklink' ) ); + $tools[] = $sk->link( $talk, wfMsgHtml( 'sp-contributions-talk' ) ); + if( ( $id !== null && $wgSysopUserBans ) || ( $id === null && IP::isIPAddress( $nt->getText() ) ) ) { + if( $wgUser->isAllowed( 'block' ) ) { # Block / Change block / Unblock links + if ( $userObj->isBlocked() ) { + $tools[] = $sk->linkKnown( # Change block link + SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), + wfMsgHtml( 'change-blocklink' ) + ); + $tools[] = $sk->linkKnown( # Unblock link + SpecialPage::getTitleFor( 'BlockList' ), + wfMsgHtml( 'unblocklink' ), + array(), + array( + 'action' => 'unblock', + 'ip' => $nt->getDBkey() + ) + ); + } + else { # User is not blocked + $tools[] = $sk->linkKnown( # Block link + SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), + wfMsgHtml( 'blocklink' ) + ); + } + } # Block log link - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), - wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() ); + $tools[] = $sk->linkKnown( + SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'sp-contributions-blocklog' ), + array(), + array( + 'type' => 'block', + 'page' => $nt->getPrefixedText() + ) + ); } # Other logs link - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), - wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() ); - # Link to undeleted contributions - $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $nt->getDBkey() ), - wfMsgHtml( 'contributions' ) ); - + $tools[] = $sk->linkKnown( + SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'sp-contributions-logs' ), + array(), + array( 'user' => $nt->getText() ) + ); + # Link to contributions + $tools[] = $sk->linkKnown( + SpecialPage::getTitleFor( 'Contributions', $nt->getDBkey() ), + wfMsgHtml( 'sp-deletedcontributions-contribs' ) + ); + + # Add a link to change user rights for privileged users + $userrightsPage = new UserrightsPage(); + if( $id !== null && $userrightsPage->userCanChangeRights( User::newFromId( $id ) ) ) { + $tools[] = $sk->linkKnown( + SpecialPage::getTitleFor( 'Userrights', $nt->getDBkey() ), + wfMsgHtml( 'sp-contributions-userrights' ) + ); + } + wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) ); $links = $wgLang->pipeList( $tools ); + + // Show a note if the user is blocked and display the last block log entry. + if ( $userObj->isBlocked() ) { + LogEventsList::showLogExtract( + $wgOut, + 'block', + $nt->getPrefixedText(), + '', + array( + 'lim' => 1, + 'showIfEmpty' => false, + 'msgKey' => array( + 'sp-contributions-blocked-notice', + $nt->getText() # Support GENDER in 'sp-contributions-blocked-notice' + ), + 'offset' => '' # don't use $wgRequest parameter offset + ) + ); + } } // Old message 'contribsub' had one parameter, but that doesn't work for @@ -333,9 +445,9 @@ class DeletedContributionsPage extends SpecialPage { * @param $options Array: the options to be included. */ function getForm( $options ) { - global $wgScript, $wgTitle, $wgRequest; + global $wgScript, $wgRequest; - $options['title'] = $wgTitle->getPrefixedText(); + $options['title'] = SpecialPage::getTitleFor( 'DeletedContributions' )->getPrefixedText(); if ( !isset( $options['target'] ) ) { $options['target'] = ''; } else { @@ -366,7 +478,10 @@ class DeletedContributionsPage extends SpecialPage { $f .= Xml::openElement( 'fieldset' ) . Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) . Xml::tags( 'label', array( 'for' => 'target' ), wfMsgExt( 'sp-contributions-username', 'parseinline' ) ) . ' ' . - Xml::input( 'target', 20, $options['target']) . ' '. + Html::input( 'target', $options['target'], 'text', array( + 'size' => '20', + 'required' => '' + ) + ( $options['target'] ? array() : array( 'autofocus' ) ) ) . ' '. Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' . Xml::namespaceSelector( $options['namespace'], '' ) . ' ' . Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) . diff --git a/includes/specials/SpecialDisambiguations.php b/includes/specials/SpecialDisambiguations.php index 0a728b68..1941112a 100644 --- a/includes/specials/SpecialDisambiguations.php +++ b/includes/specials/SpecialDisambiguations.php @@ -88,7 +88,7 @@ class DisambiguationsPage extends PageQueryPage { $dp = Title::makeTitle( $result->namespace, $result->title ); $from = $skin->link( $title ); - $edit = $skin->link( $title, "(".wfMsgHtml("qbedit").")", array(), array( 'redirect' => 'no', 'action' => 'edit' ) ); + $edit = $skin->link( $title, wfMsgExt( 'parentheses', array( 'escape' ), wfMsg( 'editlink' ) ) , array(), array( 'redirect' => 'no', 'action' => 'edit' ) ); $arr = $wgContLang->getArrow(); $to = $skin->link( $dp ); diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php index b1bad0c3..893fee9e 100644 --- a/includes/specials/SpecialDoubleRedirects.php +++ b/includes/specials/SpecialDoubleRedirects.php @@ -74,16 +74,34 @@ class DoubleRedirectsPage extends PageQueryPage { } } if ( !$result ) { - return '<s>' . $skin->makeLinkObj( $titleA, '', 'redirect=no' ) . '</s>'; + return '<s>' . $skin->link( $titleA, null, array(), array( 'redirect' => 'no' ) ) . '</s>'; } $titleB = Title::makeTitle( $result->nsb, $result->tb ); $titleC = Title::makeTitle( $result->nsc, $result->tc ); - $linkA = $skin->makeKnownLinkObj( $titleA, '', 'redirect=no' ); - $edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no'); - $linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' ); - $linkC = $skin->makeKnownLinkObj( $titleC ); + $linkA = $skin->linkKnown( + $titleA, + null, + array(), + array( 'redirect' => 'no' ) + ); + $edit = $skin->linkKnown( + $titleA, + wfMsgExt( 'parentheses', array( 'escape' ), wfMsg( 'editlink' ) ), + array(), + array( + 'redirect' => 'no', + 'action' => 'edit' + ) + ); + $linkB = $skin->linkKnown( + $titleB, + null, + array(), + array( 'redirect' => 'no' ) + ); + $linkC = $skin->linkKnown( $titleC ); $arr = $wgContLang->getArrow() . $wgContLang->getDirMark(); return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" ); diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index 58e2514e..48088ded 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -48,6 +48,12 @@ function wfSpecialEmailuser( $par ) { case 'mailnologin': $wgOut->showErrorPage( 'mailnologin', 'mailnologintext' ); return; + default: + // It's a hook error + list( $title, $msg, $params ) = $error; + $wgOut->showErrorPage( $title, $msg, $params ); + return; + } } @@ -256,7 +262,7 @@ class EmailUserForm { } static function validateEmailTarget ( $target ) { - if ( "" == $target ) { + if ( $target == "" ) { wfDebug( "Target is empty.\n" ); return "notarget"; } @@ -268,7 +274,7 @@ class EmailUserForm { } $nu = User::newFromName( $nt->getText() ); - if( is_null( $nu ) || !$nu->getId() ) { + if( !$nu instanceof User || !$nu->getId() ) { wfDebug( "Target is invalid user.\n" ); return "notarget"; } else if ( !$nu->isEmailConfirmed() ) { @@ -284,6 +290,10 @@ class EmailUserForm { static function getPermissionsError ( $user, $editToken ) { if( !$user->canSendEmail() ) { wfDebug( "User can't send.\n" ); + // FIXME: this is also the error if user is in a group + // that is not allowed to send e-mail (no right + // 'sendemail'). Error messages should probably + // be more fine grained. return "mailnologin"; } @@ -297,12 +307,17 @@ class EmailUserForm { return 'actionthrottledtext'; } + $hookErr = null; + wfRunHooks( 'EmailUserPermissionsErrors', array( $user, $editToken, &$hookErr ) ); + + if ($hookErr) { + return $hookErr; + } + if( !$user->matchEditToken( $editToken ) ) { wfDebug( "Matching edit token failed.\n" ); return 'sessionfailure'; } - - return; } static function newFromURL( $target, $text, $subject, $cc_me ) diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php index 8bf16a71..b9a44d48 100644 --- a/includes/specials/SpecialExport.php +++ b/includes/specials/SpecialExport.php @@ -44,17 +44,18 @@ class SpecialExport extends SpecialPage { $this->templates = $wgRequest->getCheck( 'templates' ); $this->images = $wgRequest->getCheck( 'images' ); // Doesn't do anything yet $this->pageLinkDepth = $this->validateLinkDepth( - $wgRequest->getIntOrNull( 'pagelink-depth' ) ); + $wgRequest->getIntOrNull( 'pagelink-depth' ) ); + $nsindex = ''; if ( $wgRequest->getCheck( 'addcat' ) ) { $page = $wgRequest->getText( 'pages' ); $catname = $wgRequest->getText( 'catname' ); - if ( $catname !== '' && $catname !== NULL && $catname !== false ) { + if ( $catname !== '' && $catname !== null && $catname !== false ) { $t = Title::makeTitleSafe( NS_MAIN, $catname ); if ( $t ) { /** - * @fixme This can lead to hitting memory limit for very large + * @todo Fixme: this can lead to hitting memory limit for very large * categories. Ideally we would do the lookup synchronously * during the export in a single query. */ @@ -65,15 +66,15 @@ class SpecialExport extends SpecialPage { } else if( $wgRequest->getCheck( 'addns' ) && $wgExportFromNamespaces ) { $page = $wgRequest->getText( 'pages' ); - $nsindex = $wgRequest->getText( 'nsindex' ); + $nsindex = $wgRequest->getText( 'nsindex', '' ); - if ( $nsindex !== '' && $nsindex !== NULL && $nsindex !== false ) { + if ( strval( $nsindex ) !== '' ) { /** - * Same implementation as above, so same @fixme + * Same implementation as above, so same @todo */ $nspages = $this->getPagesFromNamespace( $nsindex ); if ( $nspages ) $page .= "\n" . implode( "\n", $nspages ); - } + } } else if( $wgRequest->wasPosted() && $par == '' ) { $page = $wgRequest->getText( 'pages' ); @@ -87,15 +88,15 @@ class SpecialExport extends SpecialPage { $limit = $wgRequest->getInt( 'limit' ); $dir = $wgRequest->getVal( 'dir' ); $history = array( - 'dir' => 'asc', - 'offset' => false, - 'limit' => $wgExportMaxHistory, - ); + 'dir' => 'asc', + 'offset' => false, + 'limit' => $wgExportMaxHistory, + ); $historyCheck = $wgRequest->getCheck( 'history' ); if ( $this->curonly ) { $history = WikiExporter::CURRENT; } elseif ( !$historyCheck ) { - if ( $limit > 0 && $limit < $wgExportMaxHistory ) { + if ( $limit > 0 && ($wgExportMaxHistory == 0 || $limit < $wgExportMaxHistory ) ) { $history['limit'] = $limit; } if ( !is_null( $offset ) ) { @@ -146,12 +147,12 @@ class SpecialExport extends SpecialPage { $wgOut->addWikiMsg( 'exporttext' ); $form = Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $this->getTitle()->getLocalUrl( 'action=submit' ) ) ); + 'action' => $this->getTitle()->getLocalUrl( 'action=submit' ) ) ); $form .= Xml::inputLabel( wfMsg( 'export-addcattext' ) , 'catname', 'catname', 40 ) . ' '; $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />'; if ( $wgExportFromNamespaces ) { - $form .= Xml::namespaceSelector( '', null, 'nsindex', wfMsg( 'export-addnstext' ) ) . ' '; + $form .= Xml::namespaceSelector( $nsindex, null, 'nsindex', wfMsg( 'export-addnstext' ) ) . ' '; $form .= Xml::submitButton( wfMsg( 'export-addns' ), array( 'name' => 'addns' ) ) . '<br />'; } @@ -190,10 +191,22 @@ class SpecialExport extends SpecialPage { private function doExport( $page, $history, $list_authors ) { global $wgExportMaxHistory; - /* Split up the input and look up linked pages */ - $inputPages = array_filter( explode( "\n", $page ), array( $this, 'filterPage' ) ); - $pageSet = array_flip( $inputPages ); + $pageSet = array(); // Inverted index of all pages to look up + + // Split up and normalize input + foreach( explode( "\n", $page ) as $pageName ) { + $pageName = trim( $pageName ); + $title = Title::newFromText( $pageName ); + if( $title && $title->getInterwiki() == '' && $title->getText() !== '' ) { + // Only record each page once! + $pageSet[$title->getPrefixedText()] = true; + } + } + // Set of original pages to pass on to further manipulation... + $inputPages = array_keys( $pageSet ); + + // Look up any linked pages if asked... if( $this->templates ) { $pageSet = $this->getTemplates( $inputPages, $pageSet ); } @@ -210,7 +223,13 @@ class SpecialExport extends SpecialPage { */ $pages = array_keys( $pageSet ); - + + // Normalize titles to the same format and remove dupes, see bug 17374 + foreach( $pages as $k => $v ) { + $pages[$k] = str_replace( " ", "_", $v ); + } + $pages = array_unique( $pages ); + /* Ok, let's get to it... */ if( $history == WikiExporter::CURRENT ) { $lb = false; @@ -256,8 +275,7 @@ class SpecialExport extends SpecialPage { $lb->closeAll(); } } - - + private function getPagesFromCategory( $title ) { global $wgContLang; @@ -374,7 +392,7 @@ class SpecialExport extends SpecialPage { $title = Title::newFromText( $page ); if( $title ) { $pageSet[$title->getPrefixedText()] = true; - /// @fixme May or may not be more efficient to batch these + /// @todo Fixme: May or may not be more efficient to batch these /// by namespace when given multiple input pages. $result = $dbr->select( array( 'page', $table ), @@ -382,7 +400,7 @@ class SpecialExport extends SpecialPage { array_merge( $join, array( 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBKey() ) ), + 'page_title' => $title->getDBkey() ) ), __METHOD__ ); foreach( $result as $row ) { $template = Title::makeTitle( $row->namespace, $row->title ); @@ -392,12 +410,5 @@ class SpecialExport extends SpecialPage { } return $pageSet; } - - /** - * Callback function to remove empty strings from the pages array. - */ - private function filterPage( $page ) { - return $page !== '' && $page !== null; - } } diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php index afd5ad48..65d76a65 100644 --- a/includes/specials/SpecialFewestrevisions.php +++ b/includes/specials/SpecialFewestrevisions.php @@ -53,15 +53,26 @@ class FewestrevisionsPage extends QueryPage { global $wgLang, $wgContLang; $nt = Title::makeTitleSafe( $result->namespace, $result->title ); + if( !$nt ) { + return '<!-- bad title -->'; + } + $text = $wgContLang->convert( $nt->getPrefixedText() ); - $plink = $skin->makeKnownLinkObj( $nt, $text ); + $plink = $skin->linkKnown( + $nt, + $text + ); - $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'), + $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) ); - $redirect = $result->redirect ? ' - ' . wfMsg( 'isredirect' ) : ''; - $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' ) . $redirect; - + $redirect = $result->redirect ? ' - ' . wfMsgHtml( 'isredirect' ) : ''; + $nlink = $skin->linkKnown( + $nt, + $nl, + array(), + array( 'action' => 'history' ) + ) . $redirect; return wfSpecialList( $plink, $nlink ); } diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php index 4fde0a60..0ed7020a 100644 --- a/includes/specials/SpecialFileDuplicateSearch.php +++ b/includes/specials/SpecialFileDuplicateSearch.php @@ -51,9 +51,12 @@ class FileDuplicateSearchPage extends QueryPage { $nt = Title::makeTitle( NS_FILE, $result->title ); $text = $wgContLang->convert( $nt->getText() ); - $plink = $skin->makeLink( $nt->getPrefixedText(), $text ); + $plink = $skin->link( + Title::newFromText( $nt->getPrefixedText() ), + $text + ); - $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text ); + $user = $skin->link( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text ); $time = $wgLang->timeanddate( $result->img_timestamp ); return "$plink . . $user . . $time"; @@ -73,7 +76,7 @@ function wfSpecialFileDuplicateSearch( $par = null ) { if( $title && $title->getText() != '' ) { $dbr = wfGetDB( DB_SLAVE ); $image = $dbr->tableName( 'image' ); - $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDBKey() ) ); + $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDBkey() ) ); $sql = "SELECT img_sha1 from $image where img_name = $encFilename"; $res = $dbr->query( $sql ); $row = $dbr->fetchRow( $res ); @@ -96,7 +99,7 @@ function wfSpecialFileDuplicateSearch( $par = null ) { ); if( $hash != '' ) { - $align = $wgContLang->isRtl() ? 'left' : 'right'; + $align = $wgContLang->alignEnd(); # Show a thumbnail of the file $img = wfFindFile( $title ); @@ -122,14 +125,14 @@ function wfSpecialFileDuplicateSearch( $par = null ) { # Show a short summary if( $count == 1 ) { - $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-1">' . - wfMsgHtml( 'fileduplicatesearch-result-1', $filename ) . - '</p>' + $wgOut->wrapWikiMsg( + "<p class='mw-fileduplicatesearch-result-1'>\n$1\n</p>", + array( 'fileduplicatesearch-result-1', $filename ) ); } elseif ( $count > 1 ) { - $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-n">' . - wfMsgExt( 'fileduplicatesearch-result-n', array( 'parseinline' ), $filename, $wgLang->formatNum( $count - 1 ) ) . - '</p>' + $wgOut->wrapWikiMsg( + "<p class='mw-fileduplicatesearch-result-n'>\n$1\n</p>", + array( 'fileduplicatesearch-result-n', $filename, $wgLang->formatNum( $count - 1 ) ) ); } } diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php index 4a724b1f..8bc1c68b 100644 --- a/includes/specials/SpecialFilepath.php +++ b/includes/specials/SpecialFilepath.php @@ -37,13 +37,13 @@ class FilepathForm { } function execute() { - global $wgOut, $wgTitle, $wgScript; + global $wgOut, $wgScript; $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'specialfilepath' ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'filepath' ) ) . - Xml::hidden( 'title', $wgTitle->getPrefixedText() ) . + Xml::hidden( 'title', SpecialPage::getTitleFor( 'Filepath' )->getPrefixedText() ) . Xml::inputLabel( wfMsg( 'filepath-page' ), 'file', 'file', 25, is_object( $this->mTitle ) ? $this->mTitle->getText() : '' ) . ' ' . Xml::submitButton( wfMsg( 'filepath-submit' ) ) . "\n" . Xml::closeElement( 'fieldset' ) . diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php index 457e03b4..6beeab7f 100644 --- a/includes/specials/SpecialImport.php +++ b/includes/specials/SpecialImport.php @@ -132,11 +132,11 @@ class SpecialImport extends SpecialPage { } private function showForm() { - global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources, $wgExportMaxLinkDepth; + global $wgUser, $wgOut, $wgRequest, $wgImportSources, $wgExportMaxLinkDepth; if( !$wgUser->isAllowed( 'import' ) && !$wgUser->isAllowed( 'importupload' ) ) return $wgOut->permissionRequired( 'import' ); - $action = $wgTitle->getLocalUrl( 'action=submit' ); + $action = $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) ); if( $wgUser->isAllowed( 'importupload' ) ) { $wgOut->addWikiMsg( "importtext" ); @@ -273,7 +273,7 @@ class SpecialImport extends SpecialPage { * @ingroup SpecialPage */ class ImportReporter { - private $reason=false; + private $reason=false; function __construct( $importer, $upload, $interwiki , $reason=false ) { $importer->setPageOutCallback( array( $this, 'reportPage' ) ); @@ -299,7 +299,7 @@ class ImportReporter { $contentCount = $wgContLang->formatNum( $successCount ); if( $successCount > 0 ) { - $wgOut->addHTML( "<li>" . $skin->makeKnownLinkObj( $title ) . " " . + $wgOut->addHTML( "<li>" . $skin->linkKnown( $title ) . " " . wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) . "</li>\n" ); @@ -309,7 +309,7 @@ class ImportReporter { $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ), $contentCount ); if ( $this->reason ) { - $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason; + $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason; } $log->addEntry( 'upload', $title, $detail ); } else { @@ -318,7 +318,7 @@ class ImportReporter { $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ), $contentCount, $interwiki ); if ( $this->reason ) { - $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason; + $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason; } $log->addEntry( 'interwiki', $title, $detail ); } @@ -333,7 +333,8 @@ class ImportReporter { $article->updateRevisionOn( $dbw, $nullRevision ); wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) ); } else { - $wgOut->addHTML( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' ); + $wgOut->addHTML( "<li>" . $skin->linkKnown( $title ) . " " . + wfMsgHtml( 'import-nonewrevisions' ) . "</li>\n" ); } } diff --git a/includes/specials/SpecialIpblocklist.php b/includes/specials/SpecialIpblocklist.php index 4ba1c811..dfdcf1a7 100644 --- a/includes/specials/SpecialIpblocklist.php +++ b/includes/specials/SpecialIpblocklist.php @@ -5,12 +5,13 @@ */ /** + * @param $ip part of title: Special:Ipblocklist/<ip>. * @todo document */ -function wfSpecialIpblocklist() { +function wfSpecialIpblocklist( $ip = '' ) { global $wgUser, $wgOut, $wgRequest; - - $ip = trim( $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) ) ); + $ip = $wgRequest->getVal( 'ip', $ip ); + $ip = trim( $wgRequest->getVal( 'wpUnblockAddress', $ip ) ); $id = $wgRequest->getVal( 'id' ); $reason = $wgRequest->getText( 'wpUnblockReason' ); $action = $wgRequest->getText( 'action' ); @@ -94,7 +95,7 @@ class IPUnblockForm { $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); $action = $titleObj->getLocalURL( "action=submit" ); - if ( "" != $err ) { + if ( $err != "" ) { $wgOut->setSubtitle( wfMsg( "formerror" ) ); $wgOut->addWikiText( Xml::tags( 'span', array( 'class' => 'error' ), $err ) . "\n" ); } @@ -184,8 +185,7 @@ class IPUnblockForm { if ( !$block ) { return array('ipb_cant_unblock', htmlspecialchars($id)); } - if( $block->mRangeStart != $block->mRangeEnd - && !strstr( $ip, "/" ) ) { + if( $block->mRangeStart != $block->mRangeEnd && !strstr( $ip, "/" ) ) { /* If the specified IP is a single address, and the block is * a range block, don't unblock the range. */ $range = $block->mAddress; @@ -221,8 +221,7 @@ class IPUnblockForm { function doSubmit() { global $wgOut, $wgUser; $retval = self::doUnblock($this->id, $this->ip, $this->reason, $range, $wgUser); - if(!empty($retval)) - { + if( !empty($retval) ) { $key = array_shift($retval); $this->showForm(wfMsgReal($key, $retval)); return; @@ -237,7 +236,7 @@ class IPUnblockForm { global $wgOut, $wgUser; $wgOut->setPagetitle( wfMsg( "ipblocklist" ) ); - if ( "" != $msg ) { + if ( $msg != "" ) { $wgOut->setSubtitle( $msg ); } @@ -264,10 +263,9 @@ class IPUnblockForm { // Fixme -- encapsulate this sort of query-building. $dbr = wfGetDB( DB_SLAVE ); $encIp = $dbr->addQuotes( IP::sanitizeIP($this->ip) ); - $encRange = $dbr->addQuotes( "$range%" ); $encAddr = $dbr->addQuotes( $iaddr ); $conds[] = "(ipb_address = $encIp) OR - (ipb_range_start LIKE $encRange AND + (ipb_range_start" . $dbr->buildLike( $range, $dbr->anyString() ) . " AND ipb_range_start <= $encAddr AND ipb_range_end >= $encAddr)"; } else { @@ -299,25 +297,48 @@ class IPUnblockForm { $conds[] = "ipb_user != 0 OR ipb_range_end > ipb_range_start"; } + // Search form + $wgOut->addHTML( $this->searchForm() ); + + // Check for other blocks, i.e. global/tor blocks + $otherBlockLink = array(); + wfRunHooks( 'OtherBlockLogLink', array( &$otherBlockLink, $this->ip ) ); + + // Show additional header for the local block only when other blocks exists. + // Not necessary in a standard installation without such extensions enabled + if( count( $otherBlockLink ) ) { + $wgOut->addHTML( + Html::rawElement( 'h2', array(), wfMsg( 'ipblocklist-localblock' ) ) . "\n" + ); + } $pager = new IPBlocklistPager( $this, $conds ); if ( $pager->getNumRows() ) { $wgOut->addHTML( - $this->searchForm() . $pager->getNavigationBar() . Xml::tags( 'ul', null, $pager->getBody() ) . $pager->getNavigationBar() ); } elseif ( $this->ip != '') { - $wgOut->addHTML( $this->searchForm() ); $wgOut->addWikiMsg( 'ipblocklist-no-results' ); } else { - $wgOut->addHTML( $this->searchForm() ); $wgOut->addWikiMsg( 'ipblocklist-empty' ); } + + if( count( $otherBlockLink ) ) { + $wgOut->addHTML( + Html::rawElement( 'h2', array(), wfMsgExt( 'ipblocklist-otherblocks', 'parseinline', count( $otherBlockLink ) ) ) . "\n" + ); + $list = ''; + foreach( $otherBlockLink as $link ) { + $list .= Html::rawElement( 'li', array(), $link ) . "\n"; + } + $wgOut->addHTML( Html::rawElement( 'ul', array( 'class' => 'mw-ipblocklist-otherblocks' ), $list ) . "\n" ); + } + } function searchForm() { - global $wgTitle, $wgScript, $wgRequest, $wgLang; + global $wgScript, $wgRequest, $wgLang; $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ) ); $nondefaults = array(); @@ -345,7 +366,7 @@ class IPUnblockForm { return Xml::tags( 'form', array( 'action' => $wgScript ), - Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) . + Xml::hidden( 'title', SpecialPage::getTitleFor( 'Ipblocklist' )->getPrefixedDbKey() ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) . Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) . @@ -366,7 +387,7 @@ class IPUnblockForm { global $wgUser; $sk = $wgUser->getSkin(); $params = $override + $options; - $ipblocklist = SpecialPage::getTitleFor( 'IPBlockList' ); + $ipblocklist = SpecialPage::getTitleFor( 'Ipblocklist' ); return $sk->link( $ipblocklist, htmlspecialchars( $title ), ( $active ? array( 'style'=>'font-weight: bold;' ) : array() ), $params, array( 'known' ) ); } @@ -386,11 +407,10 @@ class IPUnblockForm { if( is_null( $msg ) ) { $msg = array(); $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink', 'change-blocklink', - 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock', 'blocklist-nousertalk' ); + 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock', 'blocklist-nousertalk', 'blocklistline' ); foreach( $keys as $key ) { $msg[$key] = wfMsgHtml( $key ); } - $msg['blocklistline'] = wfMsg( 'blocklistline' ); } # Prepare links to the blocker's user and talk pages @@ -407,7 +427,7 @@ class IPUnblockForm { . $sk->userToolLinks( $block->mUser, $block->mAddress, false, Linker::TOOL_LINKS_NOBLOCK ); } - $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true ); + $formattedTime = htmlspecialchars( $wgLang->timeanddate( $block->mTimestamp, true ) ); $properties = array(); $properties[] = Block::formatExpiry( $block->mExpiry ); @@ -445,7 +465,7 @@ class IPUnblockForm { # Create changeblocklink for all blocks with exception of autoblocks if( !$block->mAuto ) { - $changeblocklink = wfMsg( 'pipe-separator' ) . + $changeblocklink = wfMsgExt( 'pipe-separator', 'escapenoentities' ) . $sk->link( SpecialPage::getTitleFor( 'Blockip', $block->mAddress ), $msg['change-blocklink'], array(), array(), 'known' ); @@ -453,7 +473,7 @@ class IPUnblockForm { $toolLinks = "($unblocklink$changeblocklink)"; } - $comment = $sk->commentBlock( $block->mReason ); + $comment = $sk->commentBlock( htmlspecialchars($block->mReason) ); $s = "{$line} $comment"; if ( $block->mHideName ) diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php index 267ef690..5913f4b4 100644 --- a/includes/specials/SpecialLinkSearch.php +++ b/includes/specials/SpecialLinkSearch.php @@ -9,9 +9,7 @@ /** * Special:LinkSearch to search the external-links table. - * @ingroup SpecialPage */ - function wfSpecialLinkSearch( $par ) { list( $limit, $offset ) = wfCheckLimits(); @@ -48,7 +46,7 @@ function wfSpecialLinkSearch( $par ) { $self = Title::makeTitle( NS_SPECIAL, 'Linksearch' ); - $wgOut->addWikiText( wfMsg( 'linksearch-text', '<nowiki>' . $wgLang->commaList( $wgUrlProtocols) . '</nowiki>' ) ); + $wgOut->addWikiMsg( 'linksearch-text', '<nowiki>' . $wgLang->commaList( $wgUrlProtocols ) . '</nowiki>' ); $s = Xml::openElement( 'form', array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => $GLOBALS['wgScript'] ) ) . Xml::hidden( 'title', $self->getPrefixedDbKey() ) . '<fieldset>' . @@ -96,11 +94,11 @@ class LinkSearchPage extends QueryPage { */ static function mungeQuery( $query , $prot ) { $field = 'el_index'; - $rv = LinkFilter::makeLike( $query , $prot ); + $rv = LinkFilter::makeLikeArray( $query , $prot ); if ($rv === false) { //makeLike doesn't handle wildcard in IP, so we'll have to munge here. if (preg_match('/^(:?[0-9]{1,3}\.)+\*\s*$|^(:?[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]*\*\s*$/', $query)) { - $rv = $prot . rtrim($query, " \t*") . '%'; + $rv = array( $prot . rtrim($query, " \t*"), $dbr->anyString() ); $field = 'el_to'; } } @@ -125,8 +123,8 @@ class LinkSearchPage extends QueryPage { /* strip everything past first wildcard, so that index-based-only lookup would be done */ list( $munged, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt ); - $stripped = substr($munged,0,strpos($munged,'%')+1); - $encSearch = $dbr->addQuotes( $stripped ); + $stripped = LinkFilter::keepOneWildcard( $munged ); + $like = $dbr->buildLike( $stripped ); $encSQL = ''; if ( isset ($this->mNs) && !$wgMiserMode ) @@ -144,14 +142,14 @@ class LinkSearchPage extends QueryPage { $externallinks $use_index WHERE page_id=el_from - AND $clause LIKE $encSearch + AND $clause $like $encSQL"; } function formatResult( $skin, $result ) { $title = Title::makeTitle( $result->namespace, $result->title ); $url = $result->url; - $pageLink = $skin->makeKnownLinkObj( $title ); + $pageLink = $skin->linkKnown( $title ); $urlLink = $skin->makeExternalLink( $url, $url ); return wfMsgHtml( 'linksearch-line', $urlLink, $pageLink ); @@ -164,7 +162,7 @@ class LinkSearchPage extends QueryPage { global $wgOut; list( $this->mMungedQuery, $clause ) = LinkSearchPage::mungeQuery( $this->mQuery, $this->mProt ); if( $this->mMungedQuery === false ) { - $wgOut->addWikiText( wfMsg( 'linksearch-error' ) ); + $wgOut->addWikiMsg( 'linksearch-error' ); } else { // For debugging // Generates invalid xhtml with patterns that contain -- diff --git a/includes/specials/SpecialListUserRestrictions.php b/includes/specials/SpecialListUserRestrictions.php deleted file mode 100644 index 98e7111f..00000000 --- a/includes/specials/SpecialListUserRestrictions.php +++ /dev/null @@ -1,162 +0,0 @@ -<?php - -function wfSpecialListUserRestrictions() { - global $wgOut, $wgRequest; - - $wgOut->addWikiMsg( 'listuserrestrictions-intro' ); - $f = new SpecialListUserRestrictionsForm(); - $wgOut->addHTML( $f->getHTML() ); - - if( !mt_rand( 0, 10 ) ) - UserRestriction::purgeExpired(); - $pager = new UserRestrictionsPager( $f->getConds() ); - if( $pager->getNumRows() ) - $wgOut->addHTML( $pager->getNavigationBar() . - Xml::tags( 'ul', null, $pager->getBody() ) . - $pager->getNavigationBar() - ); - elseif( $f->getConds() ) - $wgOut->addWikiMsg( 'listuserrestrictions-notfound' ); - else - $wgOut->addWikiMsg( 'listuserrestrictions-empty' ); -} - -class SpecialListUserRestrictionsForm { - public function getHTML() { - global $wgRequest, $wgScript, $wgTitle; - $action = htmlspecialchars( $wgScript ); - $s = ''; - $s .= Xml::fieldset( wfMsg( 'listuserrestrictions-legend' ) ); - $s .= "<form action=\"{$action}\">"; - $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); - $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-type' ), 'type' ) . ' ' . - self::typeSelector( 'type', $wgRequest->getVal( 'type' ), 'type' ); - $s .= ' '; - $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-user' ), 'user', 'user', - false, $wgRequest->getVal( 'user' ) ); - $s .= '<p>'; - $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-namespace' ), 'namespace' ) . ' ' . - Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ), '', 'namespace' ); - $s .= ' '; - $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-page' ), 'page', 'page', - false, $wgRequest->getVal( 'page' ) ); - $s .= Xml::submitButton( wfMsg( 'listuserrestrictions-submit' ) ); - $s .= "</p></form></fieldset>"; - return $s; - } - - public static function typeSelector( $name = 'type', $value = '', $id = false ) { - $s = new XmlSelect( $name, $id, $value ); - $s->addOption( wfMsg( 'userrestrictiontype-none' ), '' ); - $s->addOption( wfMsg( 'userrestrictiontype-page' ), UserRestriction::PAGE ); - $s->addOption( wfMsg( 'userrestrictiontype-namespace' ), UserRestriction::NAMESPACE ); - return $s->getHTML(); - } - - public function getConds() { - global $wgRequest; - $conds = array(); - - $type = $wgRequest->getVal( 'type' ); - if( in_array( $type, array( UserRestriction::PAGE, UserRestriction::NAMESPACE ) ) ) - $conds['ur_type'] = $type; - - $user = $wgRequest->getVal( 'user' ); - if( $user ) - $conds['ur_user_text'] = $user; - - $namespace = $wgRequest->getVal( 'namespace' ); - if( $namespace || $namespace === '0' ) - $conds['ur_namespace'] = $namespace; - - $page = $wgRequest->getVal( 'page' ); - $title = Title::newFromText( $page ); - if( $title ) { - $conds['ur_page_namespace'] = $title->getNamespace(); - $conds['ur_page_title'] = $title->getDBKey(); - } - - return $conds; - } -} - -class UserRestrictionsPager extends ReverseChronologicalPager { - public $mConds; - - public function __construct( $conds = array() ) { - $this->mConds = $conds; - parent::__construct(); - } - - public function getStartBody() { - # Copied from Special:Ipblocklist - wfProfileIn( __METHOD__ ); - # Do a link batch query - $this->mResult->seek( 0 ); - $lb = new LinkBatch; - - # Faster way - # Usernames and titles are in fact related by a simple substitution of space -> underscore - # The last few lines of Title::secureAndSplit() tell the story. - foreach( $this->mResult as $row ) { - $name = str_replace( ' ', '_', $row->ur_by_text ); - $lb->add( NS_USER, $name ); - $lb->add( NS_USER_TALK, $name ); - $name = str_replace( ' ', '_', $row->ur_user_text ); - $lb->add( NS_USER, $name ); - $lb->add( NS_USER_TALK, $name ); - if( $row->ur_type == UserRestriction::PAGE ) - $lb->add( $row->ur_page_namespace, $row->ur_page_title ); - } - $lb->execute(); - wfProfileOut( __METHOD__ ); - return ''; - } - - public function getQueryInfo() { - return array( - 'tables' => 'user_restrictions', - 'fields' => '*', - 'conds' => $this->mConds, - ); - } - - public function formatRow( $row ) { - return self::formatRestriction( UserRestriction::newFromRow( $row ) ); - } - - // Split off for use on Special:RestrictUser - public static function formatRestriction( $r ) { - global $wgUser, $wgLang; - $sk = $wgUser->getSkin(); - $timestamp = $wgLang->timeanddate( $r->getTimestamp(), true ); - $blockerlink = $sk->userLink( $r->getBlockerId(), $r->getBlockerText() ) . - $sk->userToolLinks( $r->getBlockerId(), $r->getBlockerText() ); - $subjlink = $sk->userLink( $r->getSubjectId(), $r->getSubjectText() ) . - $sk->userToolLinks( $r->getSubjectId(), $r->getSubjectText() ); - $expiry = is_numeric( $r->getExpiry() ) ? - wfMsg( 'listuserrestrictions-row-expiry', $wgLang->timeanddate( $r->getExpiry() ) ) : - wfMsg( 'ipbinfinite' ); - $msg = ''; - if( $r->isNamespace() ) { - $msg = wfMsgHtml( 'listuserrestrictions-row-ns', $subjlink, - $wgLang->getDisplayNsText( $r->getNamespace() ), $expiry ); - } - if( $r->isPage() ) { - $pagelink = $sk->link( $r->getPage() ); - $msg = wfMsgHtml( 'listuserrestrictions-row-page', $subjlink, - $pagelink, $expiry ); - } - $reason = $sk->commentBlock( $r->getReason() ); - $removelink = ''; - if( $wgUser->isAllowed( 'restrict' ) ) { - $removelink = '(' . $sk->link( SpecialPage::getTitleFor( 'RemoveRestrictions' ), - wfMsgHtml( 'listuserrestrictions-remove' ), array(), array( 'id' => $r->getId() ) ) . ')'; - } - return "<li>{$timestamp}, {$blockerlink} {$msg} {$reason} {$removelink}</li>\n"; - } - - public function getIndexField() { - return 'ur_timestamp'; - } -} diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php index e15b6959..b9332422 100644 --- a/includes/specials/SpecialListfiles.php +++ b/includes/specials/SpecialListfiles.php @@ -34,13 +34,11 @@ class ImageListPager extends TablePager { } $search = $wgRequest->getText( 'ilsearch' ); if ( $search != '' && !$wgMiserMode ) { - $nt = Title::newFromUrl( $search ); + $nt = Title::newFromURL( $search ); if( $nt ) { $dbr = wfGetDB( DB_SLAVE ); - $m = $dbr->strencode( strtolower( $nt->getDBkey() ) ); - $m = str_replace( "%", "\\%", $m ); - $m = str_replace( "_", "\\_", $m ); - $this->mQueryConds = array( "LOWER(img_name) LIKE '%{$m}%'" ); + $this->mQueryConds = array( 'LOWER(img_name)' . $dbr->buildLike( $dbr->anyString(), + strtolower( $nt->getDBkey() ), $dbr->anyString() ) ); } } @@ -127,21 +125,23 @@ class ImageListPager extends TablePager { global $wgLang; switch ( $field ) { case 'img_timestamp': - return $wgLang->timeanddate( $value, true ); + return htmlspecialchars( $wgLang->timeanddate( $value, true ) ); case 'img_name': static $imgfile = null; if ( $imgfile === null ) $imgfile = wfMsg( 'imgfile' ); $name = $this->mCurrentRow->img_name; - $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_FILE, $name ), $value ); + $link = $this->getSkin()->linkKnown( Title::makeTitle( NS_FILE, $name ), $value ); $image = wfLocalFile( $value ); $url = $image->getURL(); $download = Xml::element('a', array( 'href' => $url ), $imgfile ); return "$link ($download)"; case 'img_user_text': if ( $this->mCurrentRow->img_user ) { - $link = $this->getSkin()->makeLinkObj( Title::makeTitle( NS_USER, $value ), - htmlspecialchars( $value ) ); + $link = $this->getSkin()->link( + Title::makeTitle( NS_USER, $value ), + htmlspecialchars( $value ) + ); } else { $link = htmlspecialchars( $value ); } @@ -156,10 +156,10 @@ class ImageListPager extends TablePager { } function getForm() { - global $wgRequest, $wgMiserMode; + global $wgRequest, $wgScript, $wgMiserMode; $search = $wgRequest->getText( 'ilsearch' ); - $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-listfiles-form' ) ) . + $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listfiles-form' ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'listfiles' ) ) . Xml::tags( 'label', null, wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) ); diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php index d1fc0818..83724a4f 100644 --- a/includes/specials/SpecialListgrouprights.php +++ b/includes/specials/SpecialListgrouprights.php @@ -25,14 +25,15 @@ class SpecialListGroupRights extends SpecialPage { */ public function execute( $par ) { global $wgOut, $wgImplicitGroups, $wgMessageCache; - global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups; + global $wgGroupPermissions, $wgRevokePermissions, $wgAddGroups, $wgRemoveGroups; + global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; $wgMessageCache->loadAllMessages(); $this->setHeaders(); $this->outputHeader(); $wgOut->addHTML( - Xml::openElement( 'table', array( 'class' => 'mw-listgrouprights-table' ) ) . + Xml::openElement( 'table', array( 'class' => 'wikitable mw-listgrouprights-table' ) ) . '<tr>' . Xml::element( 'th', null, wfMsg( 'listgrouprights-group' ) ) . Xml::element( 'th', null, wfMsg( 'listgrouprights-rights' ) ) . @@ -40,7 +41,7 @@ class SpecialListGroupRights extends SpecialPage { ); foreach( $wgGroupPermissions as $group => $permissions ) { - $groupname = ( $group == '*' ) ? 'all' : htmlspecialchars( $group ); // Replace * with a more descriptive groupname + $groupname = ( $group == '*' ) ? 'all' : $group; // Replace * with a more descriptive groupname $msg = wfMsg( 'group-' . $groupname ); if ( wfEmptyMsg( 'group-' . $groupname, $msg ) || $msg == '' ) { @@ -58,23 +59,41 @@ class SpecialListGroupRights extends SpecialPage { if( $group == '*' ) { // Do not make a link for the generic * group - $grouppage = $groupnameLocalized; + $grouppage = htmlspecialchars($groupnameLocalized); } else { - $grouppage = $this->skin->makeLink( $grouppageLocalized, $groupnameLocalized ); + $grouppage = $this->skin->link( + Title::newFromText( $grouppageLocalized ), + htmlspecialchars($groupnameLocalized) + ); } if ( $group === 'user' ) { // Link to Special:listusers for implicit group 'user' - $grouplink = '<br />' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), '' ); + $grouplink = '<br />' . $this->skin->link( + SpecialPage::getTitleFor( 'Listusers' ), + wfMsgHtml( 'listgrouprights-members' ), + array(), + array(), + array( 'known', 'noclasses' ) + ); } elseif ( !in_array( $group, $wgImplicitGroups ) ) { - $grouplink = '<br />' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), 'group=' . $group ); + $grouplink = '<br />' . $this->skin->link( + SpecialPage::getTitleFor( 'Listusers' ), + wfMsgHtml( 'listgrouprights-members' ), + array(), + array( 'group' => $group ), + array( 'known', 'noclasses' ) + ); } else { // No link to Special:listusers for other implicit groups as they are unlistable $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(); $wgOut->addHTML( '<tr> @@ -82,30 +101,47 @@ class SpecialListGroupRights extends SpecialPage { $grouppage . $grouplink . '</td> <td>' . - self::formatPermissions( $permissions, $addgroups, $removegroups ) . + self::formatPermissions( $permissions, $revoke, $addgroups, $removegroups, $addgroupsSelf, $removegroupsSelf ) . '</td> </tr>' ); } $wgOut->addHTML( - Xml::closeElement( 'table' ) . "\n" + Xml::closeElement( 'table' ) . "\n<br /><hr />\n" ); + $wgOut->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' ); } /** * Create a user-readable list of permissions from the given array. * * @param $permissions Array of permission => bool (from $wgGroupPermissions items) + * @param $revoke Array of permission => bool (from $wgRevokePermissions items) + * @param $add Array of groups this group is allowed to add or true + * @param $remove Array of groups this group is allowed to remove or true + * @param $addSelf Array of groups this group is allowed to add to self or true + * @param $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 static function formatPermissions( $permissions, $add, $remove ) { + private static function formatPermissions( $permissions, $revoke, $add, $remove, $addSelf, $removeSelf ) { global $wgLang; + $r = array(); foreach( $permissions as $permission => $granted ) { - if ( $granted ) { + //show as granted only if it isn't revoked to prevent duplicate display of permissions + if( $granted && ( !isset( $revoke[$permission] ) || !$revoke[$permission] ) ) { $description = wfMsgExt( 'listgrouprights-right-display', array( 'parseinline' ), User::getRightDescription( $permission ), - $permission + '<span class="mw-listgrouprights-right-name">' . $permission . '</span>' + ); + $r[] = $description; + } + } + foreach( $revoke as $permission => $revoked ) { + if( $revoked ) { + $description = wfMsgExt( 'listgrouprights-right-revoked', array( 'parseinline' ), + User::getRightDescription( $permission ), + '<span class="mw-listgrouprights-right-name">' . $permission . '</span>' ); $r[] = $description; } @@ -114,13 +150,27 @@ class SpecialListGroupRights extends SpecialPage { if( $add === true ){ $r[] = wfMsgExt( 'listgrouprights-addgroup-all', array( 'escape' ) ); } else if( is_array( $add ) && count( $add ) ) { + $add = array_values( array_unique( $add ) ); $r[] = wfMsgExt( 'listgrouprights-addgroup', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ), count( $add ) ); } if( $remove === true ){ $r[] = wfMsgExt( 'listgrouprights-removegroup-all', array( 'escape' ) ); } else if( is_array( $remove ) && count( $remove ) ) { + $remove = array_values( array_unique( $remove ) ); $r[] = wfMsgExt( 'listgrouprights-removegroup', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ), count( $remove ) ); } + if( $addSelf === true ){ + $r[] = wfMsgExt( 'listgrouprights-addgroup-self-all', array( 'escape' ) ); + } else if( is_array( $addSelf ) && count( $addSelf ) ) { + $addSelf = array_values( array_unique( $addSelf ) ); + $r[] = wfMsgExt( 'listgrouprights-addgroup-self', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $addSelf ) ), count( $addSelf ) ); + } + if( $removeSelf === true ){ + $r[] = wfMsgExt( 'listgrouprights-removegroup-self-all', array( 'escape' ) ); + } else if( is_array( $removeSelf ) && count( $removeSelf ) ) { + $removeSelf = array_values( array_unique( $removeSelf ) ); + $r[] = wfMsgExt( 'listgrouprights-removegroup-self', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $removeSelf ) ), count( $removeSelf ) ); + } if( empty( $r ) ) { return ''; } else { diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php index 9555bd16..bf594070 100644 --- a/includes/specials/SpecialListredirects.php +++ b/includes/specials/SpecialListredirects.php @@ -32,7 +32,12 @@ class ListredirectsPage extends QueryPage { # Make a link to the redirect itself $rd_title = Title::makeTitle( $result->namespace, $result->title ); - $rd_link = $skin->makeLinkObj( $rd_title, '', 'redirect=no' ); + $rd_link = $skin->link( + $rd_title, + null, + array(), + array( 'redirect' => 'no' ) + ); # Find out where the redirect leads $revision = Revision::newFromTitle( $rd_title ); @@ -41,7 +46,7 @@ class ListredirectsPage extends QueryPage { $target = Title::newFromRedirect( $revision->getText() ); if( $target ) { $arr = $wgContLang->getArrow() . $wgContLang->getDirMark(); - $targetLink = $skin->makeLinkObj( $target ); + $targetLink = $skin->link( $target ); return "$rd_link $arr $targetLink"; } else { return "<s>$rd_link</s>"; diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php index aa057801..bdb59980 100644 --- a/includes/specials/SpecialListusers.php +++ b/includes/specials/SpecialListusers.php @@ -71,10 +71,12 @@ class UsersPager extends AlphabeticPager { } function getQueryInfo() { + global $wgUser; $dbr = wfGetDB( DB_SLAVE ); $conds = array(); // Don't show hidden names - $conds[] = 'ipb_deleted IS NULL OR ipb_deleted = 0'; + if( !$wgUser->isAllowed('hideuser') ) + $conds[] = 'ipb_deleted IS NULL'; if( $this->requestedGroup != '' ) { $conds['ug_group'] = $this->requestedGroup; $useIndex = ''; @@ -84,7 +86,7 @@ class UsersPager extends AlphabeticPager { if( $this->requestedUser != '' ) { # Sorted either by account creation or name if( $this->creationSort ) { - $conds[] = 'user_id >= ' . User::idFromName( $this->requestedUser ); + $conds[] = 'user_id >= ' . intval( User::idFromName( $this->requestedUser ) ); } else { $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser ); } @@ -97,14 +99,16 @@ class UsersPager extends AlphabeticPager { $query = array( 'tables' => " $user $useIndex LEFT JOIN $user_groups ON user_id=ug_user - LEFT JOIN $ipblocks ON user_id=ipb_user AND ipb_auto=0 ", + LEFT JOIN $ipblocks ON user_id=ipb_user AND ipb_deleted=1 AND ipb_auto=0 ", 'fields' => array( $this->creationSort ? 'MAX(user_name) AS user_name' : 'user_name', $this->creationSort ? 'user_id' : 'MAX(user_id) AS user_id', 'MAX(user_editcount) AS edits', 'COUNT(ug_group) AS numgroups', - 'MAX(ug_group) AS singlegroup', - 'MIN(user_registration) AS creation'), + 'MAX(ug_group) AS singlegroup', // the usergroup if there is only one + 'MIN(user_registration) AS creation', + 'MAX(ipb_deleted) AS ipb_deleted' // block/hide status + ), 'options' => array('GROUP BY' => $this->creationSort ? 'user_id' : 'user_name'), 'conds' => $conds ); @@ -117,7 +121,7 @@ class UsersPager extends AlphabeticPager { global $wgLang; $userPage = Title::makeTitle( NS_USER, $row->user_name ); - $name = $this->getSkin()->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) ); + $name = $this->getSkin()->link( $userPage, htmlspecialchars( $userPage->getText() ) ); if( $row->numgroups > 1 || ( $this->requestedGroup && $row->numgroups == 1 ) ) { $list = array(); @@ -131,11 +135,14 @@ class UsersPager extends AlphabeticPager { } $item = wfSpecialList( $name, $groups ); + if( $row->ipb_deleted ) { + $item = "<span class=\"deleted\">$item</span>"; + } global $wgEdititis; if ( $wgEdititis ) { $editCount = $wgLang->formatNum( $row->edits ); - $edits = ' [' . wfMsgExt( 'usereditcount', 'parsemag', $editCount ) . ']'; + $edits = ' [' . wfMsgExt( 'usereditcount', array( 'parsemag', 'escape' ), $editCount ) . ']'; } else { $edits = ''; } @@ -145,7 +152,8 @@ class UsersPager extends AlphabeticPager { if( $row->creation ) { $d = $wgLang->date( wfTimestamp( TS_MW, $row->creation ), true ); $t = $wgLang->time( wfTimestamp( TS_MW, $row->creation ), true ); - $created = ' (' . wfMsgHtml( 'usercreated', $d, $t ) . ')'; + $created = ' (' . wfMsg( 'usercreated', $d, $t ) . ')'; + $created = htmlspecialchars( $created ); } wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) ); @@ -185,11 +193,11 @@ class UsersPager extends AlphabeticPager { Xml::option( wfMsg( 'group-all' ), '' ); foreach( $this->getAllGroups() as $group => $groupText ) $out .= Xml::option( $groupText, $group, $group == $this->requestedGroup ); - $out .= Xml::closeElement( 'select' ) . '<br/>'; + $out .= Xml::closeElement( 'select' ) . '<br />'; $out .= Xml::checkLabel( wfMsg('listusers-editsonly'), 'editsOnly', 'editsOnly', $this->editsOnly ); $out .= ' '; $out .= Xml::checkLabel( wfMsg('listusers-creationsort'), 'creationSort', 'creationSort', $this->creationSort ); - $out .= '<br/>'; + $out .= '<br />'; wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) ); @@ -233,7 +241,7 @@ class UsersPager extends AlphabeticPager { /** * Get a list of groups the specified user belongs to * - * @param int $uid + * @param $uid Integer: user id * @return array */ protected static function getGroups( $uid ) { @@ -245,13 +253,13 @@ class UsersPager extends AlphabeticPager { /** * Format a link to a group description page * - * @param string $group + * @param $group String: group name * @return string */ protected static function buildGroupLink( $group ) { static $cache = array(); if( !isset( $cache[$group] ) ) - $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) ); + $cache[$group] = User::makeGroupLinkHtml( $group, htmlspecialchars( User::getGroupMember( $group ) ) ); return $cache[$group]; } } diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php index 5859d5b2..8c701dd6 100644 --- a/includes/specials/SpecialLockdb.php +++ b/includes/specials/SpecialLockdb.php @@ -53,7 +53,7 @@ class DBLockForm { $wgOut->setPagetitle( wfMsg( 'lockdb' ) ); $wgOut->addWikiMsg( 'lockdbtext' ); - if ( "" != $err ) { + if ( $err != "" ) { $wgOut->setSubtitle( wfMsg( 'formerror' ) ); $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" ); } @@ -65,7 +65,7 @@ class DBLockForm { $reason = htmlspecialchars( $this->reason ); $token = htmlspecialchars( $wgUser->editToken() ); - $wgOut->addHTML( <<<END + $wgOut->addHTML( <<<HTML <form id="lockdb" method="post" action="{$action}"> {$elr}: <textarea name="wpLockReason" rows="10" cols="60" wrap="virtual">{$reason}</textarea> @@ -85,7 +85,7 @@ class DBLockForm { </table> <input type="hidden" name="wpEditToken" value="{$token}" /> </form> -END +HTML ); } diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php index 2382344b..d1ccc8c4 100644 --- a/includes/specials/SpecialLog.php +++ b/includes/specials/SpecialLog.php @@ -52,9 +52,19 @@ function wfSpecialLog( $par = '' ) { $y = ''; $m = ''; } + # Handle type-specific inputs + $qc = array(); + if( $type == 'suppress' ) { + $offender = User::newFromName( $wgRequest->getVal('offender'), false ); + if( $offender && $offender->getId() > 0 ) { + $qc = array( 'ls_field' => 'target_author_id', 'ls_value' => $offender->getId() ); + } else if( $offender && IP::isIPAddress( $offender->getName() ) ) { + $qc = array( 'ls_field' => 'target_author_ip', 'ls_value' => $offender->getName() ); + } + } # Create a LogPager item to get the results and a LogEventsList item to format them... $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 ); - $pager = new LogPager( $loglist, $type, $user, $title, $pattern, array(), $y, $m, $tagFilter ); + $pager = new LogPager( $loglist, $type, $user, $title, $pattern, $qc, $y, $m, $tagFilter ); # Set title and add header $loglist->showHeader( $pager->getType() ); # Show form options diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php index cdfde24e..dafe003e 100644 --- a/includes/specials/SpecialMIMEsearch.php +++ b/includes/specials/SpecialMIMEsearch.php @@ -65,15 +65,20 @@ class MIMEsearchPage extends QueryPage { $nt = Title::makeTitle( $result->namespace, $result->title ); $text = $wgContLang->convert( $nt->getText() ); - $plink = $skin->makeLink( $nt->getPrefixedText(), $text ); + $plink = $skin->link( + Title::newFromText( $nt->getPrefixedText() ), + htmlspecialchars( $text ) + ); $download = $skin->makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) ); $bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), $wgLang->formatNum( $result->img_size ) ); - $dimensions = wfMsgHtml( 'widthheight', $wgLang->formatNum( $result->img_width ), - $wgLang->formatNum( $result->img_height ) ); - $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text ); - $time = $wgLang->timeanddate( $result->img_timestamp ); + $dimensions = htmlspecialchars( wfMsg( 'widthheight', + $wgLang->formatNum( $result->img_width ), + $wgLang->formatNum( $result->img_height ) + ) ); + $user = $skin->link( Title::makeTitle( NS_USER, $result->img_user_text ), htmlspecialchars( $result->img_user_text ) ); + $time = htmlspecialchars( $wgLang->timeanddate( $result->img_timestamp ) ); return "($download) $plink . . $dimensions . . $bytes . . $user . . $time"; } @@ -83,13 +88,14 @@ class MIMEsearchPage extends QueryPage { * Output the HTML search form, and constructs the MIMEsearchPage object. */ function wfSpecialMIMEsearch( $par = null ) { - global $wgRequest, $wgTitle, $wgOut; + global $wgRequest, $wgOut; $mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' ); $wgOut->addHTML( - Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) . + Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => SpecialPage::getTitleFor( 'MIMEsearch' )->getLocalUrl() ) ) . Xml::openElement( 'fieldset' ) . + Xml::hidden( 'title', SpecialPage::getTitleFor( 'MIMEsearch' )->getPrefixedText() ) . Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) . Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' . Xml::submitButton( wfMsg( 'ilsubmit' ) ) . diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php index c51ce7c3..1b4ef30c 100644 --- a/includes/specials/SpecialMergeHistory.php +++ b/includes/specials/SpecialMergeHistory.php @@ -67,7 +67,7 @@ class MergehistoryForm { } function execute() { - global $wgOut, $wgUser; + global $wgOut; $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) ); @@ -155,7 +155,7 @@ class MergehistoryForm { $haveRevisions = $revisions && $revisions->getNumRows() > 0; $titleObj = SpecialPage::getTitleFor( "Mergehistory" ); - $action = $titleObj->getLocalURL( "action=submit" ); + $action = $titleObj->getLocalURL( array( 'action' => 'submit' ) ); # Start the form here $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) ); $wgOut->addHTML( $top ); @@ -218,7 +218,7 @@ class MergehistoryForm { } function formatRevisionRow( $row ) { - global $wgUser, $wgLang; + global $wgLang; $rev = new Revision( $row ); @@ -228,8 +228,12 @@ class MergehistoryForm { $ts = wfTimestamp( TS_MW, $row->rev_timestamp ); $checkBox = Xml::radio( "mergepoint", $ts, false ); - $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(), - htmlspecialchars( $wgLang->timeanddate( $ts ) ), 'oldid=' . $rev->getId() ); + $pageLink = $this->sk->linkKnown( + $rev->getTitle(), + htmlspecialchars( $wgLang->timeanddate( $ts ) ), + array(), + array( 'oldid' => $rev->getId() ) + ); if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { $pageLink = '<span class="history-deleted">' . $pageLink . '</span>'; } @@ -238,8 +242,15 @@ class MergehistoryForm { if( !$rev->userCan( Revision::DELETED_TEXT ) ) $last = $this->message['last']; else if( isset($this->prevId[$row->rev_id]) ) - $last = $this->sk->makeKnownLinkObj( $rev->getTitle(), $this->message['last'], - "diff=" . $row->rev_id . "&oldid=" . $this->prevId[$row->rev_id] ); + $last = $this->sk->linkKnown( + $rev->getTitle(), + $this->message['last'], + array(), + array( + 'diff' => $row->rev_id, + 'oldid' => $this->prevId[$row->rev_id] + ) + ); $userLink = $this->sk->revUserTools( $rev ); @@ -261,8 +272,15 @@ class MergehistoryForm { if( !$this->userCan($row, Revision::DELETED_TEXT) ) { return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>'; } else { - $link = $this->sk->makeKnownLinkObj( $titleObj, - $wgLang->timeanddate( $ts, true ), "target=$target×tamp=$ts" ); + $link = $this->sk->linkKnown( + $titleObj, + $wgLang->timeanddate( $ts, true ), + array(), + array( + 'target' => $target, + 'timestamp' => $ts + ) + ); if( $this->isDeleted($row, Revision::DELETED_TEXT) ) $link = '<span class="history-deleted">' . $link . '</span>'; return $link; @@ -270,7 +288,7 @@ class MergehistoryForm { } function merge() { - global $wgOut, $wgUser; + global $wgOut; # 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 # keep it consistent... diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php index 078489bd..f112ae17 100644 --- a/includes/specials/SpecialMostlinked.php +++ b/includes/specials/SpecialMostlinked.php @@ -22,22 +22,35 @@ class MostlinkedPage extends QueryPage { function isExpensive() { return true; } function isSyndicated() { return false; } - /** - * Note: Getting page_namespace only works if $this->isCached() is false - */ function getSQL() { + global $wgMiserMode; + $dbr = wfGetDB( DB_SLAVE ); + + # In miser mode, reduce the query cost by adding a threshold for large wikis + if ( $wgMiserMode ) { + $numPages = SiteStats::pages(); + if ( $numPages > 10000 ) { + $cutoff = 100; + } elseif ( $numPages > 100 ) { + $cutoff = intval( sqrt( $numPages ) ); + } else { + $cutoff = 1; + } + } else { + $cutoff = 1; + } + list( $pagelinks, $page ) = $dbr->tableNamesN( 'pagelinks', 'page' ); return "SELECT 'Mostlinked' AS type, pl_namespace AS namespace, pl_title AS title, - COUNT(*) AS value, - page_namespace + COUNT(*) AS value FROM $pagelinks LEFT JOIN $page ON pl_namespace=page_namespace AND pl_title=page_title - GROUP BY pl_namespace, pl_title, page_namespace - HAVING COUNT(*) > 1"; + GROUP BY pl_namespace, pl_title + HAVING COUNT(*) > $cutoff"; } /** @@ -57,12 +70,13 @@ class MostlinkedPage extends QueryPage { * Make a link to "what links here" for the specified title * * @param $title Title being queried + * @param $caption String: text to display on the link * @param $skin Skin to use - * @return string + * @return String */ function makeWlhLink( &$title, $caption, &$skin ) { $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() ); - return $skin->makeKnownLinkObj( $wlh, $caption ); + return $skin->linkKnown( $wlh, $caption ); } /** @@ -75,7 +89,10 @@ class MostlinkedPage extends QueryPage { function formatResult( $skin, $result ) { global $wgLang; $title = Title::makeTitleSafe( $result->namespace, $result->title ); - $link = $skin->makeLinkObj( $title ); + if ( !$title ) { + return '<!-- ' . htmlspecialchars( "Invalid title: [[$title]]" ) . ' -->'; + } + $link = $skin->link( $title ); $wlh = $this->makeWlhLink( $title, wfMsgExt( 'nlinks', array( 'parsemag', 'escape'), $wgLang->formatNum( $result->value ) ), $skin ); diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php index ab250675..20a35c97 100644 --- a/includes/specials/SpecialMostlinkedcategories.php +++ b/includes/specials/SpecialMostlinkedcategories.php @@ -58,7 +58,7 @@ class MostlinkedCategoriesPage extends QueryPage { $nt = Title::makeTitle( $result->namespace, $result->title ); $text = $wgContLang->convert( $nt->getText() ); - $plink = $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ); + $plink = $skin->link( $nt, htmlspecialchars( $text ) ); $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'), $wgLang->formatNum( $result->value ) ); diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php index 2d398a38..71a6b539 100644 --- a/includes/specials/SpecialMostlinkedtemplates.php +++ b/includes/specials/SpecialMostlinkedtemplates.php @@ -16,7 +16,7 @@ class SpecialMostlinkedtemplates extends QueryPage { /** * Name of the report * - * @return string + * @return String */ public function getName() { return 'Mostlinkedtemplates'; @@ -25,7 +25,7 @@ class SpecialMostlinkedtemplates extends QueryPage { /** * Is this report expensive, i.e should it be cached? * - * @return bool + * @return Boolean */ public function isExpensive() { return true; @@ -34,7 +34,7 @@ class SpecialMostlinkedtemplates extends QueryPage { /** * Is there a feed available? * - * @return bool + * @return Boolean */ public function isSyndicated() { return false; @@ -43,7 +43,7 @@ class SpecialMostlinkedtemplates extends QueryPage { /** * Sort the results in descending order? * - * @return bool + * @return Boolean */ public function sortDescending() { return true; @@ -52,7 +52,7 @@ class SpecialMostlinkedtemplates extends QueryPage { /** * Generate SQL for the report * - * @return string + * @return String */ public function getSql() { $dbr = wfGetDB( DB_SLAVE ); @@ -70,8 +70,8 @@ class SpecialMostlinkedtemplates extends QueryPage { /** * Pre-cache page existence to speed up link generation * - * @param Database $dbr Database connection - * @param int $res Result pointer + * @param $db Database connection + * @param $res ResultWrapper */ public function preprocessResults( $db, $res ) { $batch = new LinkBatch(); @@ -86,16 +86,15 @@ class SpecialMostlinkedtemplates extends QueryPage { /** * Format a result row * - * @param Skin $skin Skin to use for UI elements - * @param object $result Result row - * @return string + * @param $skin Skin to use for UI elements + * @param $result Result row + * @return String */ public function formatResult( $skin, $result ) { $title = Title::makeTitleSafe( $result->namespace, $result->title ); - $skin->link( $title ); return wfSpecialList( - $skin->makeLinkObj( $title ), + $skin->link( $title ), $this->makeWlhLink( $title, $skin, $result ) ); } @@ -103,10 +102,10 @@ class SpecialMostlinkedtemplates extends QueryPage { /** * Make a "what links here" link for a given title * - * @param Title $title Title to make the link for - * @param Skin $skin Skin to use - * @param object $result Result row - * @return string + * @param $title Title to make the link for + * @param $skin Skin to use + * @param $result Result row + * @return String */ private function makeWlhLink( $title, $skin, $result ) { global $wgLang; @@ -120,7 +119,7 @@ class SpecialMostlinkedtemplates extends QueryPage { /** * Execution function * - * @param mixed $par Parameters passed to the page + * @param $par Mixed: parameters passed to the page */ function wfSpecialMostlinkedtemplates( $par = false ) { list( $limit, $offset ) = wfCheckLimits(); diff --git a/includes/specials/SpecialMostrevisions.php b/includes/specials/SpecialMostrevisions.php index f5a0f8c0..414e8d97 100644 --- a/includes/specials/SpecialMostrevisions.php +++ b/includes/specials/SpecialMostrevisions.php @@ -42,11 +42,16 @@ class MostrevisionsPage extends QueryPage { $nt = Title::makeTitle( $result->namespace, $result->title ); $text = $wgContLang->convert( $nt->getPrefixedText() ); - $plink = $skin->makeKnownLinkObj( $nt, $text ); + $plink = $skin->linkKnown( $nt, $text ); $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'), $wgLang->formatNum( $result->value ) ); - $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' ); + $nlink = $skin->linkKnown( + $nt, + $nl, + array(), + array( 'action' => 'history' ) + ); return wfSpecialList($plink, $nlink); } diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index 8fcf33a9..02197b19 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -17,7 +17,9 @@ function wfSpecialMovepage( $par = null ) { } $target = isset( $par ) ? $par : $wgRequest->getVal( 'target' ); - $oldTitleText = $wgRequest->getText( 'wpOldTitle', $target ); + + // Yes, the use of getVal() and getText() is wanted, see bug 20365 + $oldTitleText = $wgRequest->getVal( 'wpOldTitle', $target ); $newTitleText = $wgRequest->getText( 'wpNewTitle' ); $oldTitle = Title::newFromText( $oldTitleText ); @@ -56,12 +58,12 @@ function wfSpecialMovepage( $par = null ) { class MovePageForm { var $oldTitle, $newTitle; # Objects var $reason; # Text input - var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect; # Checks + var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect, $moveOverShared; # Checks private $watch = false; function __construct( $oldTitle, $newTitle ) { - global $wgRequest; + global $wgRequest, $wgUser; $target = isset($par) ? $par : $wgRequest->getVal( 'target' ); $this->oldTitle = $oldTitle; $this->newTitle = $newTitle; @@ -77,7 +79,8 @@ class MovePageForm { } $this->moveSubpages = $wgRequest->getBool( 'wpMovesubpages', false ); $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' ); - $this->watch = $wgRequest->getCheck( 'wpWatch' ); + $this->moveOverShared = $wgRequest->getBool( 'wpMoveOverSharedFile', false ); + $this->watch = $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn(); } /** @@ -87,11 +90,11 @@ class MovePageForm { * OutputPage::wrapWikiMsg(). */ function showForm( $err ) { - global $wgOut, $wgUser, $wgFixDoubleRedirects; + global $wgOut, $wgUser, $wgContLang, $wgFixDoubleRedirects; $skin = $wgUser->getSkin(); - $oldTitleLink = $skin->makeLinkObj( $this->oldTitle ); + $oldTitleLink = $skin->link( $this->oldTitle ); $wgOut->setPagetitle( wfMsg( 'move-page', $this->oldTitle->getPrefixedText() ) ); $wgOut->setSubtitle( wfMsg( 'move-page-backlink', $oldTitleLink ) ); @@ -128,12 +131,21 @@ class MovePageForm { </tr>"; $err = ''; } else { + if ($this->oldTitle->getNamespace() == NS_USER && !$this->oldTitle->isSubpage() ) { + $wgOut->wrapWikiMsg( "<div class=\"error mw-moveuserpage-warning\">\n$1\n</div>", 'moveuserpage-warning' ); + } $wgOut->addWikiMsg( 'movepagetext' ); $movepagebtn = wfMsg( 'movepagebtn' ); $submitVar = 'wpMove'; $confirm = false; } + if ( !empty($err) && $err[0] == 'file-exists-sharedrepo' && $wgUser->isAllowed( 'reupload-shared' ) ) { + $wgOut->addWikiMsg( 'move-over-sharedrepo', $newTitle->getPrefixedText() ); + $submitVar = 'wpMoveOverSharedFile'; + $err = ''; + } + $oldTalk = $this->oldTitle->getTalkPage(); $considerTalk = ( !$this->oldTitle->isTalkPage() && $oldTalk->exists() ); @@ -166,6 +178,22 @@ class MovePageForm { } } + if ( $this->oldTitle->isProtected( 'move' ) ) { + # Is the title semi-protected? + if ( $this->oldTitle->isSemiProtected( 'move' ) ) { + $noticeMsg = 'semiprotectedpagemovewarning'; + $classes[] = 'mw-textarea-sprotected'; + } else { + # Then it must be protected based on static groups (regular) + $noticeMsg = 'protectedpagemovewarning'; + $classes[] = 'mw-textarea-protected'; + } + $wgOut->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" ); + $wgOut->addWikiMsg( $noticeMsg ); + LogEventsList::showLogExtract( $wgOut, 'protect', $this->oldTitle->getPrefixedText(), '', array( 'lim' => 1 ) ); + $wgOut->addHTML( "</div>\n" ); + } + $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) . Xml::openElement( 'fieldset' ) . @@ -184,7 +212,7 @@ class MovePageForm { Xml::label( wfMsg( 'newtitle' ), 'wpNewTitle' ) . "</td> <td class='mw-input'>" . - Xml::input( 'wpNewTitle', 40, $newTitle->getPrefixedText(), array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) . + Xml::input( 'wpNewTitle', 40, $wgContLang->recodeForEdit( $newTitle->getPrefixedText() ), array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) . Xml::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) . "</td> </tr> @@ -193,7 +221,8 @@ class MovePageForm { Xml::label( wfMsg( 'movereason' ), 'wpReason' ) . "</td> <td class='mw-input'>" . - Xml::tags( 'textarea', array( 'name' => 'wpReason', 'id' => 'wpReason', 'cols' => 60, 'rows' => 2 ), htmlspecialchars( $this->reason ) ) . + Html::element( 'textarea', array( 'name' => 'wpReason', 'id' => 'wpReason', 'cols' => 60, 'rows' => 2, + 'maxlength' => 200 ), $this->reason ) . "</td> </tr>" ); @@ -242,34 +271,43 @@ class MovePageForm { <tr> <td></td> <td class=\"mw-input\">" . - Xml::checkLabel( wfMsgExt( + Xml::check( + 'wpMovesubpages', + # Don't check the box if we only have talk subpages to + # move and we aren't moving the talk page. + $this->moveSubpages && ($this->oldTitle->hasSubpages() || $this->moveTalk), + array( 'id' => 'wpMovesubpages' ) + ) . ' ' . + Xml::tags( 'label', array( 'for' => 'wpMovesubpages' ), + wfMsgExt( ( $this->oldTitle->hasSubpages() ? 'move-subpages' : 'move-talk-subpages' ), - array( 'parsemag' ), + array( 'parseinline' ), $wgLang->formatNum( $wgMaximumMovedPages ), # $2 to allow use of PLURAL in message. $wgMaximumMovedPages - ), - 'wpMovesubpages', 'wpMovesubpages', - # Don't check the box if we only have talk subpages to - # move and we aren't moving the talk page. - $this->moveSubpages && ($this->oldTitle->hasSubpages() || $this->moveTalk) + ) ) . "</td> </tr>" ); } - $watchChecked = $this->watch || $wgUser->getBoolOption( 'watchmoves' ) - || $this->oldTitle->userIsWatching(); - $wgOut->addHTML( " + $watchChecked = $wgUser->isLoggedIn() && ($this->watch || $wgUser->getBoolOption( 'watchmoves' ) + || $this->oldTitle->userIsWatching()); + # Don't allow watching if user is not logged in + if( $wgUser->isLoggedIn() ) { + $wgOut->addHTML( " <tr> <td></td> <td class='mw-input'>" . Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) . "</td> - </tr> + </tr>"); + } + + $wgOut->addHTML( " {$confirm} <tr> <td> </td> @@ -329,12 +367,24 @@ class MovePageForm { return; } + # Show a warning if the target file exists on a shared repo + if ( $nt->getNamespace() == NS_FILE + && !( $this->moveOverShared && $wgUser->isAllowed( 'reupload-shared' ) ) + && !RepoGroup::singleton()->getLocalRepo()->findFile( $nt ) + && wfFindFile( $nt ) ) + { + $this->showForm( array('file-exists-sharedrepo') ); + return; + + } + if ( $wgUser->isAllowed( 'suppressredirect' ) ) { $createRedirect = $this->leaveRedirect; } else { $createRedirect = true; } + # Do the actual move. $error = $ot->moveTo( $nt, true, $this->reason, $createRedirect ); if ( $error !== true ) { # FIXME: show all the errors in a list, not just the first one @@ -393,7 +443,7 @@ class MovePageForm { ) ) ) { $conds = array( - 'page_title LIKE '.$dbr->addQuotes( $dbr->escapeLike( $ot->getDBkey() ) . '/%' ) + 'page_title' . $dbr->buildLike( $ot->getDBkey() . '/', $dbr->anyString() ) .' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() ) ); $conds['page_namespace'] = array(); @@ -406,7 +456,7 @@ class MovePageForm { } elseif( $this->moveTalk ) { $conds = array( 'page_namespace' => $ot->getTalkPage()->getNamespace(), - 'page_title' => $ot->getDBKey() + 'page_title' => $ot->getDBkey() ); } else { # Skip the query @@ -428,15 +478,15 @@ class MovePageForm { $skin = $wgUser->getSkin(); $count = 1; foreach( $extraPages as $oldSubpage ) { - if( $oldSubpage->getArticleId() == $ot->getArticleId() ) { + if( $ot->equals( $oldSubpage ) ) { # Already did this one. continue; } $newPageName = preg_replace( - '#^'.preg_quote( $ot->getDBKey(), '#' ).'#', - $nt->getDBKey(), - $oldSubpage->getDBKey() + '#^'.preg_quote( $ot->getDBkey(), '#' ).'#', + StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234 + $oldSubpage->getDBkey() ); if( $oldSubpage->isTalkPage() ) { $newNs = $nt->getTalkPage()->getNamespace(); @@ -447,7 +497,7 @@ class MovePageForm { # be longer than 255 characters. $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); if( !$newSubpage ) { - $oldLink = $skin->makeKnownLinkObj( $oldSubpage ); + $oldLink = $skin->linkKnown( $oldSubpage ); $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink, htmlspecialchars(Title::makeName( $newNs, $newPageName ))); continue; @@ -455,7 +505,7 @@ class MovePageForm { # This was copy-pasted from Renameuser, bleh. if ( $newSubpage->exists() && !$oldSubpage->isValidMoveTarget( $newSubpage ) ) { - $link = $skin->makeKnownLinkObj( $newSubpage ); + $link = $skin->linkKnown( $newSubpage ); $extraOutput []= wfMsgHtml( 'movepage-page-exists', $link ); } else { $success = $oldSubpage->moveTo( $newSubpage, true, $this->reason, $createRedirect ); @@ -463,21 +513,26 @@ class MovePageForm { if ( $this->fixRedirects ) { DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage ); } - $oldLink = $skin->makeKnownLinkObj( $oldSubpage, '', 'redirect=no' ); - $newLink = $skin->makeKnownLinkObj( $newSubpage ); + $oldLink = $skin->linkKnown( + $oldSubpage, + null, + array(), + array( 'redirect' => 'no' ) + ); + $newLink = $skin->linkKnown( $newSubpage ); $extraOutput []= wfMsgHtml( 'movepage-page-moved', $oldLink, $newLink ); + ++$count; + if( $count >= $wgMaximumMovedPages ) { + $extraOutput []= wfMsgExt( 'movepage-max-pages', array( 'parsemag', 'escape' ), $wgLang->formatNum( $wgMaximumMovedPages ) ); + break; + } } else { - $oldLink = $skin->makeKnownLinkObj( $oldSubpage ); - $newLink = $skin->makeLinkObj( $newSubpage ); + $oldLink = $skin->linkKnown( $oldSubpage ); + $newLink = $skin->link( $newSubpage ); $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink, $newLink ); } } - ++$count; - if( $count >= $wgMaximumMovedPages ) { - $extraOutput []= wfMsgExt( 'movepage-max-pages', array( 'parsemag', 'escape' ), $wgLang->formatNum( $wgMaximumMovedPages ) ); - break; - } } if( $extraOutput !== array() ) { @@ -485,17 +540,24 @@ class MovePageForm { } # Deal with watches (we don't watch subpages) - if( $this->watch ) { + if( $this->watch && $wgUser->isLoggedIn() ) { $wgUser->addWatch( $ot ); $wgUser->addWatch( $nt ); } else { $wgUser->removeWatch( $ot ); $wgUser->removeWatch( $nt ); } + + # Re-clear the file redirect cache, which may have been polluted by + # parsing in messages above. See CR r56745. + # FIXME: needs a more robust solution inside FileRepo. + if( $ot->getNamespace() == NS_FILE ) { + RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $ot ); + } } function showLogFragment( $title, &$out ) { - $out->addHTML( Xml::element( 'h2', NULL, LogPage::logName( 'move' ) ) ); + $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'move' ) ) ); LogEventsList::showLogExtract( $out, 'move', $title->getPrefixedText() ); } diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index 575e37a7..a39b56ee 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -40,7 +40,8 @@ function wfSpecialNewimages( $par, $specialPage ) { if ($hidebotsql) { $sql .= "$hidebotsql WHERE ug_group IS NULL"; } - $sql .= ' ORDER BY img_timestamp DESC LIMIT 1'; + $sql .= ' ORDER BY img_timestamp DESC'; + $sql = $dbr->limitResult($sql, 1, false); $res = $dbr->query( $sql, __FUNCTION__ ); $row = $dbr->fetchRow( $res ); if( $row !== false ) { @@ -64,13 +65,12 @@ function wfSpecialNewimages( $par, $specialPage ) { } $where = array(); - $searchpar = ''; + $searchpar = array(); if ( $wpIlMatch != '' && !$wgMiserMode) { - $nt = Title::newFromUrl( $wpIlMatch ); + $nt = Title::newFromURL( $wpIlMatch ); if( $nt ) { - $m = $dbr->escapeLike( strtolower( $nt->getDBkey() ) ); - $where[] = "LOWER(img_name) LIKE '%{$m}%'"; - $searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch ); + $where[] = 'LOWER(img_name) ' . $dbr->buildLike( $dbr->anyString(), strtolower( $nt->getDBkey() ), $dbr->anyString() ); + $searchpar['wpIlMatch'] = $wpIlMatch; } } @@ -93,7 +93,7 @@ function wfSpecialNewimages( $par, $specialPage ) { $sql .= ' WHERE ' . $dbr->makeList( $where, LIST_AND ); } $sql.=' ORDER BY img_timestamp '. ( $invertSort ? '' : ' DESC' ); - $sql.=' LIMIT ' . ( $limit + 1 ); + $sql = $dbr->limitResult($sql, ( $limit + 1 ), false); $res = $dbr->query( $sql, __FUNCTION__ ); /** @@ -125,9 +125,9 @@ function wfSpecialNewimages( $par, $specialPage ) { $ut = $s->img_user_text; $nt = Title::newFromText( $name, NS_FILE ); - $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut ); + $ul = $sk->link( Title::makeTitle( NS_USER, $ut ), $ut ); - $gallery->add( $nt, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" ); + $gallery->add( $nt, "$ul<br />\n<i>".htmlspecialchars($wgLang->timeanddate( $s->img_timestamp, true ))."</i><br />\n" ); $timestamp = wfTimestamp( TS_MW, $s->img_timestamp ); if( empty( $firstTimestamp ) ) { @@ -162,29 +162,72 @@ function wfSpecialNewimages( $par, $specialPage ) { # If we change bot visibility, this needs to be carried along. if( !$hidebots ) { - $botpar = '&hidebots=0'; + $botpar = array( 'hidebots' => 0 ); } else { - $botpar = ''; + $botpar = array(); } $now = wfTimestampNow(); $d = $wgLang->date( $now, true ); $t = $wgLang->time( $now, true ); - $dateLink = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml( 'sp-newimages-showfrom', $d, $t ), - 'from='.$now.$botpar.$searchpar ); - - $botLink = $sk->makeKnownLinkObj($titleObj, wfMsgHtml( 'showhidebots', - ($hidebots ? wfMsgHtml('show') : wfMsgHtml('hide'))),'hidebots='.($hidebots ? '0' : '1').$searchpar); - + $query = array_merge( + array( 'from' => $now ), + $botpar, + $searchpar + ); + + $dateLink = $sk->linkKnown( + $titleObj, + htmlspecialchars( wfMsg( 'sp-newimages-showfrom', $d, $t ) ), + array(), + $query + ); + + $query = array_merge( + array( 'hidebots' => ( $hidebots ? 0 : 1 ) ), + $searchpar + ); + + $showhide = $hidebots ? wfMsg( 'show' ) : wfMsg( 'hide' ); + + $botLink = $sk->linkKnown( + $titleObj, + htmlspecialchars( wfMsg( 'showhidebots', $showhide ) ), + array(), + $query + ); $opts = array( 'parsemag', 'escapenoentities' ); $prevLink = wfMsgExt( 'pager-newer-n', $opts, $wgLang->formatNum( $limit ) ); if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) { - $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar ); + $query = array_merge( + array( 'from' => $firstTimestamp ), + $botpar, + $searchpar + ); + + $prevLink = $sk->linkKnown( + $titleObj, + $prevLink, + array(), + $query + ); } $nextLink = wfMsgExt( 'pager-older-n', $opts, $wgLang->formatNum( $limit ) ); if( $shownImages > $limit && $lastTimestamp ) { - $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar ); + $query = array_merge( + array( 'until' => $lastTimestamp ), + $botpar, + $searchpar + ); + + $nextLink = $sk->linkKnown( + $titleObj, + $nextLink, + array(), + $query + ); + } $prevnext = '<p>' . $botLink . ' '. wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $dateLink ) .'</p>'; diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 886c41a2..903ddab0 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -89,7 +89,7 @@ class SpecialNewpages extends SpecialPage { * @return string */ public function execute( $par ) { - global $wgLang, $wgUser, $wgOut; + global $wgLang, $wgOut; $this->setHeaders(); $this->outputHeader(); @@ -165,6 +165,7 @@ class SpecialNewpages extends SpecialPage { $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW $namespace = $this->opts->consumeValue( 'namespace' ); $username = $this->opts->consumeValue( 'username' ); + $tagFilterVal = $this->opts->consumeValue( 'tagfilter' ); // Check username input validity $ut = Title::makeTitleSafe( NS_USER, $username ); @@ -177,7 +178,7 @@ class SpecialNewpages extends SpecialPage { } $hidden = implode( "\n", $hidden ); - $tagFilter = ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] ); + $tagFilter = ChangeTags::buildTagFilterSelector( $tagFilterVal ); if ($tagFilter) list( $tagFilterLabel, $tagFilterSelector ) = $tagFilter; @@ -231,12 +232,8 @@ class SpecialNewpages extends SpecialPage { protected function setSyndicated() { global $wgOut; - $queryParams = array( - 'namespace' => $this->opts->getValue( 'namespace' ), - 'username' => $this->opts->getValue( 'username' ) - ); $wgOut->setSyndicated( true ); - $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) ); + $wgOut->setFeedAppendQuery( wfArrayToCGI( $this->opts->getAllValues() ) ); } /** @@ -247,17 +244,32 @@ class SpecialNewpages extends SpecialPage { * @return string */ public function formatRow( $result ) { - global $wgLang, $wgContLang, $wgUser; + global $wgLang, $wgContLang; $classes = array(); $dm = $wgContLang->getDirMark(); $title = Title::makeTitleSafe( $result->rc_namespace, $result->rc_title ); - $time = $wgLang->timeAndDate( $result->rc_timestamp, true ); - $query = $this->patrollable( $result ) ? "rcid={$result->rc_id}&redirect=no" : 'redirect=no'; - $plink = $this->skin->makeKnownLinkObj( $title, '', $query ); - $hist = $this->skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' ); + $time = htmlspecialchars( $wgLang->timeAndDate( $result->rc_timestamp, true ) ); + + $query = array( 'redirect' => 'no' ); + + if( $this->patrollable( $result ) ) + $query['rcid'] = $result->rc_id; + + $plink = $this->skin->linkKnown( + $title, + null, + array(), + $query + ); + $hist = $this->skin->linkKnown( + $title, + wfMsgHtml( 'hist' ), + array(), + array( 'action' => 'history' ) + ); $length = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->length ) ); $ulink = $this->skin->userLink( $result->rc_user, $result->rc_user_text ) . ' ' . @@ -345,7 +357,7 @@ class SpecialNewpages extends SpecialPage { $this->feedItemAuthor( $row ), $comments); } else { - return NULL; + return null; } } diff --git a/includes/specials/SpecialPopularpages.php b/includes/specials/SpecialPopularpages.php index eb572736..88b90bc3 100644 --- a/includes/specials/SpecialPopularpages.php +++ b/includes/specials/SpecialPopularpages.php @@ -48,9 +48,15 @@ class PopularPagesPage extends QueryPage { function formatResult( $skin, $result ) { global $wgLang, $wgContLang; $title = Title::makeTitle( $result->namespace, $result->title ); - $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) ); - $nv = wfMsgExt( 'nviews', array( 'parsemag', 'escape'), - $wgLang->formatNum( $result->value ) ); + $link = $skin->linkKnown( + $title, + htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) + ); + $nv = wfMsgExt( + 'nviews', + array( 'parsemag', 'escape'), + $wgLang->formatNum( $result->value ) + ); return wfSpecialList($link, $nv); } } diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index 49c4f4e0..4c8bbb09 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -1,1308 +1,75 @@ <?php -/** - * Hold things related to displaying and saving user preferences. - * @file - * @ingroup SpecialPage - */ -/** - * Entry point that create the "Preferences" object - */ -function wfSpecialPreferences() { - global $wgRequest; - - $form = new PreferencesForm( $wgRequest ); - $form->execute(); -} - -/** - * Preferences form handling - * This object will show the preferences form and can save it as well. - * @ingroup SpecialPage - */ -class PreferencesForm { - var $mQuickbar, $mStubs; - var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick; - var $mUserLanguage, $mUserVariant; - var $mSearch, $mRecent, $mRecentDays, $mTimeZone, $mHourDiff, $mSearchLines, $mSearchChars, $mAction; - var $mReset, $mPosted, $mToggles, $mSearchNs, $mRealName, $mImageSize; - var $mUnderline, $mWatchlistEdits, $mGender; - - /** - * Constructor - * Load some values - */ - function __construct( &$request ) { - global $wgContLang, $wgUser, $wgAllowRealName; - - $this->mQuickbar = $request->getVal( 'wpQuickbar' ); - $this->mStubs = $request->getVal( 'wpStubs' ); - $this->mRows = $request->getVal( 'wpRows' ); - $this->mCols = $request->getVal( 'wpCols' ); - $this->mSkin = Skin::normalizeKey( $request->getVal( 'wpSkin' ) ); - $this->mMath = $request->getVal( 'wpMath' ); - $this->mDate = $request->getVal( 'wpDate' ); - $this->mUserEmail = $request->getVal( 'wpUserEmail' ); - $this->mRealName = $wgAllowRealName ? $request->getVal( 'wpRealName' ) : ''; - $this->mEmailFlag = $request->getCheck( 'wpEmailFlag' ) ? 0 : 1; - $this->mNick = $request->getVal( 'wpNick' ); - $this->mUserLanguage = $request->getVal( 'wpUserLanguage' ); - $this->mUserVariant = $request->getVal( 'wpUserVariant' ); - $this->mSearch = $request->getVal( 'wpSearch' ); - $this->mRecent = $request->getVal( 'wpRecent' ); - $this->mRecentDays = $request->getVal( 'wpRecentDays' ); - $this->mTimeZone = $request->getVal( 'wpTimeZone' ); - $this->mHourDiff = $request->getVal( 'wpHourDiff' ); - $this->mSearchLines = $request->getVal( 'wpSearchLines' ); - $this->mSearchChars = $request->getVal( 'wpSearchChars' ); - $this->mImageSize = $request->getVal( 'wpImageSize' ); - $this->mThumbSize = $request->getInt( 'wpThumbSize' ); - $this->mUnderline = $request->getInt( 'wpOpunderline' ); - $this->mAction = $request->getVal( 'action' ); - $this->mReset = $request->getCheck( 'wpReset' ); - $this->mRestoreprefs = $request->getCheck( 'wpRestore' ); - $this->mPosted = $request->wasPosted(); - $this->mSuccess = $request->getCheck( 'success' ); - $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' ); - $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' ); - $this->mDisableMWSuggest = $request->getCheck( 'wpDisableMWSuggest' ); - $this->mGender = $request->getVal( 'wpGender' ); - - $this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) && - $this->mPosted && - $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) ); - - # User toggles (the big ugly unsorted list of checkboxes) - $this->mToggles = array(); - if ( $this->mPosted ) { - $togs = User::getToggles(); - foreach ( $togs as $tname ) { - $this->mToggles[$tname] = $request->getCheck( "wpOp$tname" ) ? 1 : 0; - } - } - - $this->mUsedToggles = array(); - - # Search namespace options - # Note: namespaces don't necessarily have consecutive keys - $this->mSearchNs = array(); - if ( $this->mPosted ) { - $namespaces = $wgContLang->getNamespaces(); - foreach ( $namespaces as $i => $namespace ) { - if ( $i >= 0 ) { - $this->mSearchNs[$i] = $request->getCheck( "wpNs$i" ) ? 1 : 0; - } - } - } - - # Validate language - if ( !preg_match( '/^[a-z\-]*$/', $this->mUserLanguage ) ) { - $this->mUserLanguage = 'nolanguage'; - } - - wfRunHooks( 'InitPreferencesForm', array( $this, $request ) ); +class SpecialPreferences extends SpecialPage { + function __construct() { + parent::__construct( 'Preferences' ); } - function execute() { - global $wgUser, $wgOut, $wgTitle; + function execute( $par ) { + global $wgOut, $wgUser, $wgRequest; + + $this->setHeaders(); + $this->outputHeader(); + $wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc. if ( $wgUser->isAnon() ) { - $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext', array($wgTitle->getPrefixedDBkey()) ); + $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext', array( $this->getTitle()->getPrefixedDBkey() ) ); return; } if ( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - if ( $this->mReset ) { - $this->resetPrefs(); - $this->mainPrefsForm( 'reset', wfMsg( 'prefsreset' ) ); - } else if ( $this->mSaveprefs ) { - $this->savePreferences(); - } else if ( $this->mRestoreprefs ) { - $this->restorePreferences(); - } else { - $this->resetPrefs(); - $this->mainPrefsForm( '' ); - } - } - /** - * @access private - */ - function validateInt( &$val, $min=0, $max=0x7fffffff ) { - $val = intval($val); - $val = min($val, $max); - $val = max($val, $min); - return $val; - } - /** - * @access private - */ - function validateFloat( &$val, $min, $max=0x7fffffff ) { - $val = floatval( $val ); - $val = min( $val, $max ); - $val = max( $val, $min ); - return( $val ); - } - - /** - * @access private - */ - function validateIntOrNull( &$val, $min=0, $max=0x7fffffff ) { - $val = trim($val); - if($val === '') { - return null; - } else { - return $this->validateInt( $val, $min, $max ); - } - } - - /** - * @access private - */ - function validateDate( $val ) { - global $wgLang, $wgContLang; - if ( $val !== false && ( - in_array( $val, (array)$wgLang->getDatePreferences() ) || - in_array( $val, (array)$wgContLang->getDatePreferences() ) ) ) - { - return $val; - } else { - return $wgLang->getDefaultDateFormat(); - } - } - - /** - * Used to validate the user inputed timezone before saving it as - * 'timecorrection', will return 'System' if fed bogus data. - * @access private - * @param string $tz the user input Zoneinfo timezone - * @param string $s the user input offset string - * @return string - */ - function validateTimeZone( $tz, $s ) { - $data = explode( '|', $tz, 3 ); - switch ( $data[0] ) { - case 'ZoneInfo': - case 'System': - return $tz; - case 'Offset': - default: - $data = explode( ':', $s, 2 ); - $minDiff = 0; - if( count( $data ) == 2 ) { - $data[0] = intval( $data[0] ); - $data[1] = intval( $data[1] ); - $minDiff = abs( $data[0] ) * 60 + $data[1]; - if ( $data[0] < 0 ) $minDiff = -$minDiff; - } else { - $minDiff = intval( $data[0] ) * 60; - } - - # Max is +14:00 and min is -12:00, see: - # http://en.wikipedia.org/wiki/Timezone - $minDiff = min( $minDiff, 840 ); # 14:00 - $minDiff = max( $minDiff, -720 ); # -12:00 - return 'Offset|'.$minDiff; - } - } - - function validateGender( $val ) { - $valid = array( 'male', 'female', 'unknown' ); - if ( in_array($val, $valid) ) { - return $val; - } else { - return User::getDefaultOption( 'gender' ); - } - } - - /** - * @access private - */ - function savePreferences() { - global $wgUser, $wgOut, $wgParser; - global $wgEnableUserEmail, $wgEnableEmail; - global $wgEmailAuthentication, $wgRCMaxAge; - global $wgAuth, $wgEmailConfirmToEdit; - - $wgUser->setRealName( $this->mRealName ); - $oldOptions = $wgUser->mOptions; - - if( $wgUser->getOption( 'language' ) !== $this->mUserLanguage ) { - $needRedirect = true; - } else { - $needRedirect = false; - } - - # Validate the signature and clean it up as needed - global $wgMaxSigChars; - if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) { - global $wgLang; - $this->mainPrefsForm( 'error', - wfMsgExt( 'badsiglength', 'parsemag', $wgLang->formatNum( $wgMaxSigChars ) ) ); + if ( $par == 'reset' ) { + $this->showResetForm(); return; - } elseif( $this->mToggles['fancysig'] ) { - if( $wgParser->validateSig( $this->mNick ) !== false ) { - $this->mNick = $wgParser->cleanSig( $this->mNick ); - } else { - $this->mainPrefsForm( 'error', wfMsg( 'badsig' ) ); - return; - } - } else { - // When no fancy sig used, make sure ~{3,5} get removed. - $this->mNick = $wgParser->cleanSigInSig( $this->mNick ); - } - - $wgUser->setOption( 'language', $this->mUserLanguage ); - $wgUser->setOption( 'variant', $this->mUserVariant ); - $wgUser->setOption( 'nickname', $this->mNick ); - $wgUser->setOption( 'quickbar', $this->mQuickbar ); - global $wgAllowUserSkin; - if( $wgAllowUserSkin ) { - $wgUser->setOption( 'skin', $this->mSkin ); - } - global $wgUseTeX; - if( $wgUseTeX ) { - $wgUser->setOption( 'math', $this->mMath ); - } - $wgUser->setOption( 'date', $this->validateDate( $this->mDate ) ); - $wgUser->setOption( 'searchlimit', $this->validateIntOrNull( $this->mSearch ) ); - $wgUser->setOption( 'contextlines', $this->validateIntOrNull( $this->mSearchLines ) ); - $wgUser->setOption( 'contextchars', $this->validateIntOrNull( $this->mSearchChars ) ); - $wgUser->setOption( 'rclimit', $this->validateIntOrNull( $this->mRecent ) ); - $wgUser->setOption( 'rcdays', $this->validateInt($this->mRecentDays, 1, ceil($wgRCMaxAge / (3600*24)))); - $wgUser->setOption( 'wllimit', $this->validateIntOrNull( $this->mWatchlistEdits, 0, 1000 ) ); - $wgUser->setOption( 'rows', $this->validateInt( $this->mRows, 4, 1000 ) ); - $wgUser->setOption( 'cols', $this->validateInt( $this->mCols, 4, 1000 ) ); - $wgUser->setOption( 'stubthreshold', $this->validateIntOrNull( $this->mStubs ) ); - $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mTimeZone, $this->mHourDiff ) ); - $wgUser->setOption( 'imagesize', $this->mImageSize ); - $wgUser->setOption( 'thumbsize', $this->mThumbSize ); - $wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) ); - $wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) ); - $wgUser->setOption( 'disablesuggest', $this->mDisableMWSuggest ); - $wgUser->setOption( 'gender', $this->validateGender( $this->mGender ) ); - - # Set search namespace options - foreach( $this->mSearchNs as $i => $value ) { - $wgUser->setOption( "searchNs{$i}", $value ); - } - - if( $wgEnableEmail && $wgEnableUserEmail ) { - $wgUser->setOption( 'disablemail', $this->mEmailFlag ); - } - - # Set user toggles - foreach ( $this->mToggles as $tname => $tvalue ) { - $wgUser->setOption( $tname, $tvalue ); - } - - $error = false; - if( $wgEnableEmail ) { - $newadr = $this->mUserEmail; - $oldadr = $wgUser->getEmail(); - if( ($newadr != '') && ($newadr != $oldadr) ) { - # the user has supplied a new email address on the login page - if( $wgUser->isValidEmailAddr( $newadr ) ) { - # new behaviour: set this new emailaddr from login-page into user database record - $wgUser->setEmail( $newadr ); - # but flag as "dirty" = unauthenticated - $wgUser->invalidateEmail(); - if ($wgEmailAuthentication) { - # Mail a temporary password to the dirty address. - # User can come back through the confirmation URL to re-enable email. - $result = $wgUser->sendConfirmationMail(); - if( WikiError::isError( $result ) ) { - $error = wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) ); - } else { - $error = wfMsg( 'eauthentsent', $wgUser->getName() ); - } - } - } else { - $error = wfMsg( 'invalidemailaddress' ); - } - } else { - if( $wgEmailConfirmToEdit && empty( $newadr ) ) { - $this->mainPrefsForm( 'error', wfMsg( 'noemailtitle' ) ); - return; - } - $wgUser->setEmail( $this->mUserEmail ); - } - if( $oldadr != $newadr ) { - wfRunHooks( 'PrefsEmailAudit', array( $wgUser, $oldadr, $newadr ) ); - } } + + $wgOut->addScriptFile( 'prefs.js' ); - if( !$wgAuth->updateExternalDB( $wgUser ) ){ - $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) ); - return; + if ( $wgRequest->getCheck( 'success' ) ) { + $wgOut->wrapWikiMsg( + '<div class="successbox"><strong>$1</strong></div><div id="mw-pref-clear"></div>', + 'savedprefs' + ); } - - $msg = ''; - if ( !wfRunHooks( 'SavePreferences', array( $this, $wgUser, &$msg, $oldOptions ) ) ) { - $this->mainPrefsForm( 'error', $msg ); - return; + + if ( $wgRequest->getCheck( 'eauth' ) ) { + $wgOut->wrapWikiMsg( "<div class='error' style='clear: both;'>\n$1</div>", + 'eauthentsent', $wgUser->getName() ); } - $wgUser->setCookies(); - $wgUser->saveSettings(); - - if( $needRedirect && $error === false ) { - $title = SpecialPage::getTitleFor( 'Preferences' ); - $wgOut->redirect( $title->getFullURL( 'success' ) ); - return; - } + $htmlForm = Preferences::getFormObject( $wgUser ); + $htmlForm->setSubmitCallback( array( 'Preferences', 'tryUISubmit' ) ); - $wgOut->parserOptions( ParserOptions::newFromUser( $wgUser ) ); - $this->mainPrefsForm( $error === false ? 'success' : 'error', $error); + $htmlForm->show(); } - /** - * @access private - */ - function resetPrefs() { - global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName, $wgLocalTZoffset; + function showResetForm() { + global $wgOut; - $this->mUserEmail = $wgUser->getEmail(); - $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp(); - $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : ''; + $wgOut->addWikiMsg( 'prefs-reset-intro' ); - # language value might be blank, default to content language - $this->mUserLanguage = $wgUser->getOption( 'language', $wgContLanguageCode ); + $htmlForm = new HTMLForm( array(), 'prefs-restore' ); - $this->mUserVariant = $wgUser->getOption( 'variant'); - $this->mEmailFlag = $wgUser->getOption( 'disablemail' ) == 1 ? 1 : 0; - $this->mNick = $wgUser->getOption( 'nickname' ); + $htmlForm->setSubmitText( wfMsg( 'restoreprefs' ) ); + $htmlForm->setTitle( $this->getTitle( 'reset' ) ); + $htmlForm->setSubmitCallback( array( __CLASS__, 'submitReset' ) ); + $htmlForm->suppressReset(); - $this->mQuickbar = $wgUser->getOption( 'quickbar' ); - $this->mSkin = Skin::normalizeKey( $wgUser->getOption( 'skin' ) ); - $this->mMath = $wgUser->getOption( 'math' ); - $this->mDate = $wgUser->getDatePreference(); - $this->mRows = $wgUser->getOption( 'rows' ); - $this->mCols = $wgUser->getOption( 'cols' ); - $this->mStubs = $wgUser->getOption( 'stubthreshold' ); - - $tz = $wgUser->getOption( 'timecorrection' ); - $data = explode( '|', $tz, 3 ); - $minDiff = null; - switch ( $data[0] ) { - case 'ZoneInfo': - $this->mTimeZone = $tz; - # Check if the specified TZ exists, and change to 'Offset' if - # not. - if ( !function_exists('timezone_open') || @timezone_open( $data[2] ) === false ) { - $this->mTimeZone = 'Offset'; - $minDiff = intval( $data[1] ); - } - break; - case '': - case 'System': - $this->mTimeZone = 'System|'.$wgLocalTZoffset; - break; - case 'Offset': - $this->mTimeZone = 'Offset'; - $minDiff = intval( $data[1] ); - break; - default: - $this->mTimeZone = 'Offset'; - $data = explode( ':', $tz, 2 ); - if( count( $data ) == 2 ) { - $data[0] = intval( $data[0] ); - $data[1] = intval( $data[1] ); - $minDiff = abs( $data[0] ) * 60 + $data[1]; - if ( $data[0] < 0 ) $minDiff = -$minDiff; - } else { - $minDiff = intval( $data[0] ) * 60; - } - break; - } - if ( is_null( $minDiff ) ) { - $this->mHourDiff = ''; - } else { - $this->mHourDiff = sprintf( '%+03d:%02d', floor($minDiff/60), abs($minDiff)%60 ); - } - - $this->mSearch = $wgUser->getOption( 'searchlimit' ); - $this->mSearchLines = $wgUser->getOption( 'contextlines' ); - $this->mSearchChars = $wgUser->getOption( 'contextchars' ); - $this->mImageSize = $wgUser->getOption( 'imagesize' ); - $this->mThumbSize = $wgUser->getOption( 'thumbsize' ); - $this->mRecent = $wgUser->getOption( 'rclimit' ); - $this->mRecentDays = $wgUser->getOption( 'rcdays' ); - $this->mWatchlistEdits = $wgUser->getOption( 'wllimit' ); - $this->mUnderline = $wgUser->getOption( 'underline' ); - $this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' ); - $this->mDisableMWSuggest = $wgUser->getBoolOption( 'disablesuggest' ); - $this->mGender = $wgUser->getOption( 'gender' ); - - $togs = User::getToggles(); - foreach ( $togs as $tname ) { - $this->mToggles[$tname] = $wgUser->getOption( $tname ); - } - - $namespaces = $wgContLang->getNamespaces(); - foreach ( $namespaces as $i => $namespace ) { - if ( $i >= NS_MAIN ) { - $this->mSearchNs[$i] = $wgUser->getOption( 'searchNs'.$i ); - } - } - - wfRunHooks( 'ResetPreferences', array( $this, $wgUser ) ); + $htmlForm->show(); } - - /** - * @access private - */ - function restorePreferences() { + + static function submitReset( $formData ) { global $wgUser, $wgOut; - $wgUser->restoreOptions(); - $wgUser->setCookies(); + $wgUser->resetOptions(); $wgUser->saveSettings(); - $title = SpecialPage::getTitleFor( 'Preferences' ); - $wgOut->redirect( $title->getFullURL( 'success' ) ); - } - - /** - * @access private - */ - function namespacesCheckboxes() { - global $wgContLang; - - # Determine namespace checkboxes - $namespaces = $wgContLang->getNamespaces(); - $r1 = null; - - foreach ( $namespaces as $i => $name ) { - if ($i < 0) - continue; - $checked = $this->mSearchNs[$i] ? "checked='checked'" : ''; - $name = str_replace( '_', ' ', $namespaces[$i] ); - - if ( empty($name) ) - $name = wfMsg( 'blanknamespace' ); - - $r1 .= "<input type='checkbox' value='1' name='wpNs$i' id='wpNs$i' {$checked}/> <label for='wpNs$i'>{$name}</label><br />\n"; - } - return $r1; - } - - - function getToggle( $tname, $trailer = false, $disabled = false ) { - global $wgUser, $wgLang; - - $this->mUsedToggles[$tname] = true; - $ttext = $wgLang->getUserToggle( $tname ); - - $checked = $wgUser->getOption( $tname ) == 1 ? ' checked="checked"' : ''; - $disabled = $disabled ? ' disabled="disabled"' : ''; - $trailer = $trailer ? $trailer : ''; - return "<div class='toggle'><input type='checkbox' value='1' id=\"$tname\" name=\"wpOp$tname\"$checked$disabled />" . - " <span class='toggletext'><label for=\"$tname\">$ttext</label>$trailer</span></div>\n"; - } - - function getToggles( $items ) { - $out = ""; - foreach( $items as $item ) { - if( $item === false ) - continue; - if( is_array( $item ) ) { - list( $key, $trailer ) = $item; - } else { - $key = $item; - $trailer = false; - } - $out .= $this->getToggle( $key, $trailer ); - } - return $out; - } - - function addRow($td1, $td2) { - return "<tr><td class='mw-label'>$td1</td><td class='mw-input'>$td2</td></tr>"; - } - - /** - * Helper function for user information panel - * @param $td1 label for an item - * @param $td2 item or null - * @param $td3 optional help or null - * @return xhtml block - */ - function tableRow( $td1, $td2 = null, $td3 = null ) { - - if ( is_null( $td3 ) ) { - $td3 = ''; - } else { - $td3 = Xml::tags( 'tr', null, - Xml::tags( 'td', array( 'class' => 'pref-label', 'colspan' => '2' ), $td3 ) - ); - } - - if ( is_null( $td2 ) ) { - $td1 = Xml::tags( 'td', array( 'class' => 'pref-label', 'colspan' => '2' ), $td1 ); - $td2 = ''; - } else { - $td1 = Xml::tags( 'td', array( 'class' => 'pref-label' ), $td1 ); - $td2 = Xml::tags( 'td', array( 'class' => 'pref-input' ), $td2 ); - } - - return Xml::tags( 'tr', null, $td1 . $td2 ). $td3 . "\n"; - - } - - /** - * @access private - */ - function mainPrefsForm( $status , $message = '' ) { - global $wgUser, $wgOut, $wgLang, $wgContLang, $wgAuth; - global $wgAllowRealName, $wgImageLimits, $wgThumbLimits; - global $wgDisableLangConversion, $wgDisableTitleConversion; - global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits; - global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress; - global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication; - global $wgContLanguageCode, $wgDefaultSkin, $wgCookieExpiration; - global $wgEmailConfirmToEdit, $wgEnableMWSuggest, $wgLocalTZoffset; - - $wgOut->setPageTitle( wfMsg( 'preferences' ) ); - $wgOut->setArticleRelated( false ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->addScriptFile( 'prefs.js' ); - - $wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc. - - if ( $this->mSuccess || 'success' == $status ) { - $wgOut->wrapWikiMsg( '<div class="successbox"><strong>$1</strong></div>', 'savedprefs' ); - } else if ( 'error' == $status ) { - $wgOut->addWikiText( '<div class="errorbox"><strong>' . $message . '</strong></div>' ); - } else if ( '' != $status ) { - $wgOut->addWikiText( $message . "\n----" ); - } - - $qbs = $wgLang->getQuickbarSettings(); - $mathopts = $wgLang->getMathNames(); - $dateopts = $wgLang->getDatePreferences(); - $togs = User::getToggles(); - - $titleObj = SpecialPage::getTitleFor( 'Preferences' ); - - # Pre-expire some toggles so they won't show if disabled - $this->mUsedToggles[ 'shownumberswatching' ] = true; - $this->mUsedToggles[ 'showupdated' ] = true; - $this->mUsedToggles[ 'enotifwatchlistpages' ] = true; - $this->mUsedToggles[ 'enotifusertalkpages' ] = true; - $this->mUsedToggles[ 'enotifminoredits' ] = true; - $this->mUsedToggles[ 'enotifrevealaddr' ] = true; - $this->mUsedToggles[ 'ccmeonemails' ] = true; - $this->mUsedToggles[ 'uselivepreview' ] = true; - $this->mUsedToggles[ 'noconvertlink' ] = true; - - - if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; } - else { $emfc = ''; } - - - if ($wgEmailAuthentication && ($this->mUserEmail != '') ) { - if( $wgUser->getEmailAuthenticationTimestamp() ) { - // date and time are separate parameters to facilitate localisation. - // $time is kept for backward compat reasons. - // 'emailauthenticated' is also used in SpecialConfirmemail.php - $time = $wgLang->timeAndDate( $wgUser->getEmailAuthenticationTimestamp(), true ); - $d = $wgLang->date( $wgUser->getEmailAuthenticationTimestamp(), true ); - $t = $wgLang->time( $wgUser->getEmailAuthenticationTimestamp(), true ); - $emailauthenticated = wfMsg('emailauthenticated', $time, $d, $t ).'<br />'; - $disableEmailPrefs = false; - } else { - $disableEmailPrefs = true; - $skin = $wgUser->getSkin(); - $emailauthenticated = wfMsg('emailnotauthenticated').'<br />' . - $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Confirmemail' ), - wfMsg( 'emailconfirmlink' ) ) . '<br />'; - } - } else { - $emailauthenticated = ''; - $disableEmailPrefs = false; - } - - if ($this->mUserEmail == '') { - $emailauthenticated = wfMsg( 'noemailprefs' ) . '<br />'; - } - - $ps = $this->namespacesCheckboxes(); - - $enotifwatchlistpages = ($wgEnotifWatchlist) ? $this->getToggle( 'enotifwatchlistpages', false, $disableEmailPrefs ) : ''; - $enotifusertalkpages = ($wgEnotifUserTalk) ? $this->getToggle( 'enotifusertalkpages', false, $disableEmailPrefs ) : ''; - $enotifminoredits = ($wgEnotifWatchlist && $wgEnotifMinorEdits) ? $this->getToggle( 'enotifminoredits', false, $disableEmailPrefs ) : ''; - $enotifrevealaddr = (($wgEnotifWatchlist || $wgEnotifUserTalk) && $wgEnotifRevealEditorAddress) ? $this->getToggle( 'enotifrevealaddr', false, $disableEmailPrefs ) : ''; - - # </FIXME> - - $wgOut->addHTML( - Xml::openElement( 'form', array( - 'action' => $titleObj->getLocalUrl(), - 'method' => 'post', - 'id' => 'mw-preferences-form', - ) ) . - Xml::openElement( 'div', array( 'id' => 'preferences' ) ) - ); - - # User data - - $wgOut->addHTML( - Xml::fieldset( wfMsg('prefs-personal') ) . - Xml::openElement( 'table' ) . - $this->tableRow( Xml::element( 'h2', null, wfMsg( 'prefs-personal' ) ) ) - ); - - # Get groups to which the user belongs - $userEffectiveGroups = $wgUser->getEffectiveGroups(); - $userEffectiveGroupsArray = array(); - foreach( $userEffectiveGroups as $ueg ) { - if( $ueg == '*' ) { - // Skip the default * group, seems useless here - continue; - } - $userEffectiveGroupsArray[] = User::makeGroupLinkHTML( $ueg ); - } - asort( $userEffectiveGroupsArray ); - - $sk = $wgUser->getSkin(); - $toolLinks = array(); - $toolLinks[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'ListGroupRights' ), wfMsg( 'listgrouprights' ) ); - # At the moment one tool link only but be prepared for the future... - # FIXME: Add a link to Special:Userrights for users who are allowed to use it. - # $wgUser->isAllowed( 'userrights' ) seems to strict in some cases - - $userInformationHtml = - $this->tableRow( wfMsgHtml( 'username' ), htmlspecialchars( $wgUser->getName() ) ) . - $this->tableRow( wfMsgHtml( 'uid' ), htmlspecialchars( $wgUser->getId() ) ) . - - $this->tableRow( - wfMsgExt( 'prefs-memberingroups', array( 'parseinline' ), count( $userEffectiveGroupsArray ) ), - $wgLang->commaList( $userEffectiveGroupsArray ) . - '<br />(' . $wgLang->pipeList( $toolLinks ) . ')' - ) . - - $this->tableRow( - wfMsgHtml( 'prefs-edits' ), - $wgLang->formatNum( $wgUser->getEditCount() ) - ); - - if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) { - $wgOut->addHTML( $userInformationHtml ); - } - - if ( $wgAllowRealName ) { - $wgOut->addHTML( - $this->tableRow( - Xml::label( wfMsg('yourrealname'), 'wpRealName' ), - Xml::input( 'wpRealName', 25, $this->mRealName, array( 'id' => 'wpRealName' ) ), - Xml::tags('div', array( 'class' => 'prefsectiontip' ), - wfMsgExt( 'prefs-help-realname', 'parseinline' ) - ) - ) - ); - } - if ( $wgEnableEmail ) { - $wgOut->addHTML( - $this->tableRow( - Xml::label( wfMsg('youremail'), 'wpUserEmail' ), - Xml::input( 'wpUserEmail', 25, $this->mUserEmail, array( 'id' => 'wpUserEmail' ) ), - Xml::tags('div', array( 'class' => 'prefsectiontip' ), - wfMsgExt( $wgEmailConfirmToEdit ? 'prefs-help-email-required' : 'prefs-help-email', 'parseinline' ) - ) - ) - ); - } - - global $wgParser, $wgMaxSigChars; - if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) { - $invalidSig = $this->tableRow( - ' ', - Xml::element( 'span', array( 'class' => 'error' ), - wfMsgExt( 'badsiglength', 'parsemag', $wgLang->formatNum( $wgMaxSigChars ) ) ) - ); - } elseif( !empty( $this->mToggles['fancysig'] ) && - false === $wgParser->validateSig( $this->mNick ) ) { - $invalidSig = $this->tableRow( - ' ', - Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) ) - ); - } else { - $invalidSig = ''; - } - - $wgOut->addHTML( - $this->tableRow( - Xml::label( wfMsg( 'yournick' ), 'wpNick' ), - Xml::input( 'wpNick', 25, $this->mNick, - array( - 'id' => 'wpNick', - // Note: $wgMaxSigChars is enforced in Unicode characters, - // both on the backend and now in the browser. - // Badly-behaved requests may still try to submit - // an overlong string, however. - 'maxlength' => $wgMaxSigChars ) ) - ) . - $invalidSig . - $this->tableRow( ' ', $this->getToggle( 'fancysig' ) ) - ); - - $gender = new XMLSelect( 'wpGender', 'wpGender', $this->mGender ); - $gender->addOption( wfMsg( 'gender-unknown' ), 'unknown' ); - $gender->addOption( wfMsg( 'gender-male' ), 'male' ); - $gender->addOption( wfMsg( 'gender-female' ), 'female' ); - - $wgOut->addHTML( - $this->tableRow( - Xml::label( wfMsg( 'yourgender' ), 'wpGender' ), - $gender->getHTML(), - Xml::tags( 'div', array( 'class' => 'prefsectiontip' ), - wfMsgExt( 'prefs-help-gender', 'parseinline' ) - ) - ) - ); - - list( $lsLabel, $lsSelect) = Xml::languageSelector( $this->mUserLanguage, false ); - $wgOut->addHTML( - $this->tableRow( $lsLabel, $lsSelect ) - ); - - /* see if there are multiple language variants to choose from*/ - if(!$wgDisableLangConversion) { - $variants = $wgContLang->getVariants(); - $variantArray = array(); - - $languages = Language::getLanguageNames( true ); - foreach($variants as $v) { - $v = str_replace( '_', '-', strtolower($v)); - if( array_key_exists( $v, $languages ) ) { - // If it doesn't have a name, we'll pretend it doesn't exist - $variantArray[$v] = $languages[$v]; - } - } - - $options = "\n"; - foreach( $variantArray as $code => $name ) { - $selected = ($code == $this->mUserVariant); - $options .= Xml::option( "$code - $name", $code, $selected ) . "\n"; - } - - if(count($variantArray) > 1) { - $wgOut->addHTML( - $this->tableRow( - Xml::label( wfMsg( 'yourvariant' ), 'wpUserVariant' ), - Xml::tags( 'select', - array( 'name' => 'wpUserVariant', 'id' => 'wpUserVariant' ), - $options - ) - ) - ); - } - - if(count($variantArray) > 1 && !$wgDisableLangConversion && !$wgDisableTitleConversion) { - $wgOut->addHTML( - Xml::tags( 'tr', null, - Xml::tags( 'td', array( 'colspan' => '2' ), - $this->getToggle( "noconvertlink" ) - ) - ) - ); - } - } - - # Password - if( $wgAuth->allowPasswordChange() ) { - $link = $wgUser->getSkin()->link( SpecialPage::getTitleFor( 'ResetPass' ), wfMsgHtml( 'prefs-resetpass' ), - array() , array( 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ) ); - $wgOut->addHTML( - $this->tableRow( Xml::element( 'h2', null, wfMsg( 'changepassword' ) ) ) . - $this->tableRow( '<ul><li>' . $link . '</li></ul>' ) ); - } - - # <FIXME> - # Enotif - if ( $wgEnableEmail ) { - - $moreEmail = ''; - if ($wgEnableUserEmail) { - // fixme -- the "allowemail" pseudotoggle is a hacked-together - // inversion for the "disableemail" preference. - $emf = wfMsg( 'allowemail' ); - $disabled = $disableEmailPrefs ? ' disabled="disabled"' : ''; - $moreEmail = - "<input type='checkbox' $emfc $disabled value='1' name='wpEmailFlag' id='wpEmailFlag' /> <label for='wpEmailFlag'>$emf</label>" . - $this->getToggle( 'ccmeonemails', '', $disableEmailPrefs ); - } - - - $wgOut->addHTML( - $this->tableRow( Xml::element( 'h2', null, wfMsg( 'email' ) ) ) . - $this->tableRow( - $emailauthenticated. - $enotifrevealaddr. - $enotifwatchlistpages. - $enotifusertalkpages. - $enotifminoredits. - $moreEmail - ) - ); - } - # </FIXME> - - $wgOut->addHTML( - Xml::closeElement( 'table' ) . - Xml::closeElement( 'fieldset' ) - ); - - - # Quickbar - # - if ($this->mSkin == 'cologneblue' || $this->mSkin == 'standard') { - $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg( 'qbsettings' ) . "</legend>\n" ); - for ( $i = 0; $i < count( $qbs ); ++$i ) { - if ( $i == $this->mQuickbar ) { $checked = ' checked="checked"'; } - else { $checked = ""; } - $wgOut->addHTML( "<div><label><input type='radio' name='wpQuickbar' value=\"$i\"$checked />{$qbs[$i]}</label></div>\n" ); - } - $wgOut->addHTML( "</fieldset>\n\n" ); - } else { - # Need to output a hidden option even if the relevant skin is not in use, - # otherwise the preference will get reset to 0 on submit - $wgOut->addHTML( Xml::hidden( 'wpQuickbar', $this->mQuickbar ) ); - } - - # Skin - # - global $wgAllowUserSkin; - if( $wgAllowUserSkin ) { - $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg( 'skin' ) . "</legend>\n" ); - $mptitle = Title::newMainPage(); - $previewtext = wfMsg( 'skin-preview' ); - # Only show members of Skin::getSkinNames() rather than - # $skinNames (skins is all skin names from Language.php) - $validSkinNames = Skin::getUsableSkins(); - # Sort by UI skin name. First though need to update validSkinNames as sometimes - # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI). - foreach ( $validSkinNames as $skinkey => &$skinname ) { - $msgName = "skinname-{$skinkey}"; - $localisedSkinName = wfMsg( $msgName ); - if ( !wfEmptyMsg( $msgName, $localisedSkinName ) ) { - $skinname = $localisedSkinName; - } - } - asort($validSkinNames); - foreach( $validSkinNames as $skinkey => $sn ) { - $checked = $skinkey == $this->mSkin ? ' checked="checked"' : ''; - $mplink = htmlspecialchars( $mptitle->getLocalURL( "useskin=$skinkey" ) ); - $previewlink = "(<a target='_blank' href=\"$mplink\">$previewtext</a>)"; - $extraLinks = ''; - global $wgAllowUserCss, $wgAllowUserJs; - if( $wgAllowUserCss ) { - $cssPage = Title::makeTitleSafe( NS_USER, $wgUser->getName().'/'.$skinkey.'.css' ); - $customCSS = $sk->makeLinkObj( $cssPage, wfMsgExt('prefs-custom-css', array() ) ); - $extraLinks .= " ($customCSS)"; - } - if( $wgAllowUserJs ) { - $jsPage = Title::makeTitleSafe( NS_USER, $wgUser->getName().'/'.$skinkey.'.js' ); - $customJS = $sk->makeLinkObj( $jsPage, wfMsgHtml('prefs-custom-js') ); - $extraLinks .= " ($customJS)"; - } - if( $skinkey == $wgDefaultSkin ) - $sn .= ' (' . wfMsg( 'default' ) . ')'; - $wgOut->addHTML( "<input type='radio' name='wpSkin' id=\"wpSkin$skinkey\" value=\"$skinkey\"$checked /> - <label for=\"wpSkin$skinkey\">{$sn}</label> $previewlink{$extraLinks}<br />\n" ); - } - $wgOut->addHTML( "</fieldset>\n\n" ); - } - - # Math - # - global $wgUseTeX; - if( $wgUseTeX ) { - $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg('math') . '</legend>' ); - foreach ( $mathopts as $k => $v ) { - $checked = ($k == $this->mMath); - $wgOut->addHTML( - Xml::openElement( 'div' ) . - Xml::radioLabel( wfMsg( $v ), 'wpMath', $k, "mw-sp-math-$k", $checked ) . - Xml::closeElement( 'div' ) . "\n" - ); - } - $wgOut->addHTML( "</fieldset>\n\n" ); - } - - # Files - # - $imageLimitOptions = null; - foreach ( $wgImageLimits as $index => $limits ) { - $selected = ($index == $this->mImageSize); - $imageLimitOptions .= Xml::option( "{$limits[0]}×{$limits[1]}" . - wfMsg('unit-pixel'), $index, $selected ); - } - - $imageThumbOptions = null; - foreach ( $wgThumbLimits as $index => $size ) { - $selected = ($index == $this->mThumbSize); - $imageThumbOptions .= Xml::option($size . wfMsg('unit-pixel'), $index, - $selected); - } - - $imageSizeId = 'wpImageSize'; - $thumbSizeId = 'wpThumbSize'; - $wgOut->addHTML( - Xml::fieldset( wfMsg( 'files' ) ) . "\n" . - Xml::openElement( 'table' ) . - '<tr> - <td class="mw-label">' . - Xml::label( wfMsg( 'imagemaxsize' ), $imageSizeId ) . - '</td> - <td class="mw-input">' . - Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) . - $imageLimitOptions . - Xml::closeElement( 'select' ) . - '</td> - </tr><tr> - <td class="mw-label">' . - Xml::label( wfMsg( 'thumbsize' ), $thumbSizeId ) . - '</td> - <td class="mw-input">' . - Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) . - $imageThumbOptions . - Xml::closeElement( 'select' ) . - '</td> - </tr>' . - Xml::closeElement( 'table' ) . - Xml::closeElement( 'fieldset' ) - ); - - # Date format - # - # Date/Time - # - - $wgOut->addHTML( - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'datetime' ) ) . "\n" - ); - - if ($dateopts) { - $wgOut->addHTML( - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'dateformat' ) ) . "\n" - ); - $idCnt = 0; - $epoch = '20010115161234'; # Wikipedia day - foreach( $dateopts as $key ) { - if( $key == 'default' ) { - $formatted = wfMsg( 'datedefault' ); - } else { - $formatted = $wgLang->timeanddate( $epoch, false, $key ); - } - $wgOut->addHTML( - Xml::tags( 'div', null, - Xml::radioLabel( $formatted, 'wpDate', $key, "wpDate$idCnt", $key == $this->mDate ) - ) . "\n" - ); - $idCnt++; - } - $wgOut->addHTML( Xml::closeElement( 'fieldset' ) . "\n" ); - } - - $nowlocal = Xml::openElement( 'span', array( 'id' => 'wpLocalTime' ) ) . - $wgLang->time( $now = wfTimestampNow(), true ) . - Xml::closeElement( 'span' ); - $nowserver = $wgLang->time( $now, false ) . - Xml::hidden( 'wpServerTime', substr( $now, 8, 2 ) * 60 + substr( $now, 10, 2 ) ); - - $wgOut->addHTML( - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'timezonelegend' ) ) . - Xml::openElement( 'table' ) . - $this->addRow( wfMsg( 'servertime' ), $nowserver ) . - $this->addRow( wfMsg( 'localtime' ), $nowlocal ) - ); - $opt = Xml::openElement( 'select', array( - 'name' => 'wpTimeZone', - 'id' => 'wpTimeZone', - 'onchange' => 'javascript:updateTimezoneSelection(false)' ) ); - $opt .= Xml::option( wfMsg( 'timezoneuseserverdefault' ), "System|$wgLocalTZoffset", $this->mTimeZone === "System|$wgLocalTZoffset" ); - $opt .= Xml::option( wfMsg( 'timezoneuseoffset' ), 'Offset', $this->mTimeZone === 'Offset' ); - - if ( function_exists( 'timezone_identifiers_list' ) ) { - # Read timezone list - $tzs = timezone_identifiers_list(); - sort( $tzs ); - - # Precache localized region names - $tzRegions = array(); - $tzRegions['Africa'] = wfMsg( 'timezoneregion-africa' ); - $tzRegions['America'] = wfMsg( 'timezoneregion-america' ); - $tzRegions['Antarctica'] = wfMsg( 'timezoneregion-antarctica' ); - $tzRegions['Arctic'] = wfMsg( 'timezoneregion-arctic' ); - $tzRegions['Asia'] = wfMsg( 'timezoneregion-asia' ); - $tzRegions['Atlantic'] = wfMsg( 'timezoneregion-atlantic' ); - $tzRegions['Australia'] = wfMsg( 'timezoneregion-australia' ); - $tzRegions['Europe'] = wfMsg( 'timezoneregion-europe' ); - $tzRegions['Indian'] = wfMsg( 'timezoneregion-indian' ); - $tzRegions['Pacific'] = wfMsg( 'timezoneregion-pacific' ); - asort( $tzRegions ); - - $selZone = explode( '|', $this->mTimeZone, 3 ); - $selZone = ( $selZone[0] == 'ZoneInfo' ) ? $selZone[2] : null; - $now = date_create( 'now' ); - $optgroup = ''; - - foreach ( $tzs as $tz ) { - $z = explode( '/', $tz, 2 ); - - # timezone_identifiers_list() returns a number of - # backwards-compatibility entries. This filters them out of the - # list presented to the user. - if ( count( $z ) != 2 || !array_key_exists( $z[0], $tzRegions ) ) - continue; - - # Localize region - $z[0] = $tzRegions[$z[0]]; - - # Create region groups - if ( $optgroup != $z[0] ) { - if ( $optgroup !== '' ) { - $opt .= Xml::closeElement( 'optgroup' ); - } - $optgroup = $z[0]; - $opt .= Xml::openElement( 'optgroup', array( 'label' => $z[0] ) ) . "\n"; - } - - $minDiff = floor( timezone_offset_get( timezone_open( $tz ), $now ) / 60 ); - $opt .= Xml::option( str_replace( '_', ' ', $z[0] . '/' . $z[1] ), "ZoneInfo|$minDiff|$tz", $selZone === $tz, array( 'label' => $z[1] ) ) . "\n"; - } - if ( $optgroup !== '' ) $opt .= Xml::closeElement( 'optgroup' ); - } - $opt .= Xml::closeElement( 'select' ); - $wgOut->addHTML( - $this->addRow( - Xml::label( wfMsg( 'timezoneselect' ), 'wpTimeZone' ), - $opt ) - ); - $wgOut->addHTML( - $this->addRow( - Xml::label( wfMsg( 'timezoneoffset' ), 'wpHourDiff' ), - Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( - 'id' => 'wpHourDiff', - 'onfocus' => 'javascript:updateTimezoneSelection(true)', - 'onblur' => 'javascript:updateTimezoneSelection(false)' ) ) ) . - "<tr> - <td></td> - <td class='mw-submit'>" . - Xml::element( 'input', - array( 'type' => 'button', - 'value' => wfMsg( 'guesstimezone' ), - 'onclick' => 'javascript:guessTimezone()', - 'id' => 'guesstimezonebutton', - 'style' => 'display:none;' ) ) . - "</td> - </tr>" . - Xml::closeElement( 'table' ) . - Xml::tags( 'div', array( 'class' => 'prefsectiontip' ), wfMsgExt( 'timezonetext', 'parseinline' ) ). - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'fieldset' ) . "\n\n" - ); - - # Editing - # - global $wgLivePreview; - $wgOut->addHTML( - Xml::fieldset( wfMsg( 'textboxsize' ) ) . - wfMsgHTML( 'prefs-edit-boxsize' ) . ' ' . - Xml::inputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) . ' ' . - Xml::inputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) . - $this->getToggles( array( - 'editsection', - 'editsectiononrightclick', - 'editondblclick', - 'editwidth', - 'showtoolbar', - 'previewonfirst', - 'previewontop', - 'minordefault', - 'externaleditor', - 'externaldiff', - $wgLivePreview ? 'uselivepreview' : false, - 'forceeditsummary', - ) ) - ); - - $wgOut->addHTML( Xml::closeElement( 'fieldset' ) ); - - # Recent changes - global $wgRCMaxAge, $wgUseRCPatrol; - $wgOut->addHTML( - Xml::fieldset( wfMsg( 'prefs-rc' ) ) . - Xml::openElement( 'table' ) . - '<tr> - <td class="mw-label">' . - Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) . - '</td> - <td class="mw-input">' . - Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . ' ' . - wfMsgExt( 'recentchangesdays-max', 'parsemag', - $wgLang->formatNum( ceil( $wgRCMaxAge / ( 3600 * 24 ) ) ) ) . - '</td> - </tr><tr> - <td class="mw-label">' . - Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) . - '</td> - <td class="mw-input">' . - Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) . - '</td> - </tr>' . - Xml::closeElement( 'table' ) . - '<br />' - ); - - $toggles[] = 'hideminor'; - if( $wgUseRCPatrol ) { - $toggles[] = 'hidepatrolled'; - $toggles[] = 'newpageshidepatrolled'; - } - if( $wgRCShowWatchingUsers ) $toggles[] = 'shownumberswatching'; - $toggles[] = 'usenewrc'; - - $wgOut->addHTML( - $this->getToggles( $toggles ) . - Xml::closeElement( 'fieldset' ) - ); - - # Watchlist - $watchlistToggles = array( 'watchlisthideminor', 'watchlisthidebots', 'watchlisthideown', - 'watchlisthideanons', 'watchlisthideliu' ); - if( $wgUseRCPatrol ) $watchlistToggles[] = 'watchlisthidepatrolled'; - - $wgOut->addHTML( - Xml::fieldset( wfMsg( 'prefs-watchlist' ) ) . - Xml::inputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) . ' ' . - wfMsgHTML( 'prefs-watchlist-days-max' ) . - '<br /><br />' . - $this->getToggle( 'extendwatchlist' ) . - Xml::inputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) . ' ' . - wfMsgHTML( 'prefs-watchlist-edits-max' ) . - '<br /><br />' . - $this->getToggles( $watchlistToggles ) - ); - - if( $wgUser->isAllowed( 'createpage' ) || $wgUser->isAllowed( 'createtalk' ) ) { - $wgOut->addHTML( $this->getToggle( 'watchcreations' ) ); - } - - foreach( array( 'edit' => 'watchdefault', 'move' => 'watchmoves', 'delete' => 'watchdeletion' ) as $action => $toggle ) { - if( $wgUser->isAllowed( $action ) ) - $wgOut->addHTML( $this->getToggle( $toggle ) ); - } - $this->mUsedToggles['watchcreations'] = true; - $this->mUsedToggles['watchdefault'] = true; - $this->mUsedToggles['watchmoves'] = true; - $this->mUsedToggles['watchdeletion'] = true; - - $wgOut->addHTML( Xml::closeElement( 'fieldset' ) ); - - # Search - $mwsuggest = $wgEnableMWSuggest ? - $this->addRow( - Xml::label( wfMsg( 'mwsuggest-disable' ), 'wpDisableMWSuggest' ), - Xml::check( 'wpDisableMWSuggest', $this->mDisableMWSuggest, array( 'id' => 'wpDisableMWSuggest' ) ) - ) : ''; - $wgOut->addHTML( - // Elements for the search tab itself - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'searchresultshead' ) ) . - // Elements for the search options in the search tab - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'prefs-searchoptions' ) ) . - Xml::openElement( 'table' ) . - $this->addRow( - Xml::label( wfMsg( 'resultsperpage' ), 'wpSearch' ), - Xml::input( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) ) - ) . - $this->addRow( - Xml::label( wfMsg( 'contextlines' ), 'wpSearchLines' ), - Xml::input( 'wpSearchLines', 4, $this->mSearchLines, array( 'id' => 'wpSearchLines' ) ) - ) . - $this->addRow( - Xml::label( wfMsg( 'contextchars' ), 'wpSearchChars' ), - Xml::input( 'wpSearchChars', 4, $this->mSearchChars, array( 'id' => 'wpSearchChars' ) ) - ) . - $mwsuggest . - Xml::closeElement( 'table' ) . - Xml::closeElement( 'fieldset' ) . - // Elements for the namespace options in the search tab - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'prefs-namespaces' ) ) . - wfMsgExt( 'defaultns', array( 'parse' ) ) . - $ps . - Xml::closeElement( 'fieldset' ) . - // End of the search tab - Xml::closeElement( 'fieldset' ) - ); - - # Misc - # - $uopt = $wgUser->getOption( 'underline' ); - $wgOut->addHTML( - Xml::fieldset( wfMsg( 'prefs-misc' ) ) . - Xml::openElement( 'table' ) . - '<tr> - <td class="mw-label">' . - // Xml::label() cannot be used because 'stub-threshold' contains plain HTML - Xml::tags( 'label', array( 'for' => 'wpStubs' ), wfMsg( 'stub-threshold' ) ) . - '</td> - <td class="mw-input">' . - Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) . - '</td> - </tr><tr> - <td class="mw-label">' . - Xml::label( wfMsg( 'tog-underline' ), 'wpOpunderline' ) . - '</td> - <td class="mw-input">' . - Xml::openElement( 'select', array( 'id' => 'wpOpunderline', 'name' => 'wpOpunderline' ) ) . - Xml::option( wfMsg ( 'underline-never' ), '0', $uopt == 0 ) . - Xml::option( wfMsg ( 'underline-always' ), '1', $uopt == 1 ) . - Xml::option( wfMsg ( 'underline-default' ), '2', $uopt == 2 ) . - Xml::closeElement( 'select' ) . - '</td> - </tr>' . - Xml::closeElement( 'table' ) - ); - - # And now the rest = Misc. - foreach ( $togs as $tname ) { - if( !array_key_exists( $tname, $this->mUsedToggles ) ) { - if( $tname == 'norollbackdiff' && $wgUser->isAllowed( 'rollback' ) ) - $wgOut->addHTML( $this->getToggle( $tname ) ); - else - $wgOut->addHTML( $this->getToggle( $tname ) ); - } - } - - $wgOut->addHTML( '</fieldset>' ); - - wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) ); - $token = htmlspecialchars( $wgUser->editToken() ); - $skin = $wgUser->getSkin(); - $rtl = $wgContLang->isRTL() ? 'left' : 'right'; - $wgOut->addHTML( " - <table id='prefsubmit' cellpadding='0' width='100%' style='background:none;'><tr> - <td><input type='submit' name='wpSaveprefs' class='btnSavePrefs' value=\"" . wfMsgHtml( 'saveprefs' ) . - '"'.$skin->tooltipAndAccesskey('save')." /> - <input type='submit' name='wpReset' value=\"" . wfMsgHtml( 'resetprefs' ) . "\" /></td> - <td align='$rtl'><input type='submit' name='wpRestore' value=\"" . wfMsgHtml( 'restoreprefs' ) . "\" /></td> - </tr></table> + $url = SpecialPage::getTitleFor( 'Preferences' )->getFullURL( 'success' ); - <input type='hidden' name='wpEditToken' value=\"{$token}\" /> - </div></form>\n" ); + $wgOut->redirect( $url ); - $wgOut->addHTML( Xml::tags( 'div', array( 'class' => "prefcache" ), - wfMsgExt( 'clearyourcache', 'parseinline' ) ) - ); + return true; } } diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php index 680fe343..8b5f0c93 100644 --- a/includes/specials/SpecialPrefixindex.php +++ b/includes/specials/SpecialPrefixindex.php @@ -63,7 +63,7 @@ class SpecialPrefixindex extends SpecialAllpages { Xml::label( wfMsg( 'allpagesprefix' ), 'nsfrom' ) . "</td> <td class='mw-input'>" . - Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . + Xml::input( 'prefix', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . "</td> </tr> <tr> @@ -115,7 +115,7 @@ class SpecialPrefixindex extends SpecialAllpages { array( 'page_namespace', 'page_title', 'page_is_redirect' ), array( 'page_namespace' => $namespace, - 'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'', + 'page_title' . $dbr->buildLike( $prefixKey, $dbr->anyString() ), 'page_title >= ' . $dbr->addQuotes( $fromKey ), ), __METHOD__, @@ -136,7 +136,10 @@ class SpecialPrefixindex extends SpecialAllpages { $t = Title::makeTitle( $s->page_namespace, $s->page_title ); if( $t ) { $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) . - $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) . + $sk->linkKnown( + $t, + htmlspecialchars( $t->getText() ) + ) . ($s->page_is_redirect ? '</div>' : '' ); } else { $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; @@ -170,17 +173,26 @@ class SpecialPrefixindex extends SpecialAllpages { $nsForm . '</td> <td id="mw-prefixindex-nav-form">' . - $sk->makeKnownLinkObj( $self, wfMsg ( 'allpages' ) ); + $sk->linkKnown( $self, wfMsgHtml( 'allpages' ) ); if( isset( $res ) && $res && ( $n == $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) { - $namespaceparam = $namespace ? "&namespace=$namespace" : ""; + $query = array( + 'from' => $s->page_title, + 'prefix' => $prefix + ); + + if( $namespace ) { + $query['namespace'] = $namespace; + } + $out2 = $wgLang->pipeList( array( $out2, - $sk->makeKnownLinkObj( + $sk->linkKnown( $self, wfMsgHtml( 'nextpage', str_replace( '_',' ', htmlspecialchars( $s->page_title ) ) ), - "from=" . wfUrlEncode( $s->page_title ) . - "&prefix=" . wfUrlEncode( $prefix ) . $namespaceparam ) + array(), + $query + ) ) ); } $out2 .= "</td></tr>" . diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php index a38a8cd1..8229770c 100644 --- a/includes/specials/SpecialProtectedpages.php +++ b/includes/specials/SpecialProtectedpages.php @@ -16,7 +16,7 @@ class ProtectedPagesForm { public function showList( $msg = '' ) { global $wgOut, $wgRequest; - if( "" != $msg ) { + if( $msg != "" ) { $wgOut->setSubtitle( $msg ); } @@ -65,7 +65,7 @@ class ProtectedPagesForm { $skin = $wgUser->getSkin(); $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); - $link = $skin->makeLinkObj( $title ); + $link = $skin->link( $title ); $description_items = array (); @@ -86,7 +86,7 @@ class ProtectedPagesForm { $expiry_description = wfMsg( 'protect-expiring' , $wgLang->timeanddate( $expiry ) , $wgLang->date( $expiry ) , $wgLang->time( $expiry ) ); - $description_items[] = $expiry_description; + $description_items[] = htmlspecialchars($expiry_description); } if(!is_null($size = $row->page_len)) { @@ -95,17 +95,31 @@ class ProtectedPagesForm { # Show a link to the change protection form for allowed users otherwise a link to the protection log if( $wgUser->isAllowed( 'protect' ) ) { - $changeProtection = ' (' . $skin->makeKnownLinkObj( $title, wfMsgHtml( 'protect_change' ), - 'action=unprotect' ) . ')'; + $changeProtection = ' (' . $skin->linkKnown( + $title, + wfMsgHtml( 'protect_change' ), + array(), + array( 'action' => 'unprotect' ) + ) . ')'; } else { $ltitle = SpecialPage::getTitleFor( 'Log' ); - $changeProtection = ' (' . $skin->makeKnownLinkObj( $ltitle, wfMsgHtml( 'protectlogpage' ), - 'type=protect&page=' . $title->getPrefixedUrl() ) . ')'; + $changeProtection = ' (' . $skin->linkKnown( + $ltitle, + wfMsgHtml( 'protectlogpage' ), + array(), + array( + 'type' => 'protect', + 'page' => $title->getPrefixedText() + ) + ) . ')'; } wfProfileOut( __METHOD__ ); - return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . $changeProtection . "</li>\n"; + return Html::rawElement( + 'li', + array(), + wfSpecialList( $link . $stxt, $wgLang->commaList( $description_items ) ) . $changeProtection ) . "\n"; } /** @@ -120,7 +134,7 @@ class ProtectedPagesForm { */ protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly, $cascadeOnly ) { global $wgScript; - $title = SpecialPage::getTitleFor( 'ProtectedPages' ); + $title = SpecialPage::getTitleFor( 'Protectedpages' ); return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . Xml::openElement( 'fieldset' ) . Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) . @@ -128,10 +142,10 @@ class ProtectedPagesForm { $this->getNamespaceMenu( $namespace ) . " \n" . $this->getTypeMenu( $type ) . " \n" . $this->getLevelMenu( $level ) . " \n" . - "<br/><span style='white-space: nowrap'>" . + "<br /><span style='white-space: nowrap'>" . $this->getExpiryCheck( $indefOnly ) . " \n" . $this->getCascadeCheck( $cascadeOnly ) . " \n" . - "</span><br/><span style='white-space: nowrap'>" . + "</span><br /><span style='white-space: nowrap'>" . $this->getSizeLimit( $sizetype, $size ) . " \n" . "</span>" . " " . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . @@ -185,6 +199,8 @@ class ProtectedPagesForm { } /** + * Creates the input label of the restriction type + * @param $pr_type string Protection type * @return string Formatted HTML */ protected function getTypeMenu( $pr_type ) { @@ -213,6 +229,8 @@ class ProtectedPagesForm { } /** + * Creates the input label of the restriction level + * @param $pr_level string Protection level * @return string Formatted HTML */ protected function getLevelMenu( $pr_level ) { @@ -223,6 +241,7 @@ class ProtectedPagesForm { // First pass to load the log names foreach( $wgRestrictionLevels as $type ) { + // Messages used can be 'restriction-level-sysop' and 'restriction-level-autoconfirmed' if( $type !='' && $type !='*') { $text = wfMsg("restriction-level-$type"); $m[$text] = $type; @@ -235,11 +254,11 @@ class ProtectedPagesForm { $options[] = Xml::option( $text, $type, $selected ); } - return - Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . ' ' . + return "<span style='white-space: nowrap'>" . + Xml::label( wfMsg( 'restriction-level' ) , $this->IdLevel ) . ' ' . Xml::tags( 'select', array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ), - implode( "\n", $options ) ); + implode( "\n", $options ) ) . "</span>"; } } diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php index 7e8126d9..d65b3f79 100644 --- a/includes/specials/SpecialProtectedtitles.php +++ b/includes/specials/SpecialProtectedtitles.php @@ -16,7 +16,7 @@ class ProtectedTitlesForm { function showList( $msg = '' ) { global $wgOut, $wgRequest; - if ( "" != $msg ) { + if ( $msg != "" ) { $wgOut->setSubtitle( $msg ); } @@ -61,7 +61,7 @@ class ProtectedTitlesForm { $skin = $wgUser->getSkin(); $title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title ); - $link = $skin->makeLinkObj( $title ); + $link = $skin->link( $title ); $description_items = array (); @@ -94,7 +94,7 @@ class ProtectedTitlesForm { function showOptions( $namespace, $type='edit', $level, $sizetype, $size ) { global $wgScript; $action = htmlspecialchars( $wgScript ); - $title = SpecialPage::getTitleFor( 'ProtectedTitles' ); + $title = SpecialPage::getTitleFor( 'Protectedtitles' ); $special = htmlspecialchars( $title->getPrefixedDBkey() ); return "<form action=\"$action\" method=\"get\">\n" . '<fieldset>' . diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php index 31199b23..fd3f17f2 100644 --- a/includes/specials/SpecialRandompage.php +++ b/includes/specials/SpecialRandompage.php @@ -9,12 +9,12 @@ */ class RandomPage extends SpecialPage { private $namespaces; // namespaces to select pages from + protected $isRedir = false; // should the result be a redirect? + protected $extra = array(); // Extra SQL statements - function __construct( $name = 'Randompage' ){ + public function __construct( $name = 'Randompage' ){ global $wgContentNamespaces; - $this->namespaces = $wgContentNamespaces; - parent::__construct( $name ); } @@ -28,22 +28,23 @@ class RandomPage extends SpecialPage { } // select redirects instead of normal pages? - // Overriden by SpecialRandomredirect public function isRedirect(){ - return false; + return $this->isRedir; } public function execute( $par ) { global $wgOut, $wgContLang; - if ($par) + if ($par) { $this->setNamespace( $wgContLang->getNsIndex( $par ) ); + } $title = $this->getRandomTitle(); if( is_null( $title ) ) { $this->setHeaders(); - $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages', $wgContLang->getNsText( $this->namespace ) ); + $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages', + $this->getNsList(), count( $this->namespaces ) ); return; } @@ -51,6 +52,23 @@ class RandomPage extends SpecialPage { $wgOut->redirect( $title->getFullUrl( $query ) ); } + /** + * Get a comma-delimited list of namespaces we don't have + * any pages in + * @return String + */ + private function getNsList() { + global $wgContLang; + $nsNames = array(); + foreach( $this->namespaces as $n ) { + if( $n === NS_MAIN ) + $nsNames[] = wfMsgForContent( 'blanknamespace' ); + else + $nsNames[] = $wgContLang->getNsText( $n ); + } + return $wgContLang->commaList( $nsNames ); + } + /** * Choose a random title. @@ -58,6 +76,10 @@ class RandomPage extends SpecialPage { */ public function getRandomTitle() { $randstr = wfRandom(); + $title = null; + if ( !wfRunHooks( 'SpecialRandomGetRandomTitle', array( &$randstr, &$this->isRedir, &$this->namespaces, &$this->extra, &$title ) ) ) { + return $title; + } $row = $this->selectRandomPageFromDB( $randstr ); /* If we picked a value that was higher than any in @@ -78,8 +100,6 @@ class RandomPage extends SpecialPage { private function selectRandomPageFromDB( $randstr ) { global $wgExtraRandompageSQL; - $fname = 'RandomPage::selectRandomPageFromDB'; - $dbr = wfGetDB( DB_SLAVE ); $use_index = $dbr->useIndexClause( 'page_random' ); @@ -87,8 +107,17 @@ class RandomPage extends SpecialPage { $ns = implode( ",", $this->namespaces ); $redirect = $this->isRedirect() ? 1 : 0; - - $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : ""; + + if ( $wgExtraRandompageSQL ) { + $this->extra[] = $wgExtraRandompageSQL; + } + if ( $this->addExtraSQL() ) { + $this->extra[] = $this->addExtraSQL(); + } + $extra = ''; + if ( $this->extra ) { + $extra = 'AND (' . implode( ') AND (', $this->extra ) . ')'; + } $sql = "SELECT page_title, page_namespace FROM $page $use_index WHERE page_namespace IN ( $ns ) @@ -98,7 +127,15 @@ class RandomPage extends SpecialPage { ORDER BY page_random"; $sql = $dbr->limitResult( $sql, 1, 0 ); - $res = $dbr->query( $sql, $fname ); + $res = $dbr->query( $sql, __METHOD__ ); return $dbr->fetchObject( $res ); } + + /* an alternative to $wgExtraRandompageSQL so subclasses + * can add their own SQL by overriding this function + * @deprecated, append to $this->extra instead + */ + public function addExtraSQL() { + return ''; + } } diff --git a/includes/specials/SpecialRandomredirect.php b/includes/specials/SpecialRandomredirect.php index 629d5b3c..28cb2aae 100644 --- a/includes/specials/SpecialRandomredirect.php +++ b/includes/specials/SpecialRandomredirect.php @@ -10,10 +10,7 @@ class SpecialRandomredirect extends RandomPage { function __construct(){ parent::__construct( 'Randomredirect' ); + $this->isRedir = true; } - // Override parent::isRedirect() - public function isRedirect(){ - return true; - } } diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index 91c0ecbe..283eeaf4 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -5,6 +5,8 @@ * @ingroup SpecialPage */ class SpecialRecentChanges extends SpecialPage { + var $rcOptions, $rcSubpage; + public function __construct() { parent::__construct( 'Recentchanges' ); $this->includable( true ); @@ -40,7 +42,7 @@ class SpecialRecentChanges extends SpecialPage { } /** - * Get a FormOptions object with options as specified by the user + * Create a FormOptions object with options as specified by the user * * @return FormOptions */ @@ -55,31 +57,45 @@ class SpecialRecentChanges extends SpecialPage { $this->parseParameters( $parameters, $opts ); } - $opts->validateIntBounds( 'limit', 0, 500 ); + $opts->validateIntBounds( 'limit', 0, 5000 ); return $opts; } /** - * Get a FormOptions object sepcific for feed requests + * Create a FormOptions object specific for feed requests and return it * * @return FormOptions */ public function feedSetup() { global $wgFeedLimit, $wgRequest; $opts = $this->getDefaultOptions(); - # Feed is cached on limit,hideminor; other params would randomly not work - $opts->fetchValuesFromRequest( $wgRequest, array( 'limit', 'hideminor' ) ); + # Feed is cached on limit,hideminor,namespace; other params would randomly not work + $opts->fetchValuesFromRequest( $wgRequest, array( 'limit', 'hideminor', 'namespace' ) ); $opts->validateIntBounds( 'limit', 0, $wgFeedLimit ); return $opts; } /** + * Get the current FormOptions for this request + */ + public function getOptions() { + if ( $this->rcOptions === null ) { + global $wgRequest; + $feedFormat = $wgRequest->getVal( 'feed' ); + $this->rcOptions = $feedFormat ? $this->feedSetup() : $this->setup( $this->rcSubpage ); + } + return $this->rcOptions; + } + + + /** * Main execution point * - * @param $parameters string + * @param $subpage string */ - public function execute( $parameters ) { + public function execute( $subpage ) { global $wgRequest, $wgOut; + $this->rcSubpage = $subpage; $feedFormat = $wgRequest->getVal( 'feed' ); # 10 seconds server-side caching max @@ -90,12 +106,11 @@ class SpecialRecentChanges extends SpecialPage { return; } - $opts = $feedFormat ? $this->feedSetup() : $this->setup( $parameters ); + $opts = $this->getOptions(); $this->setHeaders(); $this->outputHeader(); // Fetch results, prepare a batch link existence check query - $rows = array(); $conds = $this->buildMainQueryConds( $opts ); $rows = $this->doMainQuery( $conds, $opts ); if( $rows === false ){ @@ -114,10 +129,9 @@ class SpecialRecentChanges extends SpecialPage { } $batch->execute(); } - $target = isset($opts['target']) ? $opts['target'] : ''; // RCL has targets if( $feedFormat ) { - list( $feed, $feedObj ) = $this->getFeedObject( $feedFormat ); - $feed->execute( $feedObj, $rows, $opts['limit'], $opts['hideminor'], $lastmod, $target ); + list( $changesFeed, $formatter ) = $this->getFeedObject( $feedFormat ); + $changesFeed->execute( $formatter, $rows, $lastmod, $opts ); } else { $this->webOutput( $rows, $opts ); } @@ -131,12 +145,12 @@ class SpecialRecentChanges extends SpecialPage { * @return array */ public function getFeedObject( $feedFormat ){ - $feed = new ChangesFeed( $feedFormat, 'rcfeed' ); - $feedObj = $feed->getFeedObject( + $changesFeed = new ChangesFeed( $feedFormat, 'rcfeed' ); + $formatter = $changesFeed->getFeedObject( wfMsgForContent( 'recentchanges' ), wfMsgForContent( 'recentchanges-feed-description' ) ); - return array( $feed, $feedObj ); + return array( $changesFeed, $formatter ); } /** @@ -177,7 +191,7 @@ class SpecialRecentChanges extends SpecialPage { public function checkLastModified( $feedFormat ) { global $wgUseRCPatrol, $wgOut; $dbr = wfGetDB( DB_SLAVE ); - $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __FUNCTION__ ); + $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ ); if( $feedFormat || !$wgUseRCPatrol ) { if( $lastmod && $wgOut->checkLastModified( $lastmod ) ) { # Client cache fresh and headers sent, nothing more to do. @@ -278,8 +292,6 @@ class SpecialRecentChanges extends SpecialPage { $namespace = $opts['namespace']; $invert = $opts['invert']; - $join_conds = array(); - // JOIN on watchlist for users if( $uid ) { $tables[] = 'watchlist'; @@ -293,20 +305,23 @@ class SpecialRecentChanges extends SpecialPage { // Tag stuff. $fields = array(); // Fields are * in this case, so let the function modify an empty array to keep it happy. - ChangeTags::modifyDisplayQuery( $tables, - $fields, - $conds, - $join_conds, - $query_options, - $opts['tagfilter'] - ); - - wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) ); - - // Is there either one namespace selected or excluded? - // Tag filtering also has a better index. - // Also, if this is "all" or main namespace, just use timestamp index. - if( is_null($namespace) || $invert || $namespace == NS_MAIN || $opts['tagfilter'] ) { + ChangeTags::modifyDisplayQuery( + $tables, $fields, $conds, $join_conds, $query_options, $opts['tagfilter'] + ); + + if ( !wfRunHooks( 'SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts, &$query_options ) ) ) + return false; + + // Don't use the new_namespace_time timestamp index if: + // (a) "All namespaces" selected + // (b) We want all pages NOT in a certain namespaces (inverted) + // (c) There is a tag to filter on (use tag index instead) + // (d) UNION + sort/limit is not an option for the DBMS + if( is_null($namespace) + || $invert + || $opts['tagfilter'] != '' + || !$dbr->unionSupportsOrderAndLimit() ) + { $res = $dbr->select( $tables, '*', $conds, __METHOD__, array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ) + $query_options, @@ -318,17 +333,18 @@ class SpecialRecentChanges extends SpecialPage { array( 'rc_new' => 1 ) + $conds, __METHOD__, array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit, - 'USE INDEX' => array('recentchanges' => 'new_name_timestamp') ), + 'USE INDEX' => array('recentchanges' => 'rc_timestamp') ), $join_conds ); // Old pages $sqlOld = $dbr->selectSQLText( $tables, '*', array( 'rc_new' => 0 ) + $conds, __METHOD__, array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit, - 'USE INDEX' => array('recentchanges' => 'new_name_timestamp') ), + 'USE INDEX' => array('recentchanges' => 'rc_timestamp') ), $join_conds ); # Join the two fast queries, and sort the result set - $sql = "($sqlNew) UNION ($sqlOld) ORDER BY rc_timestamp DESC LIMIT $limit"; + $sql = $dbr->unionQueries(array($sqlNew, $sqlOld), false).' ORDER BY rc_timestamp DESC'; + $sql = $dbr->limitResult($sql, $limit, false); $res = $dbr->query( $sql, __METHOD__ ); } @@ -353,7 +369,7 @@ class SpecialRecentChanges extends SpecialPage { } // And now for the content - $wgOut->setSyndicated( true ); + $wgOut->setFeedAppendQuery( $this->getFeedQuery() ); if( $wgAllowCategorizedRecentChanges ) { $this->filterByCategories( $rows, $opts ); @@ -401,6 +417,14 @@ class SpecialRecentChanges extends SpecialPage { } /** + * Get the query string to append to feed link URLs. + * This is overridden by RCL to add the target parameter + */ + public function getFeedQuery() { + return false; + } + + /** * Return the text to be displayed above the changes * * @param $opts FormOptions @@ -413,7 +437,7 @@ class SpecialRecentChanges extends SpecialPage { $defaults = $opts->getAllValues(); $nondefaults = $opts->getChangedValues(); - $opts->consumeValues( array( 'namespace', 'invert' ) ); + $opts->consumeValues( array( 'namespace', 'invert', 'tagfilter' ) ); $panel = array(); $panel[] = $this->optionsPanel( $defaults, $nondefaults ); @@ -456,6 +480,8 @@ class SpecialRecentChanges extends SpecialPage { Xml::fieldset( wfMsg( 'recentchanges-legend' ), $panelString, array( 'class' => 'rcoptions' ) ) ); + $wgOut->addHTML( ChangesList::flagLegend() ); + $this->setBottomText( $wgOut, $opts ); } @@ -597,8 +623,12 @@ class SpecialRecentChanges extends SpecialPage { global $wgUser; $sk = $wgUser->getSkin(); $params = $override + $options; - return $sk->link( $this->getTitle(), htmlspecialchars( $title ), - ( $active ? array( 'style'=>'font-weight: bold;' ) : array() ), $params, array( 'known' ) ); + if ( $active ) { + return $sk->link( $this->getTitle(), '<strong>' . htmlspecialchars( $title ) . '</strong>', + array(), $params, array( 'known' ) ); + } else { + return $sk->link( $this->getTitle(), htmlspecialchars( $title ), array() , $params, array( 'known' ) ); + } } /** @@ -618,7 +648,9 @@ class SpecialRecentChanges extends SpecialPage { if( $options['from'] ) { $note .= wfMsgExt( 'rcnotefrom', array( 'parseinline' ), $wgLang->formatNum( $options['limit'] ), - $wgLang->timeanddate( $options['from'], true ) ) . '<br />'; + $wgLang->timeanddate( $options['from'], true ), + $wgLang->date( $options['from'], true ), + $wgLang->time( $options['from'], true ) ) . '<br />'; } # Sort data for display and make sure it's unique after we've added user data. diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php index c58ffff0..3b549843 100644 --- a/includes/specials/SpecialRecentchangeslinked.php +++ b/includes/specials/SpecialRecentchangeslinked.php @@ -5,6 +5,7 @@ * @ingroup SpecialPage */ class SpecialRecentchangeslinked extends SpecialRecentchanges { + var $rclTargetTitle; function __construct(){ SpecialPage::SpecialPage( 'Recentchangeslinked' ); @@ -26,7 +27,6 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { public function feedSetup() { global $wgRequest; $opts = parent::feedSetup(); - # Feed is cached on limit,hideminor,target; other params would randomly not work $opts['target'] = $wgRequest->getVal( 'target' ); return $opts; } @@ -34,8 +34,8 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { public function getFeedObject( $feedFormat ){ $feed = new ChangesFeed( $feedFormat, false ); $feedObj = $feed->getFeedObject( - wfMsgForContent( 'recentchangeslinked-title', $this->mTargetTitle->getPrefixedText() ), - wfMsgForContent( 'recentchangeslinked' ) + wfMsgForContent( 'recentchangeslinked-title', $this->getTargetTitle()->getPrefixedText() ), + wfMsgForContent( 'recentchangeslinked-feed' ) ); return array( $feed, $feedObj ); } @@ -52,10 +52,9 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { } $title = Title::newFromURL( $target ); if( !$title || $title->getInterwiki() != '' ){ - $wgOut->wrapWikiMsg( '<div class="errorbox">$1</div><br clear="both" />', 'allpagesbadtitle' ); + $wgOut->wrapWikiMsg( "<div class=\"errorbox\">\n$1</div><br style=\"clear: both\" />", 'allpagesbadtitle' ); return false; } - $this->mTargetTitle = $title; $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $title->getPrefixedText() ) ); @@ -84,6 +83,11 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { $select[] = 'wl_user'; $join_conds['watchlist'] = array( 'LEFT JOIN', "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace" ); } + if ( $wgUser->isAllowed( 'rollback' ) ) { + $tables[] = 'page'; + $join_conds['page'] = array('LEFT JOIN', 'rc_cur_id=page_id'); + $select[] = 'page_latest'; + } ChangeTags::modifyDisplayQuery( $tables, $select, $conds, $join_conds, $query_options, $opts['tagfilter'] ); @@ -139,25 +143,37 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { } } - $subsql[] = $dbr->selectSQLText( + if( $dbr->unionSupportsOrderAndLimit()) + $order = array( 'ORDER BY' => 'rc_timestamp DESC' ); + else + $order = array(); + + + $query = $dbr->selectSQLText( array_merge( $tables, array( $link_table ) ), $select, $conds + $subconds, __METHOD__, - array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ) + $query_options, + $order + $query_options, $join_conds + array( $link_table => array( 'INNER JOIN', $subjoin ) ) ); + + if( $dbr->unionSupportsOrderAndLimit()) + $query = $dbr->limitResult( $query, $limit ); + + $subsql[] = $query; } if( count($subsql) == 0 ) return false; // should never happen - if( count($subsql) == 1 ) + if( count($subsql) == 1 && $dbr->unionSupportsOrderAndLimit() ) $sql = $subsql[0]; else { // need to resort and relimit after union - $sql = "(" . implode( ") UNION (", $subsql ) . ") ORDER BY rc_timestamp DESC LIMIT {$limit}"; + $sql = $dbr->unionQueries($subsql, false).' ORDER BY rc_timestamp DESC'; + $sql = $dbr->limitResult($sql, $limit, false); } - + $res = $dbr->query( $sql, __METHOD__ ); if( $res->numRows() == 0 ) @@ -167,10 +183,10 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { } function getExtraOptions( $opts ){ - $opts->consumeValues( array( 'showlinkedto', 'target' ) ); + $opts->consumeValues( array( 'showlinkedto', 'target', 'tagfilter' ) ); $extraOpts = array(); $extraOpts['namespace'] = $this->namespaceFilterForm( $opts ); - $extraOpts['target'] = array( wfMsg( 'recentchangeslinked-page' ), + $extraOpts['target'] = array( wfMsgHtml( 'recentchangeslinked-page' ), Xml::input( 'target', 40, str_replace('_',' ',$opts['target']) ) . Xml::check( 'showlinkedto', $opts['showlinkedto'], array('id' => 'showlinkedto') ) . ' ' . Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) ); @@ -180,19 +196,37 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { return $extraOpts; } + function getTargetTitle() { + if ( $this->rclTargetTitle === null ) { + $opts = $this->getOptions(); + if ( isset( $opts['target'] ) && $opts['target'] !== '' ) { + $this->rclTargetTitle = Title::newFromText( $opts['target'] ); + } else { + $this->rclTargetTitle = false; + } + } + return $this->rclTargetTitle; + } + function setTopText( OutputPage $out, FormOptions $opts ) { global $wgUser; $skin = $wgUser->getSkin(); - if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) ) - $out->setSubtitle( wfMsg( 'recentchangeslinked-backlink', $skin->link( $this->mTargetTitle, - $this->mTargetTitle->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) ); + $target = $this->getTargetTitle(); + if( $target ) + $out->setSubtitle( wfMsg( 'recentchangeslinked-backlink', $skin->link( $target, + $target->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) ); } - function setBottomText( OutputPage $out, FormOptions $opts ){ - if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) ){ - global $wgUser; - $out->setFeedAppendQuery( "target=" . urlencode( $this->mTargetTitle->getPrefixedDBkey() ) ); + public function getFeedQuery() { + $target = $this->getTargetTitle(); + if( $target ) { + return "target=" . urlencode( $target->getPrefixedDBkey() ); + } else { + return false; } + } + + function setBottomText( OutputPage $out, FormOptions $opts ) { if( isset( $this->mResultEmpty ) && $this->mResultEmpty ){ $out->addWikiMsg( 'recentchangeslinked-noresult' ); } diff --git a/includes/specials/SpecialRemoveRestrictions.php b/includes/specials/SpecialRemoveRestrictions.php index ded6cbe3..a3428a5a 100644 --- a/includes/specials/SpecialRemoveRestrictions.php +++ b/includes/specials/SpecialRemoveRestrictions.php @@ -1,9 +1,9 @@ <?php function wfSpecialRemoveRestrictions() { - global $wgOut, $wgRequest, $wgUser, $wgLang, $wgTitle; + global $wgOut, $wgRequest, $wgUser, $wgLang; $sk = $wgUser->getSkin(); - + $title = SpecialPage::getTitleFor( 'RemoveRestrictions' ); $id = $wgRequest->getVal( 'id' ); if( !is_numeric( $id ) ) { $wgOut->addWikiMsg( 'removerestrictions-noid' ); @@ -36,17 +36,17 @@ function wfSpecialRemoveRestrictions() { if( $result ) $wgOut->addHTML( '<strong class="success">' . wfMsgExt( 'removerestrictions-success', 'parseinline', $r->getSubjectText() ) . '</strong>' ); - $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl( array( 'id' => $id ) ), + $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $title->getLocalUrl( array( 'id' => $id ) ), 'method' => 'post' ) ) ); $wgOut->addHTML( Xml::buildForm( $form, 'removerestrictions-submit' ) ); $wgOut->addHTML( Xml::hidden( 'id', $r->getId() ) ); - $wgOut->addHTML( Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) ); + $wgOut->addHTML( Xml::hidden( 'title', $title->getPrefixedDbKey() ) ); $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) ); $wgOut->addHTML( "</form></fieldset>" ); } function wfSpecialRemoveRestrictionsProcess( $r ) { - global $wgUser, $wgRequest; + global $wgRequest; $reason = $wgRequest->getVal( 'reason' ); $result = $r->delete(); $log = new LogPage( 'restrict' ); diff --git a/includes/specials/SpecialResetpass.php b/includes/specials/SpecialResetpass.php index 059f8dbd..967d2119 100644 --- a/includes/specials/SpecialResetpass.php +++ b/includes/specials/SpecialResetpass.php @@ -37,6 +37,11 @@ class SpecialResetpass extends SpecialPage { return; } + if( $wgRequest->wasPosted() && $wgRequest->getBool( 'wpCancel' ) ) { + $this->doReturnTo(); + return; + } + if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal('token') ) ) { try { $this->attemptReset( $this->mNewpass, $this->mRetype ); @@ -54,17 +59,22 @@ class SpecialResetpass extends SpecialPage { $login = new LoginForm( new FauxRequest( $data, true ) ); $login->execute(); } - $titleObj = Title::newFromText( $wgRequest->getVal( 'returnto' ) ); - if ( !$titleObj instanceof Title ) { - $titleObj = Title::newMainPage(); - } - $wgOut->redirect( $titleObj->getFullURL() ); + $this->doReturnTo(); } catch( PasswordError $e ) { $this->error( $e->getMessage() ); } } $this->showForm(); } + + function doReturnTo() { + global $wgRequest, $wgOut; + $titleObj = Title::newFromText( $wgRequest->getVal( 'returnto' ) ); + if ( !$titleObj instanceof Title ) { + $titleObj = Title::newMainPage(); + } + $wgOut->redirect( $titleObj->getFullURL() ); + } function error( $msg ) { global $wgOut; @@ -102,52 +112,60 @@ class SpecialResetpass extends SpecialPage { array( 'method' => 'post', 'action' => $self->getLocalUrl(), - 'id' => 'mw-resetpass-form' ) ) . - Xml::hidden( 'token', $wgUser->editToken() ) . - Xml::hidden( 'wpName', $this->mUserName ) . - Xml::hidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) . - wfMsgExt( 'resetpass_text', array( 'parse' ) ) . - Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) . + 'id' => 'mw-resetpass-form' ) ) . "\n" . + Xml::hidden( 'token', $wgUser->editToken() ) . "\n" . + Xml::hidden( 'wpName', $this->mUserName ) . "\n" . + Xml::hidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) . "\n" . + wfMsgExt( 'resetpass_text', array( 'parse' ) ) . "\n" . + Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) . "\n" . $this->pretty( array( array( 'wpName', 'username', 'text', $this->mUserName ), array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ), - array( 'wpNewPassword', 'newpassword', 'password', '' ), - array( 'wpRetype', 'retypenew', 'password', '' ), - ) ) . + array( 'wpNewPassword', 'newpassword', 'password', null ), + array( 'wpRetype', 'retypenew', 'password', null ), + ) ) . "\n" . $rememberMe . - '<tr>' . - '<td></td>' . + "<tr>\n" . + "<td></td>\n" . '<td class="mw-input">' . Xml::submitButton( wfMsg( $submitMsg ) ) . - '</td>' . - '</tr>' . + Xml::submitButton( wfMsg( 'resetpass-submit-cancel' ), array( 'name' => 'wpCancel' ) ) . + "</td>\n" . + "</tr>\n" . Xml::closeElement( 'table' ) . Xml::closeElement( 'form' ) . - Xml::closeElement( 'fieldset' ) + Xml::closeElement( 'fieldset' ) . "\n" ); } function pretty( $fields ) { $out = ''; - foreach( $fields as $list ) { + foreach ( $fields as $list ) { list( $name, $label, $type, $value ) = $list; if( $type == 'text' ) { $field = htmlspecialchars( $value ); } else { - $field = Xml::input( $name, 20, $value, - array( 'id' => $name, 'type' => $type ) ); + $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 ); } - $out .= '<tr>'; - $out .= "<td class='mw-label'>"; + $out .= "<tr>\n"; + $out .= "\t<td class='mw-label'>"; if ( $type != 'text' ) $out .= Xml::label( wfMsg( $label ), $name ); else - $out .= wfMsg( $label ); - $out .= '</td>'; - $out .= "<td class='mw-input'>"; + $out .= wfMsgHtml( $label ); + $out .= "</td>\n"; + $out .= "\t<td class='mw-input'>"; $out .= $field; - $out .= '</td>'; - $out .= '</tr>'; + $out .= "</td>\n"; + $out .= "</tr>"; } return $out; } diff --git a/includes/specials/SpecialRestrictUser.php b/includes/specials/SpecialRestrictUser.php deleted file mode 100644 index b946cde8..00000000 --- a/includes/specials/SpecialRestrictUser.php +++ /dev/null @@ -1,190 +0,0 @@ -<?php - -function wfSpecialRestrictUser( $par = null ) { - global $wgOut, $wgRequest; - $user = $userOrig = null; - if( $par ) { - $userOrig = $par; - } elseif( $wgRequest->getVal( 'user' ) ) { - $userOrig = $wgRequest->getVal( 'user' ); - } else { - $wgOut->addHTML( RestrictUserForm::selectUserForm() ); - return; - } - $isIP = User::isIP( $userOrig ); - $user = $isIP ? $userOrig : User::getCanonicalName( $userOrig ); - $uid = User::idFromName( $user ); - if( !$uid && !$isIP ) { - $err = '<strong class="error">' . wfMsgHtml( 'restrictuser-notfound' ) . '</strong>'; - $wgOut->addHTML( RestrictUserForm::selectUserForm( $userOrig, $err ) ); - return; - } - $wgOut->addHTML( RestrictUserForm::selectUserForm( $user ) ); - - UserRestriction::purgeExpired(); - $old = UserRestriction::fetchForUser( $user, true ); - - RestrictUserForm::pageRestrictionForm( $uid, $user, $old ); - RestrictUserForm::namespaceRestrictionForm( $uid, $user, $old ); - - // Renew it after possible changes in previous two functions - $old = UserRestriction::fetchForUser( $user, true ); - if( $old ) { - $wgOut->addHTML( RestrictUserForm::existingRestrictions( $old ) ); - } -} - -class RestrictUserForm { - public static function selectUserForm( $val = null, $error = null ) { - global $wgScript, $wgTitle; - $action = htmlspecialchars( $wgScript ); - $s = Xml::fieldset( wfMsg( 'restrictuser-userselect' ) ) . "<form action=\"{$action}\">"; - if( $error ) - $s .= '<p>' . $error . '</p>'; - $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); - $form = array( 'restrictuser-user' => Xml::input( 'user', false, $val ) ); - $s .= Xml::buildForm( $form, 'restrictuser-go' ); - $s .= "</form></fieldset>"; - return $s; - } - - public static function existingRestrictions( $restrictions ) { - //TODO: autoload? - require_once( dirname( __FILE__ ) . '/SpecialListUserRestrictions.php' ); - $s = Xml::fieldset( wfMsg( 'restrictuser-existing' ) ) . '<ul>'; - foreach( $restrictions as $r ) - $s .= UserRestrictionsPager::formatRestriction( $r ); - $s .= "</ul></fieldset>"; - return $s; - } - - public static function pageRestrictionForm( $uid, $user, $oldRestrictions ) { - global $wgOut, $wgTitle, $wgRequest, $wgUser; - $error = ''; - $success = false; - if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::PAGE && - $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) { - - $title = Title::newFromText( $wgRequest->getVal( 'page' ) ); - if( !$title ) { - $error = array( 'restrictuser-badtitle', $wgRequest->getVal( 'page' ) ); - } elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false ) { - $error = array( 'restrictuser-badexpiry', $wgRequest->getVal( 'expiry' ) ); - } else { - foreach( $oldRestrictions as $r ) { - if( $r->isPage() && $r->getPage()->equals( $title ) ) - $error = array( 'restrictuser-duptitle' ); - } - } - if( !$error ) { - self::doPageRestriction( $uid, $user ); - $success = array('restrictuser-success', $user); - } - } - $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::PAGE; - $wgOut->addHTML( Xml::fieldset( wfMsg( 'restrictuser-legend-page' ) ) ); - - self::printSuccessError( $success, $error ); - - $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(), - 'method' => 'post' ) ) ); - $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::PAGE ) ); - $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) ); - $wgOut->addHTML( Xml::hidden( 'user', $user ) ); - $form = array(); - $form['restrictuser-title'] = Xml::input( 'page', false, - $useRequestValues ? $wgRequest->getVal( 'page' ) : false ); - $form['restrictuser-expiry'] = Xml::input( 'expiry', false, - $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false ); - $form['restrictuser-reason'] = Xml::input( 'reason', false, - $useRequestValues ? $wgRequest->getVal( 'reason' ) : false ); - $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-submit' ) ); - $wgOut->addHTML( "</form></fieldset>" ); - } - - public static function printSuccessError( $success, $error ) { - global $wgOut; - if ( $error ) - $wgOut->wrapWikiMsg( '<strong class="error">$1</strong>', $error ); - if ( $success ) - $wgOut->wrapWikiMsg( '<strong class="success">$1</strong>', $success ); - } - - public static function doPageRestriction( $uid, $user ) { - global $wgUser, $wgRequest; - $r = new UserRestriction(); - $r->setType( UserRestriction::PAGE ); - $r->setPage( Title::newFromText( $wgRequest->getVal( 'page' ) ) ); - $r->setSubjectId( $uid ); - $r->setSubjectText( $user ); - $r->setBlockerId( $wgUser->getId() ); - $r->setBlockerText( $wgUser->getName() ); - $r->setReason( $wgRequest->getVal( 'reason' ) ); - $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) ); - $r->setTimestamp( wfTimestampNow( TS_MW ) ); - $r->commit(); - $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity(); - $l = new LogPage( 'restrict' ); - $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(), - array( $r->getType(), $r->getPage()->getFullText(), $logExpiry) ); - } - - public static function namespaceRestrictionForm( $uid, $user, $oldRestrictions ) { - global $wgOut, $wgTitle, $wgRequest, $wgUser, $wgContLang; - $error = ''; - $success = false; - if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE && - $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) { - $ns = $wgRequest->getVal( 'namespace' ); - if( $wgContLang->getNsText( $ns ) === false ) - $error = wfMsgExt( 'restrictuser-badnamespace', 'parseinline' ); - elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false ) - $error = wfMsgExt( 'restrictuser-badexpiry', 'parseinline', $wgRequest->getVal( 'expiry' ) ); - else - foreach( $oldRestrictions as $r ) - if( $r->isNamespace() && $r->getNamespace() == $ns ) - $error = wfMsgExt( 'restrictuser-dupnamespace', 'parse' ); - if( !$error ) { - self::doNamespaceRestriction( $uid, $user ); - $success = array('restrictuser-success', $user); - } - } - $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE; - $wgOut->addHTML( Xml::fieldset( wfMsg( 'restrictuser-legend-namespace' ) ) ); - - self::printSuccessError( $success, $error ); - - $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(), - 'method' => 'post' ) ) ); - $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::NAMESPACE ) ); - $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) ); - $wgOut->addHTML( Xml::hidden( 'user', $user ) ); - $form = array(); - $form['restrictuser-namespace'] = Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ) ); - $form['restrictuser-expiry'] = Xml::input( 'expiry', false, - $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false ); - $form['restrictuser-reason'] = Xml::input( 'reason', false, - $useRequestValues ? $wgRequest->getVal( 'reason' ) : false ); - $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-submit' ) ); - $wgOut->addHTML( "</form></fieldset>" ); - } - - public static function doNamespaceRestriction( $uid, $user ) { - global $wgUser, $wgRequest; - $r = new UserRestriction(); - $r->setType( UserRestriction::NAMESPACE ); - $r->setNamespace( $wgRequest->getVal( 'namespace' ) ); - $r->setSubjectId( $uid ); - $r->setSubjectText( $user ); - $r->setBlockerId( $wgUser->getId() ); - $r->setBlockerText( $wgUser->getName() ); - $r->setReason( $wgRequest->getVal( 'reason' ) ); - $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) ); - $r->setTimestamp( wfTimestampNow( TS_MW ) ); - $r->commit(); - $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity(); - $l = new LogPage( 'restrict' ); - $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(), - array( $r->getType(), $r->getNamespace(), $logExpiry ) ); - } -} diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php index 7fdb3cc4..b2db869c 100644 --- a/includes/specials/SpecialRevisiondelete.php +++ b/includes/specials/SpecialRevisiondelete.php @@ -8,164 +8,287 @@ */ class SpecialRevisionDelete extends UnlistedSpecialPage { + /** Skin object */ + var $skin; + + /** True if the submit button was clicked, and the form was posted */ + var $submitClicked; + + /** Target ID list */ + var $ids; + + /** Archive name, for reviewing deleted files */ + var $archiveName; + + /** Edit token for securing image views against XSS */ + var $token; + + /** Title object for target parameter */ + var $targetObj; + + /** Deletion type, may be revision, archive, oldimage, filearchive, logging. */ + var $typeName; + + /** Array of checkbox specs (message, name, deletion bits) */ + var $checks; + + /** Information about the current type */ + var $typeInfo; + + /** The RevDel_List object, storing the list of items to be deleted/undeleted */ + var $list; + + /** + * Assorted information about each type, needed by the special page. + * TODO Move some of this to the list class + */ + static $allowedTypes = array( + 'revision' => array( + 'check-label' => 'revdelete-hide-text', + 'deletion-bits' => Revision::DELETED_TEXT, + 'success' => 'revdelete-success', + 'failure' => 'revdelete-failure', + 'list-class' => 'RevDel_RevisionList', + ), + 'archive' => array( + 'check-label' => 'revdelete-hide-text', + 'deletion-bits' => Revision::DELETED_TEXT, + 'success' => 'revdelete-success', + 'failure' => 'revdelete-failure', + 'list-class' => 'RevDel_ArchiveList', + ), + 'oldimage'=> array( + 'check-label' => 'revdelete-hide-image', + 'deletion-bits' => File::DELETED_FILE, + 'success' => 'revdelete-success', + 'failure' => 'revdelete-failure', + 'list-class' => 'RevDel_FileList', + ), + 'filearchive' => array( + 'check-label' => 'revdelete-hide-image', + 'deletion-bits' => File::DELETED_FILE, + 'success' => 'revdelete-success', + 'failure' => 'revdelete-failure', + 'list-class' => 'RevDel_ArchivedFileList', + ), + 'logging' => array( + 'check-label' => 'revdelete-hide-name', + 'deletion-bits' => LogPage::DELETED_ACTION, + 'success' => 'logdelete-success', + 'failure' => 'logdelete-failure', + 'list-class' => 'RevDel_LogList', + ), + ); + + /** Type map to support old log entries */ + static $deprecatedTypeMap = array( + 'oldid' => 'revision', + 'artimestamp' => 'archive', + 'oldimage' => 'oldimage', + 'fileid' => 'filearchive', + 'logid' => 'logging', + ); public function __construct() { - parent::__construct( 'Revisiondelete', 'deleterevision' ); - $this->includable( false ); + parent::__construct( 'Revisiondelete', 'deletedhistory' ); } public function execute( $par ) { global $wgOut, $wgUser, $wgRequest; - if( wfReadOnly() ) { - $wgOut->readOnlyPage(); + if( !$wgUser->isAllowed( 'deletedhistory' ) ) { + $wgOut->permissionRequired( 'deletedhistory' ); return; - } - if( !$wgUser->isAllowed( 'deleterevision' ) ) { - $wgOut->permissionRequired( 'deleterevision' ); + } else if( wfReadOnly() ) { + $wgOut->readOnlyPage(); return; } - $this->skin =& $wgUser->getSkin(); - # Set title and such + $this->mIsAllowed = $wgUser->isAllowed('deleterevision'); // for changes + $this->skin = $wgUser->getSkin(); $this->setHeaders(); $this->outputHeader(); - $this->wasPosted = $wgRequest->wasPosted(); - # Handle our many different possible input types - $this->target = $wgRequest->getText( 'target' ); - $this->oldids = $wgRequest->getArray( 'oldid' ); - $this->artimestamps = $wgRequest->getArray( 'artimestamp' ); - $this->logids = $wgRequest->getArray( 'logid' ); - $this->oldimgs = $wgRequest->getArray( 'oldimage' ); - $this->fileids = $wgRequest->getArray( 'fileid' ); + $this->submitClicked = $wgRequest->wasPosted() && $wgRequest->getBool( 'wpSubmit' ); + # Handle our many different possible input types. + $ids = $wgRequest->getVal( 'ids' ); + if ( !is_null( $ids ) ) { + # Allow CSV, for backwards compatibility, or a single ID for show/hide links + $this->ids = explode( ',', $ids ); + } else { + # Array input + $this->ids = array_keys( $wgRequest->getArray('ids',array()) ); + } + // $this->ids = array_map( 'intval', $this->ids ); + $this->ids = array_unique( array_filter( $this->ids ) ); + + if ( $wgRequest->getVal( 'action' ) == 'historysubmit' ) { + # For show/hide form submission from history page + $this->targetObj = $GLOBALS['wgTitle']; + $this->typeName = 'revision'; + } else { + $this->typeName = $wgRequest->getVal( 'type' ); + $this->targetObj = Title::newFromText( $wgRequest->getText( 'target' ) ); + } + # For reviewing deleted files... - $this->file = $wgRequest->getVal( 'file' ); - # Only one target set at a time please! - $i = (bool)$this->file + (bool)$this->oldids + (bool)$this->logids - + (bool)$this->artimestamps + (bool)$this->fileids + (bool)$this->oldimgs; - # No targets? - if( $i == 0 ) { - $wgOut->showErrorPage( 'notargettitle', 'notargettext' ); + $this->archiveName = $wgRequest->getVal( 'file' ); + $this->token = $wgRequest->getVal( 'token' ); + if ( $this->archiveName && $this->targetObj ) { + $this->tryShowFile( $this->archiveName ); return; } - # Too many targets? - if( $i !== 1 ) { - $wgOut->showErrorPage( 'revdelete-toomanytargets-title', 'revdelete-toomanytargets-text' ); + + if ( isset( self::$deprecatedTypeMap[$this->typeName] ) ) { + $this->typeName = self::$deprecatedTypeMap[$this->typeName]; + } + + # No targets? + if( !isset( self::$allowedTypes[$this->typeName] ) || count( $this->ids ) == 0 ) { + $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); return; } - $this->page = Title::newFromUrl( $this->target ); + $this->typeInfo = self::$allowedTypes[$this->typeName]; + # If we have revisions, get the title from the first one # since they should all be from the same page. This allows # for more flexibility with page moves... - if( count($this->oldids) > 0 ) { - $rev = Revision::newFromId( $this->oldids[0] ); - $this->page = $rev ? $rev->getTitle() : $this->page; + if( $this->typeName == 'revision' ) { + $rev = Revision::newFromId( $this->ids[0] ); + $this->targetObj = $rev ? $rev->getTitle() : $this->targetObj; } + + $this->otherReason = $wgRequest->getVal( 'wpReason' ); # We need a target page! - if( is_null($this->page) ) { + if( is_null($this->targetObj) ) { $wgOut->addWikiMsg( 'undelete-header' ); return; } - # Logs must have a type given - if( $this->logids && !strpos($this->page->getDBKey(),'/') ) { - $wgOut->showErrorPage( 'revdelete-nologtype-title', 'revdelete-nologtype-text' ); - return; - } - # For reviewing deleted files...show it now if allowed - if( $this->file ) { - $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->page, $this->file ); - $oimage->load(); - // Check if user is allowed to see this file - if( !$oimage->userCan(File::DELETED_FILE) ) { - $wgOut->permissionRequired( 'suppressrevision' ); - } else { - $this->showFile( $this->file ); - } - return; - } # Give a link to the logs/hist for this page - if( !is_null($this->page) && $this->page->getNamespace() > -1 ) { - $links = array(); + $this->showConvenienceLinks(); - $logtitle = SpecialPage::getTitleFor( 'Log' ); - $links[] = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'viewpagelogs' ), - wfArrayToCGI( array( 'page' => $this->page->getPrefixedUrl() ) ) ); - # Give a link to the page history - $links[] = $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml( 'pagehist' ), - wfArrayToCGI( array( 'action' => 'history' ) ) ); - # Link to deleted edits - if( $wgUser->isAllowed('undelete') ) { - $undelete = SpecialPage::getTitleFor( 'Undelete' ); - $links[] = $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml( 'deletedhist' ), - wfArrayToCGI( array( 'target' => $this->page->getPrefixedDBkey() ) ) ); - } - # Logs themselves don't have histories or archived revisions - $wgOut->setSubtitle( '<p>'.implode($links,' / ').'</p>' ); - } - # Lock the operation and the form context - $this->secureOperation(); - # Either submit or create our form - if( $this->wasPosted ) { - $this->submit( $wgRequest ); - } else if( $this->deleteKey == 'oldid' || $this->deleteKey == 'artimestamp' ) { - $this->showRevs(); - } else if( $this->deleteKey == 'fileid' || $this->deleteKey == 'oldimage' ) { - $this->showImages(); - } else if( $this->deleteKey == 'logid' ) { - $this->showLogItems(); - } - # Show relevant lines from the deletion log. This will show even if said ID - # does not exist...might be helpful - $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); - LogEventsList::showLogExtract( $wgOut, 'delete', $this->page->getPrefixedText() ); - if( $wgUser->isAllowed( 'suppressionlog' ) ){ - $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" ); - LogEventsList::showLogExtract( $wgOut, 'suppress', $this->page->getPrefixedText() ); - } - } - - private function secureOperation() { - global $wgUser; - $this->deleteKey = ''; - // At this point, we should only have one of these - if( $this->oldids ) { - $this->revisions = $this->oldids; - $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT ); - $this->deleteKey = 'oldid'; - } else if( $this->artimestamps ) { - $this->archrevs = $this->artimestamps; - $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT ); - $this->deleteKey = 'artimestamp'; - } else if( $this->oldimgs ) { - $this->ofiles = $this->oldimgs; - $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE ); - $this->deleteKey = 'oldimage'; - } else if( $this->fileids ) { - $this->afiles = $this->fileids; - $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE ); - $this->deleteKey = 'fileid'; - } else if( $this->logids ) { - $this->events = $this->logids; - $hide_content_name = array( 'revdelete-hide-name', 'wpHideName', LogPage::DELETED_ACTION ); - $this->deleteKey = 'logid'; - } - // Our checkbox messages depends one what we are doing, - // e.g. we don't hide "text" for logs or images + # Initialise checkboxes $this->checks = array( - $hide_content_name, + array( $this->typeInfo['check-label'], 'wpHidePrimary', $this->typeInfo['deletion-bits'] ), array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ), array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ) ); if( $wgUser->isAllowed('suppressrevision') ) { - $this->checks[] = array( 'revdelete-hide-restricted', 'wpHideRestricted', Revision::DELETED_RESTRICTED ); + $this->checks[] = array( 'revdelete-hide-restricted', + 'wpHideRestricted', Revision::DELETED_RESTRICTED ); + } + + # Either submit or create our form + if( $this->mIsAllowed && $this->submitClicked ) { + $this->submit( $wgRequest ); + } else { + $this->showForm(); + } + + $qc = $this->getLogQueryCond(); + # Show relevant lines from the deletion log + $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" ); + LogEventsList::showLogExtract( $wgOut, 'delete', + $this->targetObj->getPrefixedText(), '', array( 'lim' => 25, 'conds' => $qc ) ); + # Show relevant lines from the suppression log + if( $wgUser->isAllowed( 'suppressionlog' ) ) { + $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" ); + LogEventsList::showLogExtract( $wgOut, 'suppress', + $this->targetObj->getPrefixedText(), '', array( 'lim' => 25, 'conds' => $qc ) ); } } /** + * Show some useful links in the subtitle + */ + protected function showConvenienceLinks() { + global $wgOut, $wgUser, $wgLang; + # Give a link to the logs/hist for this page + if( $this->targetObj ) { + $links = array(); + $links[] = $this->skin->linkKnown( + SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'viewpagelogs' ), + array(), + array( 'page' => $this->targetObj->getPrefixedText() ) + ); + if ( $this->targetObj->getNamespace() != NS_SPECIAL ) { + # Give a link to the page history + $links[] = $this->skin->linkKnown( + $this->targetObj, + wfMsgHtml( 'pagehist' ), + array(), + array( 'action' => 'history' ) + ); + # Link to deleted edits + if( $wgUser->isAllowed('undelete') ) { + $undelete = SpecialPage::getTitleFor( 'Undelete' ); + $links[] = $this->skin->linkKnown( + $undelete, + wfMsgHtml( 'deletedhist' ), + array(), + array( 'target' => $this->targetObj->getPrefixedDBkey() ) + ); + } + } + # Logs themselves don't have histories or archived revisions + $wgOut->setSubtitle( '<p>' . $wgLang->pipeList( $links ) . '</p>' ); + } + } + + /** + * Get the condition used for fetching log snippets + */ + protected function getLogQueryCond() { + $conds = array(); + // Revision delete logs for these item + $conds['log_type'] = array('delete','suppress'); + $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. */ - private function showFile( $key ) { - global $wgOut, $wgRequest; + protected function tryShowFile( $archiveName ) { + global $wgOut, $wgRequest, $wgUser, $wgLang; + + $repo = RepoGroup::singleton()->getLocalRepo(); + $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName ); + $oimage->load(); + // Check if user is allowed to see this file + if ( !$oimage->exists() ) { + $wgOut->addWikiMsg( 'revdelete-no-file' ); + return; + } + if( !$oimage->userCan(File::DELETED_FILE) ) { + if( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) { + $wgOut->permissionRequired( 'suppressrevision' ); + } else { + $wgOut->permissionRequired( 'deletedtext' ); + } + return; + } + if ( !$wgUser->matchEditToken( $this->token, $archiveName ) ) { + $wgOut->addWikiMsg( 'revdelete-show-file-confirm', + $this->targetObj->getText(), + $wgLang->date( $oimage->getTimestamp() ), + $wgLang->time( $oimage->getTimestamp() ) ); + $wgOut->addHTML( + Xml::openElement( 'form', array( + 'method' => 'POST', + 'action' => $this->getTitle()->getLocalUrl( + 'target=' . urlencode( $oimage->getName() ) . + '&file=' . urlencode( $archiveName ) . + '&token=' . urlencode( $wgUser->editToken( $archiveName ) ) ) + ) + ) . + Xml::submitButton( wfMsg( 'revdelete-show-file-submit' ) ) . + '</form>' + ); + return; + } $wgOut->disable(); - # We mustn't allow the output to be Squid cached, otherwise # if an admin previews a deleted image, and it's cached, then # a user without appropriate permissions can toddle off and @@ -174,103 +297,61 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); $wgRequest->response()->header( 'Pragma: no-cache' ); - $store = FileStore::get( 'deleted' ); - $store->stream( $key ); + # Stream the file to the client + global $IP; + require_once( "$IP/includes/StreamFile.php" ); + $key = $oimage->getStorageKey(); + $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key; + wfStreamFile( $path ); } /** - * This lets a user set restrictions for live and archived revisions + * Get the list object for this request */ - private function showRevs() { - global $wgOut, $wgUser; + protected function getList() { + if ( is_null( $this->list ) ) { + $class = $this->typeInfo['list-class']; + $this->list = new $class( $this, $this->targetObj, $this->ids ); + } + return $this->list; + } + + /** + * Show a list of items that we will operate on, and show a form with checkboxes + * which will allow the user to choose new visibility settings. + */ + protected function showForm() { + global $wgOut, $wgUser, $wgLang; $UserAllowed = true; - $count = ($this->deleteKey=='oldid') ? - count($this->revisions) : count($this->archrevs); - $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count ); + if ( $this->typeName == 'logging' ) { + $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->ids) ) ); + } else { + $wgOut->addWikiMsg( 'revdelete-selected', + $this->targetObj->getPrefixedText(), count( $this->ids ) ); + } - $bitfields = 0; $wgOut->addHTML( "<ul>" ); $where = $revObjs = array(); - $dbr = wfGetDB( DB_MASTER ); - $revisions = 0; + $numRevisions = 0; // Live revisions... - if( $this->deleteKey=='oldid' ) { - // Run through and pull all our data in one query - foreach( $this->revisions as $revid ) { - $where[] = intval($revid); - } - $result = $dbr->select( array('revision','page'), '*', - array( - 'rev_page' => $this->page->getArticleID(), - 'rev_id' => $where, - 'rev_page = page_id' ), - __METHOD__ ); - while( $row = $dbr->fetchObject( $result ) ) { - $revObjs[$row->rev_id] = new Revision( $row ); - } - foreach( $this->revisions as $revid ) { - // Hiding top revisison is bad - if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) { - continue; - } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) { - // If a rev is hidden from sysops - if( !$this->wasPosted ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return; - } - $UserAllowed = false; - } - $revisions++; - $wgOut->addHTML( $this->historyLine( $revObjs[$revid] ) ); - $bitfields |= $revObjs[$revid]->mDeleted; - } - // The archives... - } else { - // Run through and pull all our data in one query - foreach( $this->archrevs as $timestamp ) { - $where[] = $dbr->timestamp( $timestamp ); - } - $result = $dbr->select( 'archive', '*', - array( - 'ar_namespace' => $this->page->getNamespace(), - 'ar_title' => $this->page->getDBKey(), - 'ar_timestamp' => $where ), - __METHOD__ ); - while( $row = $dbr->fetchObject( $result ) ) { - $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp ); - $revObjs[$timestamp] = new Revision( array( - 'page' => $this->page->getArticleId(), - 'id' => $row->ar_rev_id, - 'text' => $row->ar_text_id, - 'comment' => $row->ar_comment, - 'user' => $row->ar_user, - 'user_text' => $row->ar_user_text, - 'timestamp' => $timestamp, - 'minor_edit' => $row->ar_minor_edit, - 'text_id' => $row->ar_text_id, - 'deleted' => $row->ar_deleted, - 'len' => $row->ar_len) ); - } - foreach( $this->archrevs as $timestamp ) { - if( !isset($revObjs[$timestamp]) ) { - continue; - } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) { - // If a rev is hidden from sysops - if( !$this->wasPosted ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return; - } - $UserAllowed = false; + $list = $this->getList(); + for ( $list->reset(); $list->current(); $list->next() ) { + $item = $list->current(); + if ( !$item->canView() ) { + if( !$this->submitClicked ) { + $wgOut->permissionRequired( 'suppressrevision' ); + return; } - $revisions++; - $wgOut->addHTML( $this->historyLine( $revObjs[$timestamp] ) ); - $bitfields |= $revObjs[$timestamp]->mDeleted; + $UserAllowed = false; } + $numRevisions++; + $wgOut->addHTML( $item->getHTML() ); } - if( !$revisions ) { + + if( !$numRevisions ) { $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); return; } @@ -282,1235 +363,1485 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { // Normal sysops can always see what they did, but can't always change it if( !$UserAllowed ) return; - $items = array( - Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ), - Xml::submitButton( wfMsg( 'revdelete-submit' ) ) - ); - $hidden = array( - Xml::hidden( 'wpEditToken', $wgUser->editToken() ), - Xml::hidden( 'target', $this->page->getPrefixedText() ), - Xml::hidden( 'type', $this->deleteKey ) - ); - if( $this->deleteKey=='oldid' ) { - foreach( $revObjs as $rev ) - $hidden[] = Xml::hidden( 'oldid[]', $rev->getId() ); + // Show form if the user can submit + if( $this->mIsAllowed ) { + $out = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) ), + 'id' => 'mw-revdel-form-revisions' ) ) . + Xml::fieldset( wfMsg( 'revdelete-legend' ) ) . + $this->buildCheckBoxes() . + Xml::openElement( 'table' ) . + "<tr>\n" . + '<td class="mw-label">' . + Xml::label( wfMsg( 'revdelete-log' ), 'wpRevDeleteReasonList' ) . + '</td>' . + '<td class="mw-input">' . + Xml::listDropDown( 'wpRevDeleteReasonList', + wfMsgForContent( 'revdelete-reason-dropdown' ), + wfMsgForContent( 'revdelete-reasonotherlist' ), '', 'wpReasonDropDown', 1 + ) . + '</td>' . + "</tr><tr>\n" . + '<td class="mw-label">' . + Xml::label( wfMsg( 'revdelete-otherreason' ), 'wpReason' ) . + '</td>' . + '<td class="mw-input">' . + Xml::input( 'wpReason', 60, $this->otherReason, array( 'id' => 'wpReason' ) ) . + '</td>' . + "</tr><tr>\n" . + '<td></td>' . + '<td class="mw-submit">' . + Xml::submitButton( wfMsgExt('revdelete-submit','parsemag',$numRevisions), + array( 'name' => 'wpSubmit' ) ) . + '</td>' . + "</tr>\n" . + Xml::closeElement( 'table' ) . + Xml::hidden( 'wpEditToken', $wgUser->editToken() ) . + Xml::hidden( 'target', $this->targetObj->getPrefixedText() ) . + Xml::hidden( 'type', $this->typeName ) . + Xml::hidden( 'ids', implode( ',', $this->ids ) ) . + Xml::closeElement( 'fieldset' ) . "\n"; } else { - foreach( $revObjs as $rev ) - $hidden[] = Xml::hidden( 'artimestamp[]', $rev->getTimestamp() ); - } - $special = SpecialPage::getTitleFor( 'Revisiondelete' ); - $wgOut->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), - 'id' => 'mw-revdel-form-revisions' ) ) . - Xml::openElement( 'fieldset' ) . - xml::element( 'legend', null, wfMsg( 'revdelete-legend' ) ) - ); - - $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) ); - foreach( $items as $item ) { - $wgOut->addHTML( Xml::tags( 'p', null, $item ) ); - } - foreach( $hidden as $item ) { - $wgOut->addHTML( $item ); + $out = ''; + } + if( $this->mIsAllowed ) { + $out .= Xml::closeElement( 'form' ) . "\n"; + // Show link to edit the dropdown reasons + if( $wgUser->isAllowed( 'editinterface' ) ) { + $title = Title::makeTitle( NS_MEDIAWIKI, 'revdelete-reason-dropdown' ); + $link = $wgUser->getSkin()->link( + $title, + wfMsgHtml( 'revdelete-edit-reasonlist' ), + array(), + array( 'action' => 'edit' ) + ); + $out .= Xml::tags( 'p', array( 'class' => 'mw-revdel-editreasons' ), $link ) . "\n"; + } } - $wgOut->addHTML( - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ) . "\n" - ); + $wgOut->addHTML( $out ); } /** - * This lets a user set restrictions for archived images + * Show some introductory text + * FIXME Wikimedia-specific policy text */ - private function showImages() { - global $wgOut, $wgUser, $wgLang; - $UserAllowed = true; - - $count = ($this->deleteKey=='oldimage') ? count($this->ofiles) : count($this->afiles); - $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), - $wgLang->formatNum($count) ); - - $bitfields = 0; - $wgOut->addHTML( "<ul>" ); - - $where = $filesObjs = array(); - $dbr = wfGetDB( DB_MASTER ); - // Live old revisions... - $revisions = 0; - if( $this->deleteKey=='oldimage' ) { - // Run through and pull all our data in one query - foreach( $this->ofiles as $timestamp ) { - $where[] = $timestamp.'!'.$this->page->getDBKey(); - } - $result = $dbr->select( 'oldimage', '*', - array( - 'oi_name' => $this->page->getDBKey(), - 'oi_archive_name' => $where ), - __METHOD__ ); - while( $row = $dbr->fetchObject( $result ) ) { - $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row ); - $filesObjs[$row->oi_archive_name]->user = $row->oi_user; - $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text; + protected function addUsageText() { + global $wgOut, $wgUser; + $wgOut->addWikiMsg( 'revdelete-text' ); + if( $wgUser->isAllowed( 'suppressrevision' ) ) { + $wgOut->addWikiMsg( 'revdelete-suppress-text' ); + } + if( $this->mIsAllowed ) { + $wgOut->addWikiMsg( 'revdelete-confirm' ); + } + } + + /** + * @return String: HTML + */ + protected function buildCheckBoxes() { + global $wgRequest; + + $html = '<table>'; + // If there is just one item, use checkboxes + $list = $this->getList(); + if( $list->length() == 1 ) { + $list->reset(); + $bitfield = $list->current()->getBits(); // existing field + if( $this->submitClicked ) { + $bitfield = $this->extractBitfield( $this->extractBitParams($wgRequest), $bitfield ); } - // Check through our images - foreach( $this->ofiles as $timestamp ) { - $archivename = $timestamp.'!'.$this->page->getDBKey(); - if( !isset($filesObjs[$archivename]) ) { - continue; - } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) { - // If a rev is hidden from sysops - if( !$this->wasPosted ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return; - } - $UserAllowed = false; - } - $revisions++; - // Inject history info - $wgOut->addHTML( $this->fileLine( $filesObjs[$archivename] ) ); - $bitfields |= $filesObjs[$archivename]->deleted; + foreach( $this->checks as $item ) { + list( $message, $name, $field ) = $item; + $innerHTML = Xml::checkLabel( wfMsg($message), $name, $name, $bitfield & $field ); + if( $field == Revision::DELETED_RESTRICTED ) + $innerHTML = "<b>$innerHTML</b>"; + $line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML ); + $html .= "<tr>$line</tr>\n"; } - // Archived files... + // Otherwise, use tri-state radios } else { - // Run through and pull all our data in one query - foreach( $this->afiles as $id ) { - $where[] = intval($id); - } - $result = $dbr->select( 'filearchive', '*', - array( - 'fa_name' => $this->page->getDBKey(), - 'fa_id' => $where ), - __METHOD__ ); - while( $row = $dbr->fetchObject( $result ) ) { - $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row ); - } - - foreach( $this->afiles as $fileid ) { - if( !isset($filesObjs[$fileid]) ) { - continue; - } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) { - // If a rev is hidden from sysops - if( !$this->wasPosted ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return; - } - $UserAllowed = false; + $html .= '<tr>'; + $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-same').'</th>'; + $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-unset').'</th>'; + $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-set').'</th>'; + $html .= "<th></th></tr>\n"; + foreach( $this->checks as $item ) { + list( $message, $name, $field ) = $item; + // If there are several items, use third state by default... + if( $this->submitClicked ) { + $selected = $wgRequest->getInt( $name, 0 /* unchecked */ ); + } else { + $selected = -1; // use existing field + } + $line = '<td class="mw-revdel-checkbox">' . Xml::radio( $name, -1, $selected == -1 ) . '</td>'; + $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 0, $selected == 0 ) . '</td>'; + $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 1, $selected == 1 ) . '</td>'; + $label = wfMsgHtml($message); + if( $field == Revision::DELETED_RESTRICTED ) { + $label = "<b>$label</b>"; } - $revisions++; - // Inject history info - $wgOut->addHTML( $this->archivedfileLine( $filesObjs[$fileid] ) ); - $bitfields |= $filesObjs[$fileid]->deleted; + $line .= "<td>$label</td>"; + $html .= "<tr>$line</tr>\n"; } } - if( !$revisions ) { - $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); - return; - } - $wgOut->addHTML( "</ul>" ); - // Explanation text - $this->addUsageText(); - // Normal sysops can always see what they did, but can't always change it - if( !$UserAllowed ) return; - - $items = array( - Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ), - Xml::submitButton( wfMsg( 'revdelete-submit' ) ) - ); - $hidden = array( - Xml::hidden( 'wpEditToken', $wgUser->editToken() ), - Xml::hidden( 'target', $this->page->getPrefixedText() ), - Xml::hidden( 'type', $this->deleteKey ) - ); - if( $this->deleteKey=='oldimage' ) { - foreach( $this->ofiles as $filename ) - $hidden[] = Xml::hidden( 'oldimage[]', $filename ); - } else { - foreach( $this->afiles as $fileid ) - $hidden[] = Xml::hidden( 'fileid[]', $fileid ); - } - $special = SpecialPage::getTitleFor( 'Revisiondelete' ); - $wgOut->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), - 'id' => 'mw-revdel-form-filerevisions' ) ) . - Xml::fieldset( wfMsg( 'revdelete-legend' ) ) - ); + $html .= '</table>'; + return $html; + } - $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) ); - foreach( $items as $item ) { - $wgOut->addHTML( "<p>$item</p>" ); + /** + * UI entry point for form submission. + * @param $request WebRequest + */ + protected function submit( $request ) { + global $wgUser, $wgOut; + # Check edit token on submission + if( $this->submitClicked && !$wgUser->matchEditToken( $request->getVal('wpEditToken') ) ) { + $wgOut->addWikiMsg( 'sessionfailure' ); + return false; } - foreach( $hidden as $item ) { - $wgOut->addHTML( $item ); + $bitParams = $this->extractBitParams( $request ); + $listReason = $request->getText( 'wpRevDeleteReasonList', 'other' ); // from dropdown + $comment = $listReason; + if( $comment != 'other' && $this->otherReason != '' ) { + // Entry from drop down menu + additional comment + $comment .= wfMsgForContent( 'colon-separator' ) . $this->otherReason; + } elseif( $comment == 'other' ) { + $comment = $this->otherReason; } - - $wgOut->addHTML( - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ) . "\n" - ); + # Can the user set this field? + if( $bitParams[Revision::DELETED_RESTRICTED]==1 && !$wgUser->isAllowed('suppressrevision') ) { + $wgOut->permissionRequired( 'suppressrevision' ); + return false; + } + # 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 { + $this->failure( $status ); + } + return false; } /** - * This lets a user set restrictions for log items + * Report that the submit operation succeeded */ - private function showLogItems() { - global $wgOut, $wgUser, $wgMessageCache, $wgLang; - $UserAllowed = true; - - $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->events) ) ); + protected function success() { + global $wgOut; + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->wrapWikiMsg( '<span class="success">$1</span>', $this->typeInfo['success'] ); + $this->list->reloadFromMaster(); + $this->showForm(); + } - $bitfields = 0; - $wgOut->addHTML( "<ul>" ); + /** + * Report that the submit operation failed + */ + protected function failure( $status ) { + global $wgOut; + $wgOut->setPagetitle( wfMsg( 'actionfailed' ) ); + $wgOut->addWikiText( $status->getWikiText( $this->typeInfo['failure'] ) ); + $this->showForm(); + } - $where = $logRows = array(); - $dbr = wfGetDB( DB_MASTER ); - // Run through and pull all our data in one query - $logItems = 0; - foreach( $this->events as $logid ) { - $where[] = intval($logid); + /** + * Put together an array that contains -1, 0, or the *_deleted const for each bit + * @param $request WebRequest + * @return array + */ + protected function extractBitParams( $request ) { + $bitfield = array(); + foreach( $this->checks as $item ) { + list( /* message */ , $name, $field ) = $item; + $val = $request->getInt( $name, 0 /* unchecked */ ); + if( $val < -1 || $val > 1) { + $val = -1; // -1 for existing value + } + $bitfield[$field] = $val; } - list($log,$logtype) = explode( '/',$this->page->getDBKey(), 2 ); - $result = $dbr->select( 'logging', '*', - array( - 'log_type' => $logtype, - 'log_id' => $where ), - __METHOD__ ); - while( $row = $dbr->fetchObject( $result ) ) { - $logRows[$row->log_id] = $row; + if( !isset($bitfield[Revision::DELETED_RESTRICTED]) ) { + $bitfield[Revision::DELETED_RESTRICTED] = 0; } - $wgMessageCache->loadAllMessages(); - foreach( $this->events as $logid ) { - // Don't hide from oversight log!!! - if( !isset( $logRows[$logid] ) || $logRows[$logid]->log_type=='suppress' ) { - continue; - } else if( !LogEventsList::userCan( $logRows[$logid],Revision::DELETED_RESTRICTED) ) { - // If an event is hidden from sysops - if( !$this->wasPosted ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return; - } - $UserAllowed = false; + return $bitfield; + } + + /** + * Put together a rev_deleted bitfield + * @param $bitPars array extractBitParams() params + * @param $oldfield int current bitfield + * @return array + */ + public static function extractBitfield( $bitPars, $oldfield ) { + // Build the actual new rev_deleted bitfield + $newBits = 0; + foreach( $bitPars as $const => $val ) { + if( $val == 1 ) { + $newBits |= $const; // $const is the *_deleted const + } else if( $val == -1 ) { + $newBits |= ($oldfield & $const); // use existing } - $logItems++; - $wgOut->addHTML( $this->logLine( $logRows[$logid] ) ); - $bitfields |= $logRows[$logid]->log_deleted; - } - if( !$logItems ) { - $wgOut->showErrorPage( 'revdelete-nologid-title', 'revdelete-nologid-text' ); - return; } - - $wgOut->addHTML( "</ul>" ); - // Explanation text - $this->addUsageText(); - // Normal sysops can always see what they did, but can't always change it - if( !$UserAllowed ) return; + return $newBits; + } - $items = array( - Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ), - Xml::submitButton( wfMsg( 'revdelete-submit' ) ) ); - $hidden = array( - Xml::hidden( 'wpEditToken', $wgUser->editToken() ), - Xml::hidden( 'target', $this->page->getPrefixedText() ), - Xml::hidden( 'type', $this->deleteKey ) ); - foreach( $this->events as $logid ) { - $hidden[] = Xml::hidden( 'logid[]', $logid ); - } - - $special = SpecialPage::getTitleFor( 'Revisiondelete' ); - $wgOut->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), - 'id' => 'mw-revdel-form-logs' ) ) . - Xml::fieldset( wfMsg( 'revdelete-legend' ) ) + /** + * Do the write operations. Simple wrapper for RevDel_*List::setVisibility(). + */ + protected function save( $bitfield, $reason, $title ) { + return $this->getList()->setVisibility( + array( 'value' => $bitfield, 'comment' => $reason ) ); - - $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) ); - foreach( $items as $item ) { - $wgOut->addHTML( "<p>$item</p>" ); + } +} + +/** + * Temporary b/c interface, collection of static functions. + * @ingroup SpecialPage + */ +class RevisionDeleter { + /** + * Checks for a change in the bitfield for a certain option and updates the + * provided array accordingly. + * + * @param $desc String: description to add to the array if the option was + * enabled / disabled. + * @param $field Integer: the bitmask describing the single option. + * @param $diff Integer: the xor of the old and new bitfields. + * @param $new Integer: the new bitfield + * @param $arr Array: the array to update. + */ + protected static function checkItem( $desc, $field, $diff, $new, &$arr ) { + if( $diff & $field ) { + $arr[ ( $new & $field ) ? 0 : 1 ][] = $desc; + } + } + + /** + * Gets an array describing the changes made to the visibilit of the revision. + * If the resulting array is $arr, then $arr[0] will contain an array of strings + * describing the items that were hidden, $arr[2] will contain an array of strings + * describing the items that were unhidden, and $arr[3] will contain an array with + * a single string, which can be one of "applied restrictions to sysops", + * "removed restrictions from sysops", or null. + * + * @param $n Integer: the new bitfield. + * @param $o Integer: the old bitfield. + * @return An array as described above. + */ + protected static function getChanges( $n, $o ) { + $diff = $n ^ $o; + $ret = array( 0 => array(), 1 => array(), 2 => array() ); + // Build bitfield changes in language + self::checkItem( wfMsgForContent( 'revdelete-content' ), + Revision::DELETED_TEXT, $diff, $n, $ret ); + self::checkItem( wfMsgForContent( 'revdelete-summary' ), + Revision::DELETED_COMMENT, $diff, $n, $ret ); + self::checkItem( wfMsgForContent( 'revdelete-uname' ), + Revision::DELETED_USER, $diff, $n, $ret ); + // Restriction application to sysops + if( $diff & Revision::DELETED_RESTRICTED ) { + if( $n & Revision::DELETED_RESTRICTED ) + $ret[2][] = wfMsgForContent( 'revdelete-restricted' ); + else + $ret[2][] = wfMsgForContent( 'revdelete-unrestricted' ); } - foreach( $hidden as $item ) { - $wgOut->addHTML( $item ); + return $ret; + } + + /** + * Gets a log message to describe the given revision visibility change. This + * message will be of the form "[hid {content, edit summary, username}]; + * [unhid {...}][applied restrictions to sysops] for $count revisions: $comment". + * + * @param $count Integer: The number of effected revisions. + * @param $nbitfield Integer: The new bitfield for the revision. + * @param $obitfield Integer: The old bitfield for the revision. + * @param $isForLog Boolean + */ + public static function getLogMessage( $count, $nbitfield, $obitfield, $isForLog = false ) { + global $wgLang; + $s = ''; + $changes = self::getChanges( $nbitfield, $obitfield ); + if( count( $changes[0] ) ) { + $s .= wfMsgForContent( 'revdelete-hid', implode( ', ', $changes[0] ) ); } + if( count( $changes[1] ) ) { + if ($s) $s .= '; '; + $s .= wfMsgForContent( 'revdelete-unhid', implode( ', ', $changes[1] ) ); + } + if( count( $changes[2] ) ) { + $s .= $s ? ' (' . $changes[2][0] . ')' : $changes[2][0]; + } + $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message'; + return wfMsgExt( $msg, array( 'parsemag', 'content' ), $s, $wgLang->formatNum($count) ); - $wgOut->addHTML( - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ) . "\n" - ); } - private function addUsageText() { - global $wgOut, $wgUser; - $wgOut->addWikiMsg( 'revdelete-text' ); - if( $wgUser->isAllowed( 'suppressrevision' ) ) { - $wgOut->addWikiMsg( 'revdelete-suppress-text' ); + // Get DB field name for URL param... + // Future code for other things may also track + // other types of revision-specific changes. + // @returns string One of log_id/rev_id/fa_id/ar_timestamp/oi_archive_name + public static function getRelationType( $typeName ) { + if ( isset( SpecialRevisionDelete::$deprecatedTypeMap[$typeName] ) ) { + $typeName = SpecialRevisionDelete::$deprecatedTypeMap[$typeName]; + } + if ( isset( SpecialRevisionDelete::$allowedTypes[$typeName] ) ) { + $class = SpecialRevisionDelete::$allowedTypes[$typeName]['list-class']; + $list = new $class( null, null, null ); + return $list->getIdField(); + } else { + return null; } } - +} + +/** + * Abstract base class for a list of deletable items + */ +abstract class RevDel_List { + var $special, $title, $ids, $res, $current; + var $type = null; // override this + var $idField = null; // override this + var $dateField = false; // override this + var $authorIdField = false; // override this + var $authorNameField = false; // override this + /** - * @param int $bitfields, aggregate bitfield of all the bitfields - * @returns string HTML - */ - private function buildCheckBoxes( $bitfields ) { - $html = ''; - // FIXME: all items checked for just one rev are checked, even if not set for the others - foreach( $this->checks as $item ) { - list( $message, $name, $field ) = $item; - $line = Xml::tags( 'div', null, Xml::checkLabel( wfMsg($message), $name, $name, - $bitfields & $field ) ); - if( $field == Revision::DELETED_RESTRICTED ) $line = "<b>$line</b>"; - $html .= $line; - } - return $html; + * @param $special The parent SpecialPage + * @param $title The target title + * @param $ids Array of IDs + */ + public function __construct( $special, $title, $ids ) { + $this->special = $special; + $this->title = $title; + $this->ids = $ids; } /** - * @param Revision $rev - * @returns string + * Get the internal type name of this list. Equal to the table name. */ - private function historyLine( $rev ) { - global $wgLang, $wgUser; + public function getType() { + return $this->type; + } - $date = $wgLang->timeanddate( $rev->getTimestamp() ); - $difflink = $del = ''; - // Live revisions - if( $this->deleteKey=='oldid' ) { - $tokenParams = '&unhide=1&token='.urlencode( $wgUser->editToken( $rev->getId() ) ); - $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid='.$rev->getId() . $tokenParams ); - $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'), - 'diff=' . $rev->getId() . '&oldid=prev' . $tokenParams ) . ')'; - // Archived revisions - } else { - $undelete = SpecialPage::getTitleFor( 'Undelete' ); - $target = $this->page->getPrefixedText(); - $revlink = $this->skin->makeLinkObj( $undelete, $date, - "target=$target×tamp=" . $rev->getTimestamp() ); - $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'), - "target=$target&diff=prev×tamp=" . $rev->getTimestamp() ) . ')'; - } - // Check permissions; items may be "suppressed" - if( $rev->isDeleted(Revision::DELETED_TEXT) ) { - $revlink = '<span class="history-deleted">'.$revlink.'</span>'; - $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; - if( !$rev->userCan(Revision::DELETED_TEXT) ) { - $revlink = '<span class="history-deleted">'.$date.'</span>'; - $difflink = '(' . wfMsgHtml('diff') . ')'; - } - } - $userlink = $this->skin->revUserLink( $rev ); - $comment = $this->skin->revComment( $rev ); + /** + * Get the DB field name associated with the ID list + */ + public function getIdField() { + return $this->idField; + } - return "<li>$difflink $revlink $userlink $comment{$del}</li>"; + /** + * Get the DB field name storing timestamps + */ + public function getTimestampField() { + return $this->dateField; } /** - * @param File $file - * @returns string + * Get the DB field name storing user ids */ - private function fileLine( $file ) { - global $wgLang, $wgTitle; + public function getAuthorIdField() { + return $this->authorIdField; + } - $target = $this->page->getPrefixedText(); - $date = $wgLang->timeanddate( $file->getTimestamp(), true ); + /** + * Get the DB field name storing user names + */ + public function getAuthorNameField() { + return $this->authorNameField; + } + /** + * Set the visibility for the revisions in this list. Logging and + * transactions are done here. + * + * @param $params Associative array of parameters. Members are: + * value: The integer value to set the visibility to + * comment: The log comment. + * @return Status + */ + public function setVisibility( $params ) { + $bitPars = $params['value']; + $comment = $params['comment']; - $del = ''; - # Hidden files... - if( $file->isDeleted(File::DELETED_FILE) ) { - $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; - if( !$file->userCan(File::DELETED_FILE) ) { - $pageLink = $date; + $this->res = false; + $dbw = wfGetDB( DB_MASTER ); + $this->doQuery( $dbw ); + $dbw->begin(); + $status = Status::newGood(); + $missing = array_flip( $this->ids ); + $this->clearFileOps(); + $idsForLog = array(); + $authorIds = $authorIPs = array(); + + for ( $this->reset(); $this->current(); $this->next() ) { + $item = $this->current(); + unset( $missing[ $item->getId() ] ); + + $oldBits = $item->getBits(); + // Build the actual new rev_deleted bitfield + $newBits = SpecialRevisionDelete::extractBitfield( $bitPars, $oldBits ); + + if ( $oldBits == $newBits ) { + $status->warning( 'revdelete-no-change', $item->formatDate(), $item->formatTime() ); + $status->failCount++; + continue; + } elseif ( $oldBits == 0 && $newBits != 0 ) { + $opType = 'hide'; + } elseif ( $oldBits != 0 && $newBits == 0 ) { + $opType = 'show'; } else { - $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date, - "target=$target&file=$file->sha1.".$file->getExtension() ); + $opType = 'modify'; + } + + if ( $item->isHideCurrentOp( $newBits ) ) { + // Cannot hide current version text + $status->error( 'revdelete-hide-current', $item->formatDate(), $item->formatTime() ); + $status->failCount++; + continue; + } + if ( !$item->canView() ) { + // Cannot access this revision + $msg = ($opType == 'show') ? + 'revdelete-show-no-access' : 'revdelete-modify-no-access'; + $status->error( $msg, $item->formatDate(), $item->formatTime() ); + $status->failCount++; + continue; + } + // Cannot just "hide from Sysops" without hiding any fields + if( $newBits == Revision::DELETED_RESTRICTED ) { + $status->warning( 'revdelete-only-restricted', $item->formatDate(), $item->formatTime() ); + $status->failCount++; + continue; + } + + // Update the revision + $ok = $item->setBits( $newBits ); + + if ( $ok ) { + $idsForLog[] = $item->getId(); + $status->successCount++; + if( $item->getAuthorId() > 0 ) { + $authorIds[] = $item->getAuthorId(); + } else if( IP::isIPAddress( $item->getAuthorName() ) ) { + $authorIPs[] = $item->getAuthorName(); + } + } else { + $status->error( 'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() ); + $status->failCount++; } - $pageLink = '<span class="history-deleted">' . $pageLink . '</span>'; - # Regular files... - } else { - $url = $file->getUrlRel(); - $pageLink = "<a href=\"{$url}\">{$date}</a>"; } - $data = wfMsg( 'widthheight', - $wgLang->formatNum( $file->getWidth() ), - $wgLang->formatNum( $file->getHeight() ) ) . - ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')'; - $data = htmlspecialchars( $data ); + // Handle missing revisions + foreach ( $missing as $id => $unused ) { + $status->error( 'revdelete-modify-missing', $id ); + $status->failCount++; + } - return "<li>$pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>"; - } + if ( $status->successCount == 0 ) { + $status->ok = false; + $dbw->rollback(); + return $status; + } - /** - * @param ArchivedFile $file - * @returns string - */ - private function archivedfileLine( $file ) { - global $wgLang; + // Save success count + $successCount = $status->successCount; - $target = $this->page->getPrefixedText(); - $date = $wgLang->timeanddate( $file->getTimestamp(), true ); + // Move files, if there are any + $status->merge( $this->doPreCommitUpdates() ); + if ( !$status->isOK() ) { + // Fatal error, such as no configured archive directory + $dbw->rollback(); + return $status; + } - $undelete = SpecialPage::getTitleFor( 'Undelete' ); - $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file={$file->getKey()}" ); + // Log it + $this->updateLog( array( + 'title' => $this->title, + 'count' => $successCount, + 'newBits' => $newBits, + 'oldBits' => $oldBits, + 'comment' => $comment, + 'ids' => $idsForLog, + 'authorIds' => $authorIds, + 'authorIPs' => $authorIPs + ) ); + $dbw->commit(); - $del = ''; - if( $file->isDeleted(File::DELETED_FILE) ) { - $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; - } + // Clear caches + $status->merge( $this->doPostCommitUpdates() ); + return $status; + } - $data = wfMsg( 'widthheight', - $wgLang->formatNum( $file->getWidth() ), - $wgLang->formatNum( $file->getHeight() ) ) . - ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')'; - $data = htmlspecialchars( $data ); + /** + * Reload the list data from the master DB. This can be done after setVisibility() + * to allow $item->getHTML() to show the new data. + */ + function reloadFromMaster() { + $dbw = wfGetDB( DB_MASTER ); + $this->res = $this->doQuery( $dbw ); + } - return "<li> $pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>"; + /** + * Record a log entry on the action + * @param $params Associative array of parameters: + * newBits: The new value of the *_deleted bitfield + * oldBits: The old value of the *_deleted bitfield. + * title: The target title + * ids: The ID list + * comment: The log comment + * authorsIds: The array of the user IDs of the offenders + * authorsIPs: The array of the IP/anon user offenders + */ + protected function updateLog( $params ) { + // Get the URL param's corresponding DB field + $field = RevisionDeleter::getRelationType( $this->getType() ); + if( !$field ) { + throw new MWException( "Bad log URL param type!" ); + } + // Put things hidden from sysops in the oversight log + if ( ( $params['newBits'] | $params['oldBits'] ) & $this->getSuppressBit() ) { + $logType = 'suppress'; + } else { + $logType = 'delete'; + } + // Add params for effected page and ids + $logParams = $this->getLogParams( $params ); + // Actually add the deletion log entry + $log = new LogPage( $logType ); + $logid = $log->addEntry( $this->getLogAction(), $params['title'], + $params['comment'], $logParams ); + // Allow for easy searching of deletion log items for revision/log items + $log->addRelations( $field, $params['ids'], $logid ); + $log->addRelations( 'target_author_id', $params['authorIds'], $logid ); + $log->addRelations( 'target_author_ip', $params['authorIPs'], $logid ); } /** - * @param Array $row row - * @returns string + * Get the log action for this list type */ - private function logLine( $row ) { - global $wgLang; + public function getLogAction() { + return 'revision'; + } - $date = $wgLang->timeanddate( $row->log_timestamp ); - $paramArray = LogPage::extractParams( $row->log_params ); - $title = Title::makeTitle( $row->log_namespace, $row->log_title ); + /** + * Get log parameter array. + * @param $params Associative array of log parameters, same as updateLog() + * @return array + */ + public function getLogParams( $params ) { + return array( + $this->getType(), + implode( ',', $params['ids'] ), + "ofield={$params['oldBits']}", + "nfield={$params['newBits']}" + ); + } - $logtitle = SpecialPage::getTitleFor( 'Log' ); - $loglink = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'log' ), - wfArrayToCGI( array( 'page' => $title->getPrefixedUrl() ) ) ); - // Action text - if( !LogEventsList::userCan($row,LogPage::DELETED_ACTION) ) { - $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; + /** + * Initialise the current iteration pointer + */ + protected function initCurrent() { + $row = $this->res->current(); + if ( $row ) { + $this->current = $this->newItem( $row ); } else { - $action = LogPage::actionText( $row->log_type, $row->log_action, $title, - $this->skin, $paramArray, true, true ); - if( $row->log_deleted & LogPage::DELETED_ACTION ) - $action = '<span class="history-deleted">' . $action . '</span>'; - } - // User links - $userLink = $this->skin->userLink( $row->log_user, User::WhoIs($row->log_user) ); - if( LogEventsList::isDeleted($row,LogPage::DELETED_USER) ) { - $userLink = '<span class="history-deleted">' . $userLink . '</span>'; + $this->current = false; } - // Comment - $comment = $wgLang->getDirMark() . $this->skin->commentBlock( $row->log_comment ); - if( LogEventsList::isDeleted($row,LogPage::DELETED_COMMENT) ) { - $comment = '<span class="history-deleted">' . $comment . '</span>'; - } - return "<li>($loglink) $date $userLink $action $comment</li>"; } /** - * Generate a user tool link cluster if the current user is allowed to view it - * @param ArchivedFile $file - * @return string HTML + * Start iteration. This must be called before current() or next(). + * @return First list item */ - private function fileUserTools( $file ) { - if( $file->userCan( Revision::DELETED_USER ) ) { - $link = $this->skin->userLink( $file->user, $file->user_text ) . - $this->skin->userToolLinks( $file->user, $file->user_text ); + public function reset() { + if ( !$this->res ) { + $this->res = $this->doQuery( wfGetDB( DB_SLAVE ) ); } else { - $link = wfMsgHtml( 'rev-deleted-user' ); - } - if( $file->isDeleted( Revision::DELETED_USER ) ) { - return '<span class="history-deleted">' . $link . '</span>'; + $this->res->rewind(); } - return $link; + $this->initCurrent(); + return $this->current; } /** - * Wrap and format the given file's comment block, if the current - * user is allowed to view it. - * - * @param ArchivedFile $file - * @return string HTML + * Get the current list item, or false if we are at the end */ - private function fileComment( $file ) { - if( $file->userCan( File::DELETED_COMMENT ) ) { - $block = $this->skin->commentBlock( $file->description ); - } else { - $block = ' ' . wfMsgHtml( 'rev-deleted-comment' ); - } - if( $file->isDeleted( File::DELETED_COMMENT ) ) { - return "<span class=\"history-deleted\">$block</span>"; - } - return $block; + public function current() { + return $this->current; } /** - * @param WebRequest $request + * Move the iteration pointer to the next list item, and return it. */ - private function submit( $request ) { - global $wgUser, $wgOut; - # Check edit token on submission - if( $this->wasPosted && !$wgUser->matchEditToken( $request->getVal('wpEditToken') ) ) { - $wgOut->addWikiMsg( 'sessionfailure' ); - return false; - } - $bitfield = $this->extractBitfield( $request ); - $comment = $request->getText( 'wpReason' ); - # Can the user set this field? - if( $bitfield & Revision::DELETED_RESTRICTED && !$wgUser->isAllowed('suppressrevision') ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return false; - } - # If the save went through, go to success message. Otherwise - # bounce back to form... - if( $this->save( $bitfield, $comment, $this->page ) ) { - $this->success(); - } else if( $request->getCheck( 'oldid' ) || $request->getCheck( 'artimestamp' ) ) { - return $this->showRevs(); - } else if( $request->getCheck( 'logid' ) ) { - return $this->showLogs(); - } else if( $request->getCheck( 'oldimage' ) || $request->getCheck( 'fileid' ) ) { - return $this->showImages(); + public function next() { + $this->res->next(); + $this->initCurrent(); + return $this->current; + } + + /** + * Get the number of items in the list. + */ + public function length() { + if( !$this->res ) { + return 0; + } else { + return $this->res->numRows(); } } - private function success() { - global $wgOut; - - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - - $wrap = '<span class="success">$1</span>'; + /** + * Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates() + * STUB + */ + public function clearFileOps() { + } - if( $this->deleteKey=='logid' ) { - $wgOut->wrapWikiMsg( $wrap, 'logdelete-success' ); - $this->showLogItems(); - } else if( $this->deleteKey=='oldid' || $this->deleteKey=='artimestamp' ) { - $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' ); - $this->showRevs(); - } else if( $this->deleteKey=='fileid' ) { - $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' ); - $this->showImages(); - } else if( $this->deleteKey=='oldimage' ) { - $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' ); - $this->showImages(); - } + /** + * A hook for setVisibility(): do batch updates pre-commit. + * STUB + * @return Status + */ + public function doPreCommitUpdates() { + return Status::newGood(); } /** - * Put together a rev_deleted bitfield from the submitted checkboxes - * @param WebRequest $request - * @return int + * A hook for setVisibility(): do any necessary updates post-commit. + * STUB + * @return Status */ - private function extractBitfield( $request ) { - $bitfield = 0; - foreach( $this->checks as $item ) { - list( /* message */ , $name, $field ) = $item; - if( $request->getCheck( $name ) ) { - $bitfield |= $field; - } - } - return $bitfield; + public function doPostCommitUpdates() { + return Status::newGood(); } - private function save( $bitfield, $reason, $title ) { - $dbw = wfGetDB( DB_MASTER ); - // Don't allow simply locking the interface for no reason - if( $bitfield == Revision::DELETED_RESTRICTED ) { - $bitfield = 0; - } - $deleter = new RevisionDeleter( $dbw ); - // By this point, only one of the below should be set - if( isset($this->revisions) ) { - return $deleter->setRevVisibility( $title, $this->revisions, $bitfield, $reason ); - } else if( isset($this->archrevs) ) { - return $deleter->setArchiveVisibility( $title, $this->archrevs, $bitfield, $reason ); - } else if( isset($this->ofiles) ) { - return $deleter->setOldImgVisibility( $title, $this->ofiles, $bitfield, $reason ); - } else if( isset($this->afiles) ) { - return $deleter->setArchFileVisibility( $title, $this->afiles, $bitfield, $reason ); - } else if( isset($this->events) ) { - return $deleter->setEventVisibility( $title, $this->events, $bitfield, $reason ); - } - } + /** + * Create an item object from a DB result row + * @param $row stdclass + */ + abstract public function newItem( $row ); + + /** + * Do the DB query to iterate through the objects. + * @param $db Database object to use for the query + */ + abstract public function doQuery( $db ); + + /** + * Get the integer value of the flag used for suppression + */ + abstract public function getSuppressBit(); } /** - * Implements the actions for Revision Deletion. - * @ingroup SpecialPage + * Abstract base class for deletable items */ -class RevisionDeleter { - function __construct( $db ) { - $this->dbw = $db; +abstract class RevDel_Item { + /** The parent SpecialPage */ + var $special; + + /** The parent RevDel_List */ + var $list; + + /** The DB result row */ + var $row; + + /** + * @param $list RevDel_List + * @param $row DB result row + */ + public function __construct( $list, $row ) { + $this->special = $list->special; + $this->list = $list; + $this->row = $row; } /** - * @param $title, the page these events apply to - * @param array $items list of revision ID numbers - * @param int $bitfield new rev_deleted value - * @param string $comment Comment for log records + * Get the ID, as it would appear in the ids URL parameter */ - function setRevVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; + public function getId() { + $field = $this->list->getIdField(); + return $this->row->$field; + } - $userAllowedAll = $success = true; - $revIDs = array(); - $revCount = 0; - // Run through and pull all our data in one query - foreach( $items as $revid ) { - $where[] = intval($revid); - } - $result = $this->dbw->select( 'revision', '*', - array( - 'rev_page' => $title->getArticleID(), - 'rev_id' => $where ), - __METHOD__ ); - while( $row = $this->dbw->fetchObject( $result ) ) { - $revObjs[$row->rev_id] = new Revision( $row ); - } - // To work! - foreach( $items as $revid ) { - if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) { - $success = false; - continue; // Must exist - } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) { - $userAllowedAll=false; - continue; - } - // For logging, maintain a count of revisions - if( $revObjs[$revid]->mDeleted != $bitfield ) { - $revCount++; - $revIDs[]=$revid; + /** + * Get the date, formatted with $wgLang + */ + public function formatDate() { + global $wgLang; + return $wgLang->date( $this->getTimestamp() ); + } - $this->updateRevision( $revObjs[$revid], $bitfield ); - $this->updateRecentChangesEdits( $revObjs[$revid], $bitfield, false ); - } - } - // Clear caches... - // Don't log or touch if nothing changed - if( $revCount > 0 ) { - $this->updateLog( $title, $revCount, $bitfield, $revObjs[$revid]->mDeleted, - $comment, $title, 'oldid', $revIDs ); - $this->updatePage( $title ); - } - // Where all revs allowed to be set? - if( !$userAllowedAll ) { - //FIXME: still might be confusing??? - $wgOut->permissionRequired( 'suppressrevision' ); - return false; - } + /** + * Get the time, formatted with $wgLang + */ + public function formatTime() { + global $wgLang; + return $wgLang->time( $this->getTimestamp() ); + } - return $success; + /** + * Get the timestamp in MW 14-char form + */ + public function getTimestamp() { + $field = $this->list->getTimestampField(); + return wfTimestamp( TS_MW, $this->row->$field ); + } + + /** + * Get the author user ID + */ + public function getAuthorId() { + $field = $this->list->getAuthorIdField(); + return intval( $this->row->$field ); + } + + /** + * Get the author user name + */ + public function getAuthorName() { + $field = $this->list->getAuthorNameField(); + return strval( $this->row->$field ); } - /** - * @param $title, the page these events apply to - * @param array $items list of revision ID numbers - * @param int $bitfield new rev_deleted value - * @param string $comment Comment for log records + /** + * Returns true if the item is "current", and the operation to set the given + * bits can't be executed for that reason + * STUB */ - function setArchiveVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; + public function isHideCurrentOp( $newBits ) { + return false; + } - $userAllowedAll = $success = true; - $count = 0; - $Id_set = array(); - // Run through and pull all our data in one query - foreach( $items as $timestamp ) { - $where[] = $this->dbw->timestamp( $timestamp ); - } - $result = $this->dbw->select( 'archive', '*', - array( - 'ar_namespace' => $title->getNamespace(), - 'ar_title' => $title->getDBKey(), - 'ar_timestamp' => $where ), - __METHOD__ ); - while( $row = $this->dbw->fetchObject( $result ) ) { - $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp ); - $revObjs[$timestamp] = new Revision( array( - 'page' => $title->getArticleId(), - 'id' => $row->ar_rev_id, - 'text' => $row->ar_text_id, - 'comment' => $row->ar_comment, - 'user' => $row->ar_user, - 'user_text' => $row->ar_user_text, - 'timestamp' => $timestamp, - 'minor_edit' => $row->ar_minor_edit, - 'text_id' => $row->ar_text_id, - 'deleted' => $row->ar_deleted, - 'len' => $row->ar_len) ); - } - // To work! - foreach( $items as $timestamp ) { - // This will only select the first revision with this timestamp. - // Since they are all selected/deleted at once, we can just check the - // permissions of one. UPDATE is done via timestamp, so all revs are set. - if( !is_object($revObjs[$timestamp]) ) { - $success = false; - continue; // Must exist - } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) { - $userAllowedAll=false; - continue; - } - // Which revisions did we change anything about? - if( $revObjs[$timestamp]->mDeleted != $bitfield ) { - $Id_set[]=$timestamp; - $count++; + /** + * Returns true if the current user can view the item + */ + abstract public function canView(); + + /** + * Returns true if the current user can view the item text/file + */ + abstract public function canViewContent(); - $this->updateArchive( $revObjs[$timestamp], $title, $bitfield ); - } - } - // For logging, maintain a count of revisions - if( $count > 0 ) { - $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted, - $comment, $title, 'artimestamp', $Id_set ); - } - // Where all revs allowed to be set? - if( !$userAllowedAll ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return false; - } + /** + * Get the current deletion bitfield value + */ + abstract public function getBits(); - return $success; - } + /** + * Get the HTML of the list item. Should be include <li></li> tags. + * This is used to show the list in HTML form, by the special page. + */ + abstract public function getHTML(); - /** - * @param $title, the page these events apply to - * @param array $items list of revision ID numbers - * @param int $bitfield new rev_deleted value - * @param string $comment Comment for log records + /** + * Set the visibility of the item. This should do any necessary DB queries. + * + * The DB update query should have a condition which forces it to only update + * if the value in the DB matches the value fetched earlier with the SELECT. + * If the update fails because it did not match, the function should return + * false. This prevents concurrency problems. + * + * @return boolean success */ - function setOldImgVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; + abstract public function setBits( $newBits ); +} - $userAllowedAll = $success = true; - $count = 0; - $set = array(); - // Run through and pull all our data in one query - foreach( $items as $timestamp ) { - $where[] = $timestamp.'!'.$title->getDBKey(); - } - $result = $this->dbw->select( 'oldimage', '*', +/** + * List for revision table items + */ +class RevDel_RevisionList extends RevDel_List { + var $currentRevId; + var $type = 'revision'; + var $idField = 'rev_id'; + var $dateField = 'rev_timestamp'; + var $authorIdField = 'rev_user'; + var $authorNameField = 'rev_user_text'; + + public function doQuery( $db ) { + $ids = array_map( 'intval', $this->ids ); + return $db->select( array('revision','page'), '*', array( - 'oi_name' => $title->getDBKey(), - 'oi_archive_name' => $where ), - __METHOD__ ); - while( $row = $this->dbw->fetchObject( $result ) ) { - $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row ); - $filesObjs[$row->oi_archive_name]->user = $row->oi_user; - $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text; - } - // To work! - foreach( $items as $timestamp ) { - $archivename = $timestamp.'!'.$title->getDBKey(); - if( !isset($filesObjs[$archivename]) ) { - $success = false; - continue; // Must exist - } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) { - $userAllowedAll=false; - continue; - } + 'rev_page' => $this->title->getArticleID(), + 'rev_id' => $ids, + 'rev_page = page_id' + ), + __METHOD__, + array( 'ORDER BY' => 'rev_id DESC' ) + ); + } - $transaction = true; - // Which revisions did we change anything about? - if( $filesObjs[$archivename]->deleted != $bitfield ) { - $count++; - - $this->dbw->begin(); - $this->updateOldFiles( $filesObjs[$archivename], $bitfield ); - // If this image is currently hidden... - if( $filesObjs[$archivename]->deleted & File::DELETED_FILE ) { - if( $bitfield & File::DELETED_FILE ) { - # Leave it alone if we are not changing this... - $set[]=$archivename; - $transaction = true; - } else { - # We are moving this out - $transaction = $this->makeOldImagePublic( $filesObjs[$archivename] ); - $set[]=$transaction; - } - // Is it just now becoming hidden? - } else if( $bitfield & File::DELETED_FILE ) { - $transaction = $this->makeOldImagePrivate( $filesObjs[$archivename] ); - $set[]=$transaction; - } else { - $set[]=$timestamp; - } - // If our file operations fail, then revert back the db - if( $transaction==false ) { - $this->dbw->rollback(); - return false; - } - $this->dbw->commit(); - } - } + public function newItem( $row ) { + return new RevDel_RevisionItem( $this, $row ); + } - // Log if something was changed - if( $count > 0 ) { - $this->updateLog( $title, $count, $bitfield, $filesObjs[$archivename]->deleted, - $comment, $title, 'oldimage', $set ); - # Purge page/history - $file = wfLocalFile( $title ); - $file->purgeCache(); - $file->purgeHistory(); - # Invalidate cache for all pages using this file - $update = new HTMLCacheUpdate( $title, 'imagelinks' ); - $update->doUpdate(); - } - // Where all revs allowed to be set? - if( !$userAllowedAll ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return false; + public function getCurrent() { + if ( is_null( $this->currentRevId ) ) { + $dbw = wfGetDB( DB_MASTER ); + $this->currentRevId = $dbw->selectField( + 'page', 'page_latest', $this->title->pageCond(), __METHOD__ ); } - - return $success; + return $this->currentRevId; } - /** - * @param $title, the page these events apply to - * @param array $items list of revision ID numbers - * @param int $bitfield new rev_deleted value - * @param string $comment Comment for log records - */ - function setArchFileVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; + public function getSuppressBit() { + return Revision::DELETED_RESTRICTED; + } - $userAllowedAll = $success = true; - $count = 0; - $Id_set = array(); + public function doPreCommitUpdates() { + $this->title->invalidateCache(); + return Status::newGood(); + } - // Run through and pull all our data in one query - foreach( $items as $id ) { - $where[] = intval($id); - } - $result = $this->dbw->select( 'filearchive', '*', - array( 'fa_name' => $title->getDBKey(), - 'fa_id' => $where ), - __METHOD__ ); - while( $row = $this->dbw->fetchObject( $result ) ) { - $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row ); - } - // To work! - foreach( $items as $fileid ) { - if( !isset($filesObjs[$fileid]) ) { - $success = false; - continue; // Must exist - } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) { - $userAllowedAll=false; - continue; - } - // Which revisions did we change anything about? - if( $filesObjs[$fileid]->deleted != $bitfield ) { - $Id_set[]=$fileid; - $count++; + public function doPostCommitUpdates() { + $this->title->purgeSquid(); + // Extensions that require referencing previous revisions may need this + wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$this->title ) ); + return Status::newGood(); + } +} - $this->updateArchFiles( $filesObjs[$fileid], $bitfield ); - } - } - // Log if something was changed - if( $count > 0 ) { - $this->updateLog( $title, $count, $bitfield, $comment, - $filesObjs[$fileid]->deleted, $title, 'fileid', $Id_set ); - } - // Where all revs allowed to be set? - if( !$userAllowedAll ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return false; - } +/** + * Item class for a revision table row + */ +class RevDel_RevisionItem extends RevDel_Item { + var $revision; - return $success; + public function __construct( $list, $row ) { + parent::__construct( $list, $row ); + $this->revision = new Revision( $row ); } - /** - * @param $title, the log page these events apply to - * @param array $items list of log ID numbers - * @param int $bitfield new log_deleted value - * @param string $comment Comment for log records - */ - function setEventVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; + public function canView() { + return $this->revision->userCan( Revision::DELETED_RESTRICTED ); + } + + public function canViewContent() { + return $this->revision->userCan( Revision::DELETED_TEXT ); + } - $userAllowedAll = $success = true; - $count = 0; - $log_Ids = array(); + public function getBits() { + return $this->revision->mDeleted; + } - // Run through and pull all our data in one query - foreach( $items as $logid ) { - $where[] = intval($logid); + public function setBits( $bits ) { + $dbw = wfGetDB( DB_MASTER ); + // Update revision table + $dbw->update( 'revision', + array( 'rev_deleted' => $bits ), + array( + 'rev_id' => $this->revision->getId(), + 'rev_page' => $this->revision->getPage(), + 'rev_deleted' => $this->getBits() + ), + __METHOD__ + ); + if ( !$dbw->affectedRows() ) { + // Concurrent fail! + return false; } - list($log,$logtype) = explode( '/',$title->getDBKey(), 2 ); - $result = $this->dbw->select( 'logging', '*', + // Update recentchanges table + $dbw->update( 'recentchanges', + array( + 'rc_deleted' => $bits, + 'rc_patrolled' => 1 + ), array( - 'log_type' => $logtype, - 'log_id' => $where ), - __METHOD__ ); - while( $row = $this->dbw->fetchObject( $result ) ) { - $logRows[$row->log_id] = $row; - } - // To work! - foreach( $items as $logid ) { - if( !isset($logRows[$logid]) ) { - $success = false; - continue; // Must exist - } else if( !LogEventsList::userCan($logRows[$logid], LogPage::DELETED_RESTRICTED) - || $logRows[$logid]->log_type == 'suppress' ) { - // Don't hide from oversight log!!! - $userAllowedAll=false; - continue; - } - // Which logs did we change anything about? - if( $logRows[$logid]->log_deleted != $bitfield ) { - $log_Ids[]=$logid; - $count++; + 'rc_this_oldid' => $this->revision->getId(), // condition + // non-unique timestamp index + 'rc_timestamp' => $dbw->timestamp( $this->revision->getTimestamp() ), + ), + __METHOD__ + ); + return true; + } - $this->updateLogs( $logRows[$logid], $bitfield ); - $this->updateRecentChangesLog( $logRows[$logid], $bitfield, true ); - } - } - // Don't log or touch if nothing changed - if( $count > 0 ) { - $this->updateLog( $title, $count, $bitfield, $logRows[$logid]->log_deleted, - $comment, $title, 'logid', $log_Ids ); - } - // Were all revs allowed to be set? - if( !$userAllowedAll ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return false; - } + public function isDeleted() { + return $this->revision->isDeleted( Revision::DELETED_TEXT ); + } - return $success; + public function isHideCurrentOp( $newBits ) { + return ( $newBits & Revision::DELETED_TEXT ) + && $this->list->getCurrent() == $this->getId(); } /** - * Moves an image to a safe private location - * Caller is responsible for clearing caches - * @param File $oimage - * @returns mixed, timestamp string on success, false on failure + * Get the HTML link to the revision text. + * Overridden by RevDel_ArchiveItem. */ - function makeOldImagePrivate( $oimage ) { - $transaction = new FSTransaction(); - if( !FileStore::lock() ) { - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); - return false; - } - $oldpath = $oimage->getArchivePath() . DIRECTORY_SEPARATOR . $oimage->archive_name; - // Dupe the file into the file store - if( file_exists( $oldpath ) ) { - // Is our directory configured? - if( $store = FileStore::get( 'deleted' ) ) { - if( !$oimage->sha1 ) { - $oimage->upgradeRow(); // sha1 may be missing - } - $key = $oimage->sha1 . '.' . $oimage->getExtension(); - $transaction->add( $store->insert( $key, $oldpath, FileStore::DELETE_ORIGINAL ) ); - } else { - $group = null; - $key = null; - $transaction = false; // Return an error and do nothing - } + protected function getRevisionLink() { + global $wgLang; + $date = $wgLang->timeanddate( $this->revision->getTimestamp(), true ); + if ( $this->isDeleted() && !$this->canViewContent() ) { + return $date; + } + return $this->special->skin->link( + $this->list->title, + $date, + array(), + array( + 'oldid' => $this->revision->getId(), + 'unhide' => 1 + ) + ); + } + + /** + * Get the HTML link to the diff. + * Overridden by RevDel_ArchiveItem + */ + protected function getDiffLink() { + if ( $this->isDeleted() && !$this->canViewContent() ) { + return wfMsgHtml('diff'); } else { - wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" ); - $group = null; - $key = ''; - $transaction = new FSTransaction(); // empty + return + $this->special->skin->link( + $this->list->title, + wfMsgHtml('diff'), + array(), + array( + 'diff' => $this->revision->getId(), + 'oldid' => 'prev', + 'unhide' => 1 + ), + array( + 'known', + 'noclasses' + ) + ); } + } - if( $transaction === false ) { - // Fail to restore? - wfDebug( __METHOD__.": import to file store failed, aborting\n" ); - throw new MWException( "Could not archive and delete file $oldpath" ); - return false; + public function getHTML() { + $difflink = $this->getDiffLink(); + $revlink = $this->getRevisionLink(); + $userlink = $this->special->skin->revUserLink( $this->revision ); + $comment = $this->special->skin->revComment( $this->revision ); + if ( $this->isDeleted() ) { + $revlink = "<span class=\"history-deleted\">$revlink</span>"; } + return "<li>($difflink) $revlink $userlink $comment</li>"; + } +} - wfDebug( __METHOD__.": set db items, applying file transactions\n" ); - $transaction->commit(); - FileStore::unlock(); +/** + * List for archive table items, i.e. revisions deleted via action=delete + */ +class RevDel_ArchiveList extends RevDel_RevisionList { + var $type = 'archive'; + var $idField = 'ar_timestamp'; + var $dateField = 'ar_timestamp'; + var $authorIdField = 'ar_user'; + var $authorNameField = 'ar_user_text'; + + public function doQuery( $db ) { + $timestamps = array(); + foreach ( $this->ids as $id ) { + $timestamps[] = $db->timestamp( $id ); + } + return $db->select( 'archive', '*', + array( + 'ar_namespace' => $this->title->getNamespace(), + 'ar_title' => $this->title->getDBkey(), + 'ar_timestamp' => $timestamps + ), + __METHOD__, + array( 'ORDER BY' => 'ar_timestamp DESC' ) + ); + } - $m = explode('!',$oimage->archive_name,2); - $timestamp = $m[0]; + public function newItem( $row ) { + return new RevDel_ArchiveItem( $this, $row ); + } - return $timestamp; + public function doPreCommitUpdates() { + return Status::newGood(); } - /** - * Moves an image from a safe private location - * Caller is responsible for clearing caches - * @param File $oimage - * @returns mixed, string timestamp on success, false on failure - */ - function makeOldImagePublic( $oimage ) { - $transaction = new FSTransaction(); - if( !FileStore::lock() ) { - wfDebug( __METHOD__." could not acquire filestore lock\n" ); - return false; - } + public function doPostCommitUpdates() { + return Status::newGood(); + } +} - $store = FileStore::get( 'deleted' ); - if( !$store ) { - wfDebug( __METHOD__.": skipping row with no file.\n" ); - return false; +/** + * Item class for a archive table row + */ +class RevDel_ArchiveItem extends RevDel_RevisionItem { + public function __construct( $list, $row ) { + RevDel_Item::__construct( $list, $row ); + $this->revision = Revision::newFromArchiveRow( $row, + array( 'page' => $this->list->title->getArticleId() ) ); + } + + public function getId() { + # Convert DB timestamp to MW timestamp + return $this->revision->getTimestamp(); + } + + public function setBits( $bits ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'archive', + array( 'ar_deleted' => $bits ), + array( 'ar_namespace' => $this->list->title->getNamespace(), + 'ar_title' => $this->list->title->getDBkey(), + // use timestamp for index + 'ar_timestamp' => $this->row->ar_timestamp, + 'ar_rev_id' => $this->row->ar_rev_id, + 'ar_deleted' => $this->getBits() + ), + __METHOD__ ); + return (bool)$dbw->affectedRows(); + } + + protected function getRevisionLink() { + global $wgLang; + $undelete = SpecialPage::getTitleFor( 'Undelete' ); + $date = $wgLang->timeanddate( $this->revision->getTimestamp(), true ); + if ( $this->isDeleted() && !$this->canViewContent() ) { + return $date; } + return $this->special->skin->link( $undelete, $date, array(), + array( + 'target' => $this->list->title->getPrefixedText(), + 'timestamp' => $this->revision->getTimestamp() + ) ); + } - $key = $oimage->sha1.'.'.$oimage->getExtension(); - $destDir = $oimage->getArchivePath(); - if( !is_dir( $destDir ) ) { - wfMkdirParents( $destDir ); - } - $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name; - // Check if any other stored revisions use this file; - // if so, we shouldn't remove the file from the hidden - // archives so they will still work. Check hidden files first. - $useCount = $this->dbw->selectField( 'oldimage', '1', - array( 'oi_sha1' => $oimage->sha1, - 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ), - __METHOD__, array( 'FOR UPDATE' ) ); - // Check the rest of the deleted archives too. - // (these are the ones that don't show in the image history) - if( !$useCount ) { - $useCount = $this->dbw->selectField( 'filearchive', '1', - array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ), - __METHOD__, array( 'FOR UPDATE' ) ); - } - - if( $useCount == 0 ) { - wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" ); - $flags = FileStore::DELETE_ORIGINAL; - } else { - $flags = 0; + protected function getDiffLink() { + if ( $this->isDeleted() && !$this->canViewContent() ) { + return wfMsgHtml( 'diff' ); } - $transaction->add( $store->export( $key, $destPath, $flags ) ); + $undelete = SpecialPage::getTitleFor( 'Undelete' ); + return $this->special->skin->link( $undelete, wfMsgHtml('diff'), array(), + array( + 'target' => $this->list->title->getPrefixedText(), + 'diff' => 'prev', + 'timestamp' => $this->revision->getTimestamp() + ) ); + } +} - wfDebug( __METHOD__.": set db items, applying file transactions\n" ); - $transaction->commit(); - FileStore::unlock(); +/** + * List for oldimage table items + */ +class RevDel_FileList extends RevDel_List { + var $type = 'oldimage'; + var $idField = 'oi_archive_name'; + var $dateField = 'oi_timestamp'; + var $authorIdField = 'oi_user'; + var $authorNameField = 'oi_user_text'; + var $storeBatch, $deleteBatch, $cleanupBatch; + + public function doQuery( $db ) { + $archiveName = array(); + foreach( $this->ids as $timestamp ) { + $archiveNames[] = $timestamp . '!' . $this->title->getDBkey(); + } + return $db->select( 'oldimage', '*', + array( + 'oi_name' => $this->title->getDBkey(), + 'oi_archive_name' => $archiveNames + ), + __METHOD__, + array( 'ORDER BY' => 'oi_timestamp DESC' ) + ); + } - $m = explode('!',$oimage->archive_name,2); - $timestamp = $m[0]; + public function newItem( $row ) { + return new RevDel_FileItem( $this, $row ); + } - return $timestamp; + public function clearFileOps() { + $this->deleteBatch = array(); + $this->storeBatch = array(); + $this->cleanupBatch = array(); } - /** - * Update the revision's rev_deleted field - * @param Revision $rev - * @param int $bitfield new rev_deleted bitfield value - */ - function updateRevision( $rev, $bitfield ) { - $this->dbw->update( 'revision', - array( 'rev_deleted' => $bitfield ), - array( 'rev_id' => $rev->getId(), - 'rev_page' => $rev->getPage() ), - __METHOD__ ); + public function doPreCommitUpdates() { + $status = Status::newGood(); + $repo = RepoGroup::singleton()->getLocalRepo(); + if ( $this->storeBatch ) { + $status->merge( $repo->storeBatch( $this->storeBatch, FileRepo::OVERWRITE_SAME ) ); + } + if ( !$status->isOK() ) { + return $status; + } + if ( $this->deleteBatch ) { + $status->merge( $repo->deleteBatch( $this->deleteBatch ) ); + } + if ( !$status->isOK() ) { + // Running cleanupDeletedBatch() after a failed storeBatch() with the DB already + // modified (but destined for rollback) causes data loss + return $status; + } + if ( $this->cleanupBatch ) { + $status->merge( $repo->cleanupDeletedBatch( $this->cleanupBatch ) ); + } + return $status; } - /** - * Update the revision's rev_deleted field - * @param Revision $rev - * @param Title $title - * @param int $bitfield new rev_deleted bitfield value - */ - function updateArchive( $rev, $title, $bitfield ) { - $this->dbw->update( 'archive', - array( 'ar_deleted' => $bitfield ), - array( 'ar_namespace' => $title->getNamespace(), - 'ar_title' => $title->getDBKey(), - 'ar_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ), - 'ar_rev_id' => $rev->getId() ), - __METHOD__ ); + public function doPostCommitUpdates() { + $file = wfLocalFile( $this->title ); + $file->purgeCache(); + $file->purgeDescription(); + return Status::newGood(); } - /** - * Update the images's oi_deleted field - * @param File $file - * @param int $bitfield new rev_deleted bitfield value - */ - function updateOldFiles( $file, $bitfield ) { - $this->dbw->update( 'oldimage', - array( 'oi_deleted' => $bitfield ), - array( 'oi_name' => $file->getName(), - 'oi_timestamp' => $this->dbw->timestamp( $file->getTimestamp() ) ), - __METHOD__ ); + public function getSuppressBit() { + return File::DELETED_RESTRICTED; } +} - /** - * Update the images's fa_deleted field - * @param ArchivedFile $file - * @param int $bitfield new rev_deleted bitfield value - */ - function updateArchFiles( $file, $bitfield ) { - $this->dbw->update( 'filearchive', - array( 'fa_deleted' => $bitfield ), - array( 'fa_id' => $file->getId() ), - __METHOD__ ); +/** + * Item class for an oldimage table row + */ +class RevDel_FileItem extends RevDel_Item { + var $file; + + public function __construct( $list, $row ) { + parent::__construct( $list, $row ); + $this->file = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row ); } - /** - * Update the logging log_deleted field - * @param Row $row - * @param int $bitfield new rev_deleted bitfield value - */ - function updateLogs( $row, $bitfield ) { - $this->dbw->update( 'logging', - array( 'log_deleted' => $bitfield ), - array( 'log_id' => $row->log_id ), - __METHOD__ ); + public function getId() { + $parts = explode( '!', $this->row->oi_archive_name ); + return $parts[0]; } - /** - * Update the revision's recentchanges record if fields have been hidden - * @param Revision $rev - * @param int $bitfield new rev_deleted bitfield value - */ - function updateRecentChangesEdits( $rev, $bitfield ) { - $this->dbw->update( 'recentchanges', - array( 'rc_deleted' => $bitfield, - 'rc_patrolled' => 1 ), - array( 'rc_this_oldid' => $rev->getId(), - 'rc_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ) ), - __METHOD__ ); + public function canView() { + return $this->file->userCan( File::DELETED_RESTRICTED ); + } + + public function canViewContent() { + return $this->file->userCan( File::DELETED_FILE ); } - /** - * Update the revision's recentchanges record if fields have been hidden - * @param Row $row - * @param int $bitfield new rev_deleted bitfield value - */ - function updateRecentChangesLog( $row, $bitfield ) { - $this->dbw->update( 'recentchanges', - array( 'rc_deleted' => $bitfield, - 'rc_patrolled' => 1 ), - array( 'rc_logid' => $row->log_id, - 'rc_timestamp' => $row->log_timestamp ), - __METHOD__ ); + public function getBits() { + return $this->file->getVisibility(); + } + + public function setBits( $bits ) { + # Queue the file op + # FIXME: move to LocalFile.php + if ( $this->isDeleted() ) { + if ( $bits & File::DELETED_FILE ) { + # Still deleted + } else { + # Newly undeleted + $key = $this->file->getStorageKey(); + $srcRel = $this->file->repo->getDeletedHashPath( $key ) . $key; + $this->list->storeBatch[] = array( + $this->file->repo->getVirtualUrl( 'deleted' ) . '/' . $srcRel, + 'public', + $this->file->getRel() + ); + $this->list->cleanupBatch[] = $key; + } + } elseif ( $bits & File::DELETED_FILE ) { + # Newly deleted + $key = $this->file->getStorageKey(); + $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key; + $this->list->deleteBatch[] = array( $this->file->getRel(), $dstRel ); + } + + # Do the database operations + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'oldimage', + array( 'oi_deleted' => $bits ), + array( + 'oi_name' => $this->row->oi_name, + 'oi_timestamp' => $this->row->oi_timestamp, + 'oi_deleted' => $this->getBits() + ), + __METHOD__ + ); + return (bool)$dbw->affectedRows(); + } + + public function isDeleted() { + return $this->file->isDeleted( File::DELETED_FILE ); } /** - * Touch the page's cache invalidation timestamp; this forces cached - * history views to refresh, so any newly hidden or shown fields will - * update properly. - * @param Title $title + * Get the link to the file. + * Overridden by RevDel_ArchivedFileItem. */ - function updatePage( $title ) { - $title->invalidateCache(); - $title->purgeSquid(); - $title->touchLinks(); - // Extensions that require referencing previous revisions may need this - wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) ); + protected function getLink() { + global $wgLang, $wgUser; + $date = $wgLang->timeanddate( $this->file->getTimestamp(), true ); + if ( $this->isDeleted() ) { + # Hidden files... + if ( !$this->canViewContent() ) { + $link = $date; + } else { + $link = $this->special->skin->link( + $this->special->getTitle(), + $date, array(), + array( + 'target' => $this->list->title->getPrefixedText(), + 'file' => $this->file->getArchiveName(), + 'token' => $wgUser->editToken( $this->file->getArchiveName() ) + ) + ); + } + return '<span class="history-deleted">' . $link . '</span>'; + } else { + # Regular files... + $url = $this->file->getUrl(); + return Xml::element( 'a', array( 'href' => $this->file->getUrl() ), $date ); + } } - /** - * Checks for a change in the bitfield for a certain option and updates the - * provided array accordingly. - * - * @param String $desc Description to add to the array if the option was - * enabled / disabled. - * @param int $field The bitmask describing the single option. - * @param int $diff The xor of the old and new bitfields. - * @param array $arr The array to update. + * Generate a user tool link cluster if the current user is allowed to view it + * @return string HTML */ - function checkItem ( $desc, $field, $diff, $new, &$arr ) { - if ( $diff & $field ) { - $arr [ ( $new & $field ) ? 0 : 1 ][] = $desc; + protected function getUserTools() { + if( $this->file->userCan( Revision::DELETED_USER ) ) { + $link = $this->special->skin->userLink( $this->file->user, $this->file->user_text ) . + $this->special->skin->userToolLinks( $this->file->user, $this->file->user_text ); + } else { + $link = wfMsgHtml( 'rev-deleted-user' ); } + if( $this->file->isDeleted( Revision::DELETED_USER ) ) { + return '<span class="history-deleted">' . $link . '</span>'; + } + return $link; } /** - * Gets an array describing the changes made to the visibilit of the revision. - * If the resulting array is $arr, then $arr[0] will contain an array of strings - * describing the items that were hidden, $arr[2] will contain an array of strings - * describing the items that were unhidden, and $arr[3] will contain an array with - * a single string, which can be one of "applied restrictions to sysops", - * "removed restrictions from sysops", or null. + * Wrap and format the file's comment block, if the current + * user is allowed to view it. * - * @param int $n The new bitfield. - * @param int $o The old bitfield. - * @return An array as described above. + * @return string HTML */ - function getChanges ( $n, $o ) { - $diff = $n ^ $o; - $ret = array ( 0 => array(), 1 => array(), 2 => array() ); + protected function getComment() { + if( $this->file->userCan( File::DELETED_COMMENT ) ) { + $block = $this->special->skin->commentBlock( $this->file->description ); + } else { + $block = ' ' . wfMsgHtml( 'rev-deleted-comment' ); + } + if( $this->file->isDeleted( File::DELETED_COMMENT ) ) { + return "<span class=\"history-deleted\">$block</span>"; + } + return $block; + } - $this->checkItem ( wfMsgForContent ( 'revdelete-content' ), - Revision::DELETED_TEXT, $diff, $n, $ret ); - $this->checkItem ( wfMsgForContent ( 'revdelete-summary' ), - Revision::DELETED_COMMENT, $diff, $n, $ret ); - $this->checkItem ( wfMsgForContent ( 'revdelete-uname' ), - Revision::DELETED_USER, $diff, $n, $ret ); + public function getHTML() { + global $wgLang; + $data = + wfMsg( + 'widthheight', + $wgLang->formatNum( $this->file->getWidth() ), + $wgLang->formatNum( $this->file->getHeight() ) + ) . + ' (' . + wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $this->file->getSize() ) ) . + ')'; + $pageLink = $this->getLink(); + + return '<li>' . $this->getLink() . ' ' . $this->getUserTools() . ' ' . + $data . ' ' . $this->getComment(). '</li>'; + } +} - // Restriction application to sysops - if ( $diff & Revision::DELETED_RESTRICTED ) { - if ( $n & Revision::DELETED_RESTRICTED ) - $ret[2][] = wfMsgForContent ( 'revdelete-restricted' ); - else - $ret[2][] = wfMsgForContent ( 'revdelete-unrestricted' ); - } +/** + * List for filearchive table items + */ +class RevDel_ArchivedFileList extends RevDel_FileList { + var $type = 'filearchive'; + var $idField = 'fa_id'; + var $dateField = 'fa_timestamp'; + var $authorIdField = 'fa_user'; + var $authorNameField = 'fa_user_text'; + + public function doQuery( $db ) { + $ids = array_map( 'intval', $this->ids ); + return $db->select( 'filearchive', '*', + array( + 'fa_name' => $this->title->getDBkey(), + 'fa_id' => $ids + ), + __METHOD__, + array( 'ORDER BY' => 'fa_id DESC' ) + ); + } - return $ret; + public function newItem( $row ) { + return new RevDel_ArchivedFileItem( $this, $row ); } +} - /** - * Gets a log message to describe the given revision visibility change. This - * message will be of the form "[hid {content, edit summary, username}]; - * [unhid {...}][applied restrictions to sysops] for $count revisions: $comment". - * - * @param int $count The number of effected revisions. - * @param int $nbitfield The new bitfield for the revision. - * @param int $obitfield The old bitfield for the revision. - * @param string $comment The comment associated with the change. - * @param bool $isForLog - */ - function getLogMessage ( $count, $nbitfield, $obitfield, $comment, $isForLog = false ) { - global $wgContLang; +/** + * Item class for a filearchive table row + */ +class RevDel_ArchivedFileItem extends RevDel_FileItem { + public function __construct( $list, $row ) { + RevDel_Item::__construct( $list, $row ); + $this->file = ArchivedFile::newFromRow( $row ); + } - $s = ''; - $changes = $this->getChanges( $nbitfield, $obitfield ); + public function getId() { + return $this->row->fa_id; + } + + public function setBits( $bits ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'filearchive', + array( 'fa_deleted' => $bits ), + array( + 'fa_id' => $this->row->fa_id, + 'fa_deleted' => $this->getBits(), + ), + __METHOD__ + ); + return (bool)$dbw->affectedRows(); + } - if ( count ( $changes[0] ) ) { - $s .= wfMsgForContent ( 'revdelete-hid', implode ( ', ', $changes[0] ) ); + protected function getLink() { + global $wgLang, $wgUser; + $date = $wgLang->timeanddate( $this->file->getTimestamp(), true ); + $undelete = SpecialPage::getTitleFor( 'Undelete' ); + $key = $this->file->getKey(); + # Hidden files... + if( !$this->canViewContent() ) { + $link = $date; + } else { + $link = $this->special->skin->link( $undelete, $date, array(), + array( + 'target' => $this->list->title->getPrefixedText(), + 'file' => $key, + 'token' => $wgUser->editToken( $key ) + ) + ); + } + if( $this->isDeleted() ) { + $link = '<span class="history-deleted">' . $link . '</span>'; } + return $link; + } +} - if ( count ( $changes[1] ) ) { - if ($s) $s .= '; '; +/** + * List for logging table items + */ +class RevDel_LogList extends RevDel_List { + var $type = 'logging'; + var $idField = 'log_id'; + var $dateField = 'log_timestamp'; + var $authorIdField = 'log_user'; + var $authorNameField = 'log_user_text'; + + public function doQuery( $db ) { + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); + $ids = array_map( 'intval', $this->ids ); + return $db->select( 'logging', '*', + array( 'log_id' => $ids ), + __METHOD__, + array( 'ORDER BY' => 'log_id DESC' ) + ); + } - $s .= wfMsgForContent ( 'revdelete-unhid', implode ( ', ', $changes[1] ) ); - } + public function newItem( $row ) { + return new RevDel_LogItem( $this, $row ); + } - if ( count ( $changes[2] )) { - if ($s) - $s .= ' (' . $changes[2][0] . ')'; - else - $s = $changes[2][0]; - } + public function getSuppressBit() { + return Revision::DELETED_RESTRICTED; + } - $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message'; - $ret = wfMsgExt ( $msg, array( 'parsemag', 'content' ), - $s, $wgContLang->formatNum( $count ) ); + public function getLogAction() { + return 'event'; + } - if ( $comment ) - $ret .= ": $comment"; + public function getLogParams( $params ) { + return array( + implode( ',', $params['ids'] ), + "ofield={$params['oldBits']}", + "nfield={$params['newBits']}" + ); + } +} - return $ret; +/** + * Item class for a logging table row + */ +class RevDel_LogItem extends RevDel_Item { + public function canView() { + return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED ); + } + + public function canViewContent() { + return true; // none + } + public function getBits() { + return $this->row->log_deleted; } - /** - * Record a log entry on the action - * @param Title $title, page where item was removed from - * @param int $count the number of revisions altered for this page - * @param int $nbitfield the new _deleted value - * @param int $obitfield the old _deleted value - * @param string $comment - * @param Title $target, the relevant page - * @param string $param, URL param - * @param Array $items - */ - function updateLog( $title, $count, $nbitfield, $obitfield, $comment, $target, $param, $items = array() ) { - // Put things hidden from sysops in the oversight log - $logtype = ( ($nbitfield | $obitfield) & Revision::DELETED_RESTRICTED ) ? 'suppress' : 'delete'; - $log = new LogPage( $logtype ); + public function setBits( $bits ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'recentchanges', + array( + 'rc_deleted' => $bits, + 'rc_patrolled' => 1 + ), + array( + 'rc_logid' => $this->row->log_id, + 'rc_timestamp' => $this->row->log_timestamp // index + ), + __METHOD__ + ); + $dbw->update( 'logging', + array( 'log_deleted' => $bits ), + array( + 'log_id' => $this->row->log_id, + 'log_deleted' => $this->getBits() + ), + __METHOD__ + ); + return (bool)$dbw->affectedRows(); + } + + public function getHTML() { + global $wgLang; - $reason = $this->getLogMessage ( $count, $nbitfield, $obitfield, $comment, $param == 'logid' ); + $date = htmlspecialchars( $wgLang->timeanddate( $this->row->log_timestamp ) ); + $paramArray = LogPage::extractParams( $this->row->log_params ); + $title = Title::makeTitle( $this->row->log_namespace, $this->row->log_title ); - if( $param == 'logid' ) { - $params = array( implode( ',', $items) ); - $log->addEntry( 'event', $title, $reason, $params ); + // Log link for this page + $loglink = $this->special->skin->link( + SpecialPage::getTitleFor( 'Log' ), + wfMsgHtml( 'log' ), + array(), + array( 'page' => $title->getPrefixedText() ) + ); + // Action text + if( !$this->canView() ) { + $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; } else { - // Add params for effected page and ids - $params = array( $param, implode( ',', $items) ); - $log->addEntry( 'revision', $title, $reason, $params ); + $action = LogPage::actionText( $this->row->log_type, $this->row->log_action, $title, + $this->special->skin, $paramArray, true, true ); + if( $this->row->log_deleted & LogPage::DELETED_ACTION ) + $action = '<span class="history-deleted">' . $action . '</span>'; } + // User links + $userLink = $this->special->skin->userLink( $this->row->log_user, + User::WhoIs( $this->row->log_user ) ); + if( LogEventsList::isDeleted($this->row,LogPage::DELETED_USER) ) { + $userLink = '<span class="history-deleted">' . $userLink . '</span>'; + } + // Comment + $comment = $wgLang->getDirMark() . $this->special->skin->commentBlock( $this->row->log_comment ); + if( LogEventsList::isDeleted($this->row,LogPage::DELETED_COMMENT) ) { + $comment = '<span class="history-deleted">' . $comment . '</span>'; + } + return "<li>($loglink) $date $userLink $action $comment</li>"; } } diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index cb783819..da054e02 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -29,16 +29,15 @@ * @param $par String: (default '') */ function wfSpecialSearch( $par = '' ) { - global $wgRequest, $wgUser, $wgUseOldSearchUI; + global $wgRequest, $wgUser; // Strip underscores from title parameter; most of the time we'll want // text form here. But don't strip underscores from actual text params! $titleParam = str_replace( '_', ' ', $par ); // Fetch the search term $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $titleParam ) ); - $class = $wgUseOldSearchUI ? 'SpecialSearchOld' : 'SpecialSearch'; - $searchPage = new $class( $wgRequest, $wgUser ); - if( $wgRequest->getVal( 'fulltext' ) - || !is_null( $wgRequest->getVal( 'offset' )) + $searchPage = new SpecialSearch( $wgRequest, $wgUser ); + if( $wgRequest->getVal( 'fulltext' ) + || !is_null( $wgRequest->getVal( 'offset' )) || !is_null( $wgRequest->getVal( 'searchx' )) ) { $searchPage->showResults( $search ); @@ -74,7 +73,7 @@ class SpecialSearch { $this->active = 'advanced'; $this->sk = $user->getSkin(); $this->didYouMeanHtml = ''; # html of did you mean... link - $this->fulltext = $request->getVal('fulltext'); + $this->fulltext = $request->getVal('fulltext'); } /** @@ -103,7 +102,7 @@ class SpecialSearch { wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) ); # If the feature is enabled, go straight to the edit page if( $wgGoToEdit ) { - $wgOut->redirect( $t->getFullURL( 'action=edit' ) ); + $wgOut->redirect( $t->getFullURL( array( 'action' => 'edit' ) ) ); return; } } @@ -114,11 +113,11 @@ class SpecialSearch { * @param string $term */ public function showResults( $term ) { - global $wgOut, $wgUser, $wgDisableTextSearch, $wgContLang; + global $wgOut, $wgUser, $wgDisableTextSearch, $wgContLang, $wgScript; wfProfileIn( __METHOD__ ); - + $sk = $wgUser->getSkin(); - + $this->searchEngine = SearchEngine::create(); $search =& $this->searchEngine; $search->setLimitOffset( $this->limit, $this->offset ); @@ -126,9 +125,9 @@ class SpecialSearch { $search->showRedirects = $this->searchRedirects; $search->prefix = $this->mPrefix; $term = $search->transformSearchTerm($term); - + $this->setupPage( $term ); - + if( $wgDisableTextSearch ) { global $wgSearchForwardUrl; if( $wgSearchForwardUrl ) { @@ -152,10 +151,10 @@ class SpecialSearch { wfProfileOut( __METHOD__ ); return; } - + $t = Title::newFromText( $term ); - - // fetch search results + + // fetch search results $rewritten = $search->replacePrefixes($term); $titleMatches = $search->searchTitle( $rewritten ); @@ -165,95 +164,116 @@ class SpecialSearch { // did you mean... suggestions if( $textMatches && $textMatches->hasSuggestion() ) { $st = SpecialPage::getTitleFor( 'Search' ); + # mirror Go/Search behaviour of original request .. $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() ); - if($this->fulltext != NULL) - $didYouMeanParams['fulltext'] = $this->fulltext; - $stParams = wfArrayToCGI( + + if($this->fulltext != null) + $didYouMeanParams['fulltext'] = $this->fulltext; + + $stParams = array_merge( $didYouMeanParams, $this->powerSearchOptions() ); - $suggestLink = $sk->makeKnownLinkObj( $st, - $textMatches->getSuggestionSnippet(), - $stParams ); + + $suggestionSnippet = $textMatches->getSuggestionSnippet(); + + if( $suggestionSnippet == '' ) + $suggestionSnippet = null; + + $suggestLink = $sk->linkKnown( + $st, + $suggestionSnippet, + array(), + $stParams + ); $this->didYouMeanHtml = '<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>'; } - - // start rendering the page - $wgOut->addHtml( - Xml::openElement( 'table', array( 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) . + // start rendering the page + $wgOut->addHtml( + Xml::openElement( + 'form', + array( + 'id' => ( $this->searchAdvanced ? 'powersearch' : 'search' ), + 'method' => 'get', + 'action' => $wgScript + ) + ) + ); + $wgOut->addHtml( + Xml::openElement( 'table', array( 'id'=>'mw-search-top-table', 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) . Xml::openElement( 'tr' ) . Xml::openElement( 'td' ) . "\n" . - ( $this->searchAdvanced ? $this->powerSearchBox( $term ) : $this->shortDialog( $term ) ) . + $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 ) { $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" ); wfProfileOut( __METHOD__ ); return; } - + $filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':'; - if( '' === trim( $term ) || $filePrefix === trim( $term ) ) { - $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() ); + if( trim( $term ) === '' || $filePrefix === trim( $term ) ) { + $wgOut->addHTML( $this->searchFocus() ); + $wgOut->addHTML( $this->formHeader($term, 0, 0)); + if( $this->searchAdvanced ) { + $wgOut->addHTML( $this->powerSearchBox( $term ) ); + } + $wgOut->addHTML( '</form>' ); // Empty query -- straight view of search form wfProfileOut( __METHOD__ ); return; } - // show direct page/create link - if( !is_null($t) ) { - if( !$t->exists() ) { - $wgOut->addWikiMsg( 'searchmenu-new', wfEscapeWikiText( $t->getPrefixedText() ) ); - } else { - $wgOut->addWikiMsg( 'searchmenu-exists', wfEscapeWikiText( $t->getPrefixedText() ) ); - } - } - // Get number of results - $titleMatchesSQL = $titleMatches ? $titleMatches->numRows() : 0; - $textMatchesSQL = $textMatches ? $textMatches->numRows() : 0; + $titleMatchesNum = $titleMatches ? $titleMatches->numRows() : 0; + $textMatchesNum = $textMatches ? $textMatches->numRows() : 0; // Total initial query matches (possible false positives) - $numSQL = $titleMatchesSQL + $textMatchesSQL; + $num = $titleMatchesNum + $textMatchesNum; + // Get total actual results (after second filtering, if any) $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ? - $titleMatches->getTotalHits() : $titleMatchesSQL; + $titleMatches->getTotalHits() : $titleMatchesNum; $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ? - $textMatches->getTotalHits() : $textMatchesSQL; - $totalRes = $numTitleMatches + $numTextMatches; - + $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 - if( $numSQL > 0 ) { - if( $numSQL > 0 ) { - $top = wfMsgExt('showingresultstotal', array( 'parseinline' ), - $this->offset+1, $this->offset+$numSQL, $totalRes, $numSQL ); - } elseif( $numSQL >= $this->limit ) { - $top = wfShowingResults( $this->offset, $this->limit ); - } else { - $top = wfShowingResultsNum( $this->offset, $this->limit, $numSQL ); - } - $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" ); + $wgOut->addHTML( $this->formHeader($term, $num, $totalRes)); + if( $this->searchAdvanced ) { + $wgOut->addHTML( $this->powerSearchBox( $term ) ); } + + $wgOut->addHtml( Xml::closeElement( 'form' ) ); + $wgOut->addHtml( "<div class='searchresults'>" ); // prev/next links - if( $numSQL || $this->offset ) { + if( $num || $this->offset ) { + // Show the create link ahead + $this->showCreateLink( $t ); $prevnext = wfViewPrevNext( $this->offset, $this->limit, SpecialPage::getTitleFor( 'Search' ), wfArrayToCGI( $this->powerSearchOptions(), array( 'search' => $term ) ), - max( $titleMatchesSQL, $textMatchesSQL ) < $this->limit + max( $titleMatchesNum, $textMatchesNum ) < $this->limit ); - $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" ); + //$wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" ); wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); } else { wfRunHooks( 'SpecialSearchNoResults', array( $term ) ); - } + } - $wgOut->addHtml( "<div class='searchresults'>" ); if( $titleMatches ) { if( $numTitleMatches > 0 ) { $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' ); @@ -265,10 +285,10 @@ class SpecialSearch { // output appropriate heading if( $numTextMatches > 0 && $numTitleMatches > 0 ) { // if no title matches the heading is redundant - $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' ); + $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' ); } elseif( $totalRes == 0 ) { # Don't show the 'no text matches' if we received title matches - $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' ); + # $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' ); } // show interwiki results if any if( $textMatches->hasInterwikiResults() ) { @@ -281,20 +301,41 @@ class SpecialSearch { $textMatches->free(); } - if( $totalRes === 0 ) { - $wgOut->addWikiMsg( 'search-nonefound' ); + if( $num === 0 ) { + $wgOut->addWikiMsg( 'search-nonefound', wfEscapeWikiText( $term ) ); + $this->showCreateLink( $t ); } $wgOut->addHtml( "</div>" ); - if( $totalRes === 0 ) { - $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() ); + if( $num === 0 ) { + $wgOut->addHTML( $this->searchFocus() ); } - if( $numSQL || $this->offset ) { + if( $num || $this->offset ) { $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); } wfProfileOut( __METHOD__ ); } + protected function showCreateLink( $t ) { + global $wgOut; + + // show direct page/create link if applicable + $messageName = null; + if( !is_null($t) ) { + if( $t->isKnown() ) { + $messageName = 'searchmenu-exists'; + } elseif( $t->userCan( 'create' ) ) { + $messageName = 'searchmenu-new'; + } + } + if( $messageName ) { + $wgOut->addWikiMsg( $messageName, wfEscapeWikiText( $t->getPrefixedText() ) ); + } else { + // preserve the paragraph for margins etc... + $wgOut->addHtml( '<p></p>' ); + } + } + /** * */ @@ -304,24 +345,25 @@ class SpecialSearch { $nsAllSet = array_keys( SearchEngine::searchableNamespaces() ); if( $this->searchAdvanced ) $this->active = 'advanced'; - else if( $this->namespaces === NS_FILE || $this->startsWithImage( $term ) ) - $this->active = 'images'; - elseif( $this->namespaces === $nsAllSet ) - $this->active = 'all'; - elseif( $this->namespaces === SearchEngine::defaultNamespaces() ) - $this->active = 'default'; - elseif( $this->namespaces === SearchEngine::projectNamespaces() ) - $this->active = 'project'; - else - $this->active = 'advanced'; + else { + $profiles = $this->getSearchProfiles(); + + foreach( $profiles as $key => $data ) { + if ( $this->namespaces == $data['namespaces'] && $key != 'advanced') + $this->active = $key; + } + + } # Should advanced UI be used? $this->searchAdvanced = ($this->active === 'advanced'); if( !empty( $term ) ) { $wgOut->setPageTitle( wfMsg( 'searchresults') ); $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term ) ) ); - } + } $wgOut->setArticleRelated( false ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); + // add javascript specific to special:search + $wgOut->addScriptFile( 'search.js' ); } /** @@ -358,8 +400,8 @@ class SpecialSearch { } /** - * Show whole set of results - * + * Show whole set of results + * * @param SearchResultSet $matches */ protected function showMatches( &$matches ) { @@ -403,7 +445,20 @@ class SpecialSearch { $sk = $wgUser->getSkin(); $t = $result->getTitle(); - $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms)); + $titleSnippet = $result->getTitleSnippet($terms); + + if( $titleSnippet == '' ) + $titleSnippet = null; + + $link_t = clone $t; + + wfRunHooks( 'ShowSearchHitTitle', + array( &$link_t, &$titleSnippet, $result, $terms, $this ) ); + + $link = $this->sk->linkKnown( + $link_t, + $titleSnippet + ); //If page content is not readable, just return the title. //This is not quite safe, but better than showing excerpts from non-readable pages @@ -427,19 +482,42 @@ class SpecialSearch { $sectionTitle = $result->getSectionTitle(); $sectionText = $result->getSectionSnippet($terms); $redirect = ''; - if( !is_null($redirectTitle) ) - $redirect = "<span class='searchalttitle'>" - .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText)) - ."</span>"; + + if( !is_null($redirectTitle) ) { + if( $redirectText == '' ) + $redirectText = null; + + $redirect = "<span class='searchalttitle'>" . + wfMsg( + 'search-redirect', + $this->sk->linkKnown( + $redirectTitle, + $redirectText + ) + ) . + "</span>"; + } + $section = ''; - if( !is_null($sectionTitle) ) - $section = "<span class='searchalttitle'>" - .wfMsg('search-section', $this->sk->makeKnownLinkObj( $sectionTitle, $sectionText)) - ."</span>"; + + + if( !is_null($sectionTitle) ) { + if( $sectionText == '' ) + $sectionText = null; + + $section = "<span class='searchalttitle'>" . + wfMsg( + 'search-section', $this->sk->linkKnown( + $sectionTitle, + $sectionText + ) + ) . + "</span>"; + } // format text extract $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>"; - + // format score if( is_null( $result->getScore() ) ) { // Search engine doesn't report scoring info @@ -454,20 +532,32 @@ class SpecialSearch { $byteSize = $result->getByteSize(); $wordCount = $result->getWordCount(); $timestamp = $result->getTimestamp(); - $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ), - $this->sk->formatSize( $byteSize ), $wordCount ); + $size = wfMsgExt( + 'search-result-size', + array( 'parsemag', 'escape' ), + $this->sk->formatSize( $byteSize ), + $wgLang->formatNum( $wordCount ) + ); $date = $wgLang->timeanddate( $timestamp ); // link to related articles if supported $related = ''; if( $result->hasRelated() ) { $st = SpecialPage::getTitleFor( 'Search' ); - $stParams = wfArrayToCGI( $this->powerSearchOptions(), - array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(), - 'fulltext' => wfMsg('search') )); - - $related = ' -- ' . $sk->makeKnownLinkObj( $st, - wfMsg('search-relatedarticle'), $stParams ); + $stParams = array_merge( + $this->powerSearchOptions(), + array( + 'search' => wfMsgForContent( 'searchrelated' ) . ':' . $t->getPrefixedText(), + 'fulltext' => wfMsg( 'search' ) + ) + ); + + $related = ' -- ' . $sk->linkKnown( + $st, + wfMsg('search-relatedarticle'), + array(), + $stParams + ); } // Include a thumbnail for media files... @@ -508,7 +598,7 @@ class SpecialSearch { /** * Show results from other wikis - * + * * @param SearchResultSet $matches */ protected function showInterwiki( &$matches, $query ) { @@ -517,7 +607,7 @@ class SpecialSearch { $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>". - wfMsg('search-interwiki-caption')."</div>\n"; + wfMsg('search-interwiki-caption')."</div>\n"; $off = $this->offset + 1; $out .= "<ul class='mw-search-iwresults'>\n"; @@ -527,15 +617,15 @@ class SpecialSearch { foreach($customLines as $line) { $parts = explode(":",$line,2); if(count($parts) == 2) // validate line - $customCaptions[$parts[0]] = $parts[1]; + $customCaptions[$parts[0]] = $parts[1]; } - + $prev = null; while( $result = $matches->next() ) { $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions ); $prev = $result->getInterwikiPrefix(); } - // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax).. + // TODO: should support paging in a non-confusing way (not sure how though, maybe via ajax).. $out .= "</ul></div>\n"; // convert the whole thing to desired language variant @@ -543,63 +633,88 @@ class SpecialSearch { wfProfileOut( __METHOD__ ); return $out; } - + /** * Show single interwiki link * * @param SearchResult $result * @param string $lastInterwiki * @param array $terms - * @param string $query + * @param string $query * @param array $customCaptions iw prefix -> caption */ protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) { wfProfileIn( __METHOD__ ); global $wgContLang, $wgLang; - + if( $result->isBrokenTitle() ) { wfProfileOut( __METHOD__ ); return "<!-- Broken link in search result -->\n"; } - + $t = $result->getTitle(); - $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms)); + $titleSnippet = $result->getTitleSnippet($terms); + + if( $titleSnippet == '' ) + $titleSnippet = null; + + $link = $this->sk->linkKnown( + $t, + $titleSnippet + ); // format redirect if any $redirectTitle = $result->getRedirectTitle(); $redirectText = $result->getRedirectSnippet($terms); $redirect = ''; - if( !is_null($redirectTitle) ) - $redirect = "<span class='searchalttitle'>" - .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText)) - ."</span>"; + if( !is_null($redirectTitle) ) { + if( $redirectText == '' ) + $redirectText = null; + + $redirect = "<span class='searchalttitle'>" . + wfMsg( + 'search-redirect', + $this->sk->linkKnown( + $redirectTitle, + $redirectText + ) + ) . + "</span>"; + } $out = ""; - // display project name + // display project name if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()) { if( key_exists($t->getInterwiki(),$customCaptions) ) // captions from 'search-interwiki-custom' $caption = $customCaptions[$t->getInterwiki()]; else{ - // default is to show the hostname of the other wiki which might suck + // default is to show the hostname of the other wiki which might suck // if there are many wikis on one hostname $parsed = parse_url($t->getFullURL()); - $caption = wfMsg('search-interwiki-default', $parsed['host']); - } + $caption = wfMsg('search-interwiki-default', $parsed['host']); + } // "more results" link (special page stuff could be localized, but we might not know target lang) - $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search"); - $searchLink = $this->sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'), - wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search'))); + $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search"); + $searchLink = $this->sk->linkKnown( + $searchTitle, + wfMsg('search-interwiki-more'), + array(), + array( + 'search' => $query, + 'fulltext' => 'Search' + ) + ); $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'> {$searchLink}</span>{$caption}</div>\n<ul>"; } - $out .= "<li>{$link} {$redirect}</li>\n"; + $out .= "<li>{$link} {$redirect}</li>\n"; wfProfileOut( __METHOD__ ); return $out; } - + /** * Generates the power search box at bottom of [[Special:Search]] @@ -607,172 +722,241 @@ class SpecialSearch { * @return $out string: HTML form */ protected function powerSearchBox( $term ) { - global $wgScript; - - $namespaces = SearchEngine::searchableNamespaces(); - - $tables = $this->namespaceTables( $namespaces ); - - $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) ); - $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' ); - $searchField = Xml::inputLabel( wfMsg('powersearch-field'), 'search', 'powerSearchText', 50, $term, - array( 'type' => 'text') ); - $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' )) . "\n"; - $searchTitle = SpecialPage::getTitleFor( 'Search' ); + global $wgScript, $wgContLang; - $redirectText = ''; - // show redirects check only if backend supports it - if( $this->searchEngine->acceptListRedirects() ) { - $redirectText = "<p>". $redirect . " " . $redirectLabel ."</p>"; + // Groups namespaces into rows according to subject + $rows = array(); + foreach( SearchEngine::searchableNamespaces() as $namespace => $name ) { + $subject = MWNamespace::getSubject( $namespace ); + if( !array_key_exists( $subject, $rows ) ) { + $rows[$subject] = ""; + } + $name = str_replace( '_', ' ', $name ); + if( $name == '' ) { + $name = wfMsg( 'blanknamespace' ); + } + $rows[$subject] .= + Xml::openElement( + 'td', array( 'style' => 'white-space: nowrap' ) + ) . + Xml::checkLabel( + $name, + "ns{$namespace}", + "mw-search-ns{$namespace}", + in_array( $namespace, $this->namespaces ) + ) . + Xml::closeElement( 'td' ); } + $rows = array_values( $rows ); + $numRows = count( $rows ); - $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) . - Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n" . - "<p>" . - wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . - "</p>\n" . - '<input type="hidden" name="advanced" value="'.$this->searchAdvanced."\"/>\n". - $tables . - "<hr style=\"clear: both;\" />\n". - $redirectText ."\n". - "<div style=\"padding-top:2px;padding-bottom:2px;\">". - $searchField . - " " . - Xml::hidden( 'fulltext', 'Advanced search' ) . "\n" . - $searchButton . - "</div>". - "</form>"; - $t = Title::newFromText( $term ); - /* if( $t != null && count($this->namespaces) === 1 ) { - $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term ); - } */ - return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) . + // Lays out namespaces in multiple floating two-column tables so they'll + // be arranged nicely while still accommodating different screen widths + $namespaceTables = ''; + for( $i = 0; $i < $numRows; $i += 4 ) { + $namespaceTables .= Xml::openElement( + 'table', + array( 'cellpadding' => 0, 'cellspacing' => 0, 'border' => 0 ) + ); + for( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) { + $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] ); + } + $namespaceTables .= Xml::closeElement( 'table' ); + } + // Show redirects check only if backend supports it + $redirects = ''; + if( $this->searchEngine->acceptListRedirects() ) { + $redirects = + Xml::check( + 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) + ) . + ' ' . + Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' ); + } + // Return final output + return + Xml::openElement( + 'fieldset', + array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' ) + ) . Xml::element( 'legend', null, wfMsg('powersearch-legend') ) . - $this->formHeader($term) . $out . $this->didYouMeanHtml . + Xml::tags( 'h4', null, wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) ) . + Xml::tags( + 'div', + array( 'id' => 'mw-search-togglebox' ), + Xml::label( wfMsg( 'powersearch-togglelabel' ), 'mw-search-togglelabel' ) . + Xml::element( + 'input', + array( + 'type'=>'button', + 'id' => 'mw-search-toggleall', + 'onclick' => 'mwToggleSearchCheckboxes("all");', + 'value' => wfMsg( 'powersearch-toggleall' ) + ) + ) . + Xml::element( + 'input', + array( + 'type'=>'button', + 'id' => 'mw-search-togglenone', + 'onclick' => 'mwToggleSearchCheckboxes("none");', + 'value' => wfMsg( 'powersearch-togglenone' ) + ) + ) + ) . + Xml::element( 'div', array( 'class' => 'divider' ), '', false ) . + $namespaceTables . + Xml::element( 'div', array( 'class' => 'divider' ), '', false ) . + $redirects . + Xml::hidden( 'title', SpecialPage::getTitleFor( 'Search' )->getPrefixedText() ) . + Xml::hidden( 'advanced', $this->searchAdvanced ) . + Xml::hidden( 'fulltext', 'Advanced search' ) . Xml::closeElement( 'fieldset' ); } - + protected function searchFocus() { - global $wgJsMimeType; - return "<script type=\"$wgJsMimeType\">" . + $id = $this->searchAdvanced ? 'powerSearchText' : 'searchText'; + return Html::inlineScript( "hookEvent(\"load\", function() {" . - "document.getElementById('searchText').focus();" . - "});" . - "</script>"; + "document.getElementById('$id').focus();" . + "});" ); } + + protected function getSearchProfiles() { + // Builds list of Search Types (profiles) + $nsAllSet = array_keys( SearchEngine::searchableNamespaces() ); + + $profiles = array( + 'default' => array( + 'message' => 'searchprofile-articles', + 'tooltip' => 'searchprofile-articles-tooltip', + 'namespaces' => SearchEngine::defaultNamespaces(), + 'namespace-messages' => SearchEngine::namespacesAsText( + SearchEngine::defaultNamespaces() + ), + ), + 'images' => array( + 'message' => 'searchprofile-images', + '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', + 'namespaces' => $nsAllSet, + ), + 'advanced' => array( + 'message' => 'searchprofile-advanced', + 'tooltip' => 'searchprofile-advanced-tooltip', + 'namespaces' => $this->namespaces, + 'parameters' => array( 'advanced' => 1 ), + ) + ); + + wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) ); - protected function powerSearchFocus() { - global $wgJsMimeType; - return "<script type=\"$wgJsMimeType\">" . - "hookEvent(\"load\", function() {" . - "document.getElementById('powerSearchText').focus();" . - "});" . - "</script>"; + foreach( $profiles as $key => &$data ) { + sort($data['namespaces']); + } + + return $profiles; } - protected function formHeader( $term ) { - global $wgContLang, $wgCanonicalNamespaceNames, $wgLang; - - $sep = ' '; - $out = Xml::openElement('div', array( 'style' => 'padding-bottom:0.5em;' ) ); - + protected function formHeader( $term, $resultsShown, $totalNum ) { + global $wgContLang, $wgLang; + + $out = Xml::openElement('div', array( 'class' => 'mw-search-formheader' ) ); + $bareterm = $term; - if( $this->startsWithImage( $term ) ) - $bareterm = substr( $term, strpos( $term, ':' ) + 1 ); // delete all/image prefix - - $nsAllSet = array_keys( SearchEngine::searchableNamespaces() ); - - // search profiles headers - $m = wfMsg( 'searchprofile-articles' ); - $tt = wfMsg( 'searchprofile-articles-tooltip', - $wgLang->commaList( SearchEngine::namespacesAsText( SearchEngine::defaultNamespaces() ) ) ); - if( $this->active == 'default' ) { - $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); - } else { - $out .= $this->makeSearchLink( $bareterm, SearchEngine::defaultNamespaces(), $m, $tt ); + if( $this->startsWithImage( $term ) ) { + // Deletes prefixes + $bareterm = substr( $term, strpos( $term, ':' ) + 1 ); } - $out .= $sep; + - $m = wfMsg( 'searchprofile-images' ); - $tt = wfMsg( 'searchprofile-images-tooltip' ); - if( $this->active == 'images' ) { - $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); - } else { - $imageTextForm = $wgContLang->getFormattedNsText(NS_FILE).':'.$bareterm; - $out .= $this->makeSearchLink( $imageTextForm, array( NS_FILE ) , $m, $tt ); + $profiles = $this->getSearchProfiles(); + + // Outputs XML for Search Types + $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) ); + $out .= Xml::openElement( 'ul' ); + foreach ( $profiles as $id => $profile ) { + $tooltipParam = isset( $profile['namespace-messages'] ) ? + $wgLang->commaList( $profile['namespace-messages'] ) : null; + $out .= Xml::tags( + 'li', + array( + 'class' => $this->active == $id ? 'current' : 'normal' + ), + $this->makeSearchLink( + $bareterm, + $profile['namespaces'], + wfMsg( $profile['message'] ), + wfMsg( $profile['tooltip'], $tooltipParam ), + isset( $profile['parameters'] ) ? $profile['parameters'] : array() + ) + ); } - $out .= $sep; + $out .= Xml::closeElement( 'ul' ); + $out .= Xml::closeElement('div') ; - $m = wfMsg( 'searchprofile-project' ); - $tt = wfMsg( 'searchprofile-project-tooltip', - $wgLang->commaList( SearchEngine::namespacesAsText( SearchEngine::projectNamespaces() ) ) ); - if( $this->active == 'project' ) { - $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); - } else { - $out .= $this->makeSearchLink( $bareterm, SearchEngine::projectNamespaces(), $m, $tt ); - } - $out .= $sep; - - $m = wfMsg( 'searchprofile-everything' ); - $tt = wfMsg( 'searchprofile-everything-tooltip' ); - if( $this->active == 'all' ) { - $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); - } else { - $out .= $this->makeSearchLink( $bareterm, $nsAllSet, $m, $tt ); + // Results-info + if ( $resultsShown > 0 ) { + if ( $totalNum > 0 ){ + $top = wfMsgExt( 'showingresultsheader', array( 'parseinline' ), + $wgLang->formatNum( $this->offset + 1 ), + $wgLang->formatNum( $this->offset + $resultsShown ), + $wgLang->formatNum( $totalNum ), + wfEscapeWikiText( $term ), + $wgLang->formatNum( $resultsShown ) + ); + } elseif ( $resultsShown >= $this->limit ) { + $top = wfShowingResults( $this->offset, $this->limit ); + } else { + $top = wfShowingResultsNum( $this->offset, $this->limit, $resultsShown ); + } + $out .= Xml::tags( 'div', array( 'class' => 'results-info' ), + Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) ) + ); } - $out .= $sep; - $m = wfMsg( 'searchprofile-advanced' ); - $tt = wfMsg( 'searchprofile-advanced-tooltip' ); - if( $this->active == 'advanced' ) { - $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m ); - } else { - $out .= $this->makeSearchLink( $bareterm, $this->namespaces, $m, $tt, array( 'advanced' => '1' ) ); + $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false ); + $out .= Xml::closeElement('div'); + + // Adds hidden namespace fields + if ( !$this->searchAdvanced ) { + foreach( $this->namespaces as $ns ) { + $out .= Xml::hidden( "ns{$ns}", '1' ); + } } - $out .= Xml::closeElement('div') ; return $out; } - + protected function shortDialog( $term ) { - global $wgScript; $searchTitle = SpecialPage::getTitleFor( 'Search' ); $searchable = SearchEngine::searchableNamespaces(); - $out = Xml::openElement( 'form', array( 'id' => 'search', 'method' => 'get', 'action' => $wgScript ) ); - $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n"; - // show namespaces only for advanced search - if( $this->active == 'advanced' ) { - $active = array(); - foreach( $this->namespaces as $ns ) { - $active[$ns] = $searchable[$ns]; - } - $out .= wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . "<br/>\n"; - $out .= $this->namespaceTables( $active, 1 )."<br/>\n"; - // Still keep namespace settings otherwise, but don't show them - } else { - foreach( $this->namespaces as $ns ) { - $out .= Xml::hidden( "ns{$ns}", '1' ); - } - } + $out = Html::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n"; // Keep redirect setting - $out .= Xml::hidden( "redirs", (int)$this->searchRedirects ); + $out .= Html::hidden( "redirs", (int)$this->searchRedirects ) . "\n"; // Term box - $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . "\n"; - $out .= Xml::hidden( 'fulltext', 'Search' ); - $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) ); - $out .= ' (' . wfMsgExt('searchmenu-help',array('parseinline') ) . ')'; - $out .= Xml::closeElement( 'form' ); - // Add prefix link for single-namespace searches - $t = Title::newFromText( $term ); - /*if( $t != null && count($this->namespaces) === 1 ) { - $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term ); - }*/ - return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) . - Xml::element( 'legend', null, wfMsg('searchmenu-legend') ) . - $this->formHeader($term) . $out . $this->didYouMeanHtml . - Xml::closeElement( 'fieldset' ); + $out .= Html::input( 'search', $term, 'search', array( + 'id' => $this->searchAdvanced ? 'powerSearchText' : 'searchText', + 'size' => '50', + 'autofocus' + ) ) . "\n"; + $out .= Html::hidden( 'fulltext', 'Search' ) . "\n"; + $out .= Xml::submitButton( wfMsg( 'searchbutton' ) ) . "\n"; + return $out . $this->didYouMeanHtml; } - + /** Make a search link with some target namespaces */ protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params=array() ) { $opt = $params; @@ -781,18 +965,30 @@ class SpecialSearch { } $opt['redirs'] = $this->searchRedirects ? 1 : 0; - $st = SpecialPage::getTitleFor( 'Search' ); - $stParams = wfArrayToCGI( array( 'search' => $term, 'fulltext' => wfMsg( 'search' ) ), $opt ); + $st = SpecialPage::getTitleFor( 'Search' ); + $stParams = array_merge( + array( + 'search' => $term, + 'fulltext' => wfMsg( 'search' ) + ), + $opt + ); - return Xml::element( 'a', - array( 'href'=> $st->getLocalURL( $stParams ), 'title' => $tooltip ), - $label ); + return Xml::element( + 'a', + array( + 'href' => $st->getLocalURL( $stParams ), + 'title' => $tooltip, + 'onmousedown' => 'mwSearchHeaderClick(this);', + 'onkeydown' => 'mwSearchHeaderClick(this);'), + $label + ); } - + /** Check if query starts with image: prefix */ protected function startsWithImage( $term ) { global $wgContLang; - + $p = explode( ':', $term ); if( count( $p ) > 1 ) { return $wgContLang->getNsIndex( $p[0] ) == NS_FILE; @@ -800,689 +996,16 @@ class SpecialSearch { return false; } - protected function namespaceTables( $namespaces, $rowsPerTable = 3 ) { - global $wgContLang; - // Group namespaces into rows according to subject. - // Try not to make too many assumptions about namespace numbering. - $rows = array(); - $tables = ""; - foreach( $namespaces as $ns => $name ) { - $subj = MWNamespace::getSubject( $ns ); - if( !array_key_exists( $subj, $rows ) ) { - $rows[$subj] = ""; - } - $name = str_replace( '_', ' ', $name ); - if( '' == $name ) { - $name = wfMsg( 'blanknamespace' ); - } - $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) . - Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) . - Xml::closeElement( 'td' ) . "\n"; - } - $rows = array_values( $rows ); - $numRows = count( $rows ); - // Lay out namespaces in multiple floating two-column tables so they'll - // be arranged nicely while still accommodating different screen widths - // Float to the right on RTL wikis - $tableStyle = $wgContLang->isRTL() ? - 'float: right; margin: 0 0 0em 1em' : 'float: left; margin: 0 1em 0em 0'; - // Build the final HTML table... - for( $i = 0; $i < $numRows; $i += $rowsPerTable ) { - $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) ); - for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) { - $tables .= "<tr>\n" . $rows[$j] . "</tr>"; - } - $tables .= Xml::closeElement( 'table' ) . "\n"; - } - return $tables; - } -} - -/** - * implements Special:Search - Run text & title search and display the output - * @ingroup SpecialPage - */ -class SpecialSearchOld { - - /** - * Set up basic search parameters from the request and user settings. - * Typically you'll pass $wgRequest and $wgUser. - * - * @param WebRequest $request - * @param User $user - * @public - */ - function __construct( &$request, &$user ) { - list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' ); - $this->mPrefix = $request->getVal('prefix', ''); - $this->namespaces = $this->powerSearch( $request ); - if( empty( $this->namespaces ) ) { - $this->namespaces = SearchEngine::userNamespaces( $user ); - } - - $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false; - $this->fulltext = $request->getVal('fulltext'); - } - - /** - * If an exact title match can be found, jump straight ahead to it. - * @param string $term - * @public - */ - function goResult( $term ) { - global $wgOut; - global $wgGoToEdit; - - $this->setupPage( $term ); - - # Try to go to page as entered. - $t = Title::newFromText( $term ); - - # If the string cannot be used to create a title - if( is_null( $t ) ){ - return $this->showResults( $term ); - } - - # If there's an exact or very near match, jump right there. - $t = SearchEngine::getNearMatch( $term ); - if( !is_null( $t ) ) { - $wgOut->redirect( $t->getFullURL() ); - return; - } - - # No match, generate an edit URL - $t = Title::newFromText( $term ); - if( ! is_null( $t ) ) { - wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) ); - # If the feature is enabled, go straight to the edit page - if ( $wgGoToEdit ) { - $wgOut->redirect( $t->getFullURL( 'action=edit' ) ); - return; - } - } - - $extra = $wgOut->parse( '=='.wfMsgNoTrans( 'notitlematches' )."==\n" ); - if( $t->quickUserCan( 'create' ) && $t->quickUserCan( 'edit' ) ) { - $extra .= wfMsgExt( 'noexactmatch', 'parse', wfEscapeWikiText( $term ) ); - } else { - $extra .= wfMsgExt( 'noexactmatch-nocreate', 'parse', wfEscapeWikiText( $term ) ); - } - - $this->showResults( $term, $extra ); - } - - /** - * @param string $term - * @param string $extra Extra HTML to add after "did you mean" - */ - public function showResults( $term, $extra = '' ) { - wfProfileIn( __METHOD__ ); - global $wgOut, $wgUser; - $sk = $wgUser->getSkin(); - - $search = SearchEngine::create(); - $search->setLimitOffset( $this->limit, $this->offset ); - $search->setNamespaces( $this->namespaces ); - $search->showRedirects = $this->searchRedirects; - $search->prefix = $this->mPrefix; - $term = $search->transformSearchTerm($term); - - $this->setupPage( $term ); - - $rewritten = $search->replacePrefixes($term); - $titleMatches = $search->searchTitle( $rewritten ); - $textMatches = $search->searchText( $rewritten ); - - // did you mean... suggestions - if($textMatches && $textMatches->hasSuggestion()){ - $st = SpecialPage::getTitleFor( 'Search' ); - - # mirror Go/Search behaviour of original request - $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() ); - if($this->fulltext != NULL) - $didYouMeanParams['fulltext'] = $this->fulltext; - $stParams = wfArrayToCGI( - $didYouMeanParams, - $this->powerSearchOptions() - ); - - $suggestLink = $sk->makeKnownLinkObj( $st, - $textMatches->getSuggestionSnippet(), - $stParams ); - - $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>'); - } - - $wgOut->addHTML( $extra ); - - $wgOut->wrapWikiMsg( "<div class='mw-searchresult'>\n$1</div>", 'searchresulttext' ); - - if( '' === trim( $term ) ) { - // Empty query -- straight view of search form - $wgOut->setSubtitle( '' ); - $wgOut->addHTML( $this->powerSearchBox( $term ) ); - $wgOut->addHTML( $this->powerSearchFocus() ); - wfProfileOut( __METHOD__ ); - return; - } - - global $wgDisableTextSearch; - if ( $wgDisableTextSearch ) { - global $wgSearchForwardUrl; - if( $wgSearchForwardUrl ) { - $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl ); - $wgOut->redirect( $url ); - wfProfileOut( __METHOD__ ); - return; - } - global $wgInputEncoding; - $wgOut->addHTML( - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'search-external' ) ) . - Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) . - wfMsg( 'googlesearch', - htmlspecialchars( $term ), - htmlspecialchars( $wgInputEncoding ), - htmlspecialchars( wfMsg( 'searchbutton' ) ) - ) . - Xml::closeElement( 'fieldset' ) - ); - wfProfileOut( __METHOD__ ); - return; - } - - $wgOut->addHTML( $this->shortDialog( $term ) ); - - // Sometimes the search engine knows there are too many hits - if ($titleMatches instanceof SearchResultTooMany) { - $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" ); - $wgOut->addHTML( $this->powerSearchBox( $term ) ); - $wgOut->addHTML( $this->powerSearchFocus() ); - wfProfileOut( __METHOD__ ); - return; - } - - // show number of results - $num = ( $titleMatches ? $titleMatches->numRows() : 0 ) - + ( $textMatches ? $textMatches->numRows() : 0); - $totalNum = 0; - if($titleMatches && !is_null($titleMatches->getTotalHits())) - $totalNum += $titleMatches->getTotalHits(); - if($textMatches && !is_null($textMatches->getTotalHits())) - $totalNum += $textMatches->getTotalHits(); - if ( $num > 0 ) { - if ( $totalNum > 0 ){ - $top = wfMsgExt('showingresultstotal', array( 'parseinline' ), - $this->offset+1, $this->offset+$num, $totalNum, $num ); - } elseif ( $num >= $this->limit ) { - $top = wfShowingResults( $this->offset, $this->limit ); - } else { - $top = wfShowingResultsNum( $this->offset, $this->limit, $num ); - } - $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" ); - } - - // prev/next links - if( $num || $this->offset ) { - $prevnext = wfViewPrevNext( $this->offset, $this->limit, - SpecialPage::getTitleFor( 'Search' ), - wfArrayToCGI( - $this->powerSearchOptions(), - array( 'search' => $term ) ), - ($num < $this->limit) ); - $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" ); - wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); - } else { - wfRunHooks( 'SpecialSearchNoResults', array( $term ) ); - } - - if( $titleMatches ) { - if( $titleMatches->numRows() ) { - $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' ); - $wgOut->addHTML( $this->showMatches( $titleMatches ) ); - } - $titleMatches->free(); - } - - if( $textMatches ) { - // output appropriate heading - if( $textMatches->numRows() ) { - if($titleMatches) - $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' ); - else // if no title matches the heading is redundant - $wgOut->addHTML("<hr/>"); - } elseif( $num == 0 ) { - # Don't show the 'no text matches' if we received title matches - $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' ); - } - // show interwiki results if any - if( $textMatches->hasInterwikiResults() ) - $wgOut->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term )); - // show results - if( $textMatches->numRows() ) - $wgOut->addHTML( $this->showMatches( $textMatches ) ); - - $textMatches->free(); - } - - if ( $num == 0 ) { - $wgOut->addWikiMsg( 'nonefound' ); - } - if( $num || $this->offset ) { - $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); - } - $wgOut->addHTML( $this->powerSearchBox( $term ) ); - wfProfileOut( __METHOD__ ); - } - - #------------------------------------------------------------------ - # Private methods below this line - - /** - * - */ - function setupPage( $term ) { - global $wgOut; - if( !empty( $term ) ){ - $wgOut->setPageTitle( wfMsg( 'searchresults') ); - $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term) ) ); - } - $subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' ); - $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) ); - $wgOut->setArticleRelated( false ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - } - - /** - * Extract "power search" namespace settings from the request object, - * returning a list of index numbers to search. - * - * @param WebRequest $request - * @return array - * @private - */ - function powerSearch( &$request ) { - $arr = array(); - foreach( SearchEngine::searchableNamespaces() as $ns => $name ) { - if( $request->getCheck( 'ns' . $ns ) ) { - $arr[] = $ns; - } - } - return $arr; - } - - /** - * Reconstruct the 'power search' options for links - * @return array - * @private - */ - function powerSearchOptions() { - $opt = array(); - foreach( $this->namespaces as $n ) { - $opt['ns' . $n] = 1; - } - $opt['redirs'] = $this->searchRedirects ? 1 : 0; - return $opt; - } - - /** - * Show whole set of results - * - * @param SearchResultSet $matches - */ - function showMatches( &$matches ) { - wfProfileIn( __METHOD__ ); - - global $wgContLang; - $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); - - $out = ""; - - $infoLine = $matches->getInfo(); - if( !is_null($infoLine) ) - $out .= "\n<!-- {$infoLine} -->\n"; - - - $off = $this->offset + 1; - $out .= "<ul class='mw-search-results'>\n"; - - while( $result = $matches->next() ) { - $out .= $this->showHit( $result, $terms ); - } - $out .= "</ul>\n"; - - // convert the whole thing to desired language variant - global $wgContLang; - $out = $wgContLang->convert( $out ); - wfProfileOut( __METHOD__ ); - return $out; - } + /** Check if query starts with all: prefix */ + protected function startsWithAll( $term ) { - /** - * Format a single hit result - * @param SearchResult $result - * @param array $terms terms to highlight - */ - function showHit( $result, $terms ) { - wfProfileIn( __METHOD__ ); - global $wgUser, $wgContLang, $wgLang; + $allkeyword = wfMsgForContent('searchall'); - if( $result->isBrokenTitle() ) { - wfProfileOut( __METHOD__ ); - return "<!-- Broken link in search result -->\n"; - } - - $t = $result->getTitle(); - $sk = $wgUser->getSkin(); - - $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms)); - - //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->userCanRead()) { - wfProfileOut( __METHOD__ ); - return "<li>{$link}</li>\n"; - } - - // If the page doesn't *exist*... our search index is out of date. - // 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 "<!-- missing page " . - htmlspecialchars( $t->getPrefixedText() ) . "-->\n"; - } - - // format redirects / relevant sections - $redirectTitle = $result->getRedirectTitle(); - $redirectText = $result->getRedirectSnippet($terms); - $sectionTitle = $result->getSectionTitle(); - $sectionText = $result->getSectionSnippet($terms); - $redirect = ''; - if( !is_null($redirectTitle) ) - $redirect = "<span class='searchalttitle'>" - .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText)) - ."</span>"; - $section = ''; - if( !is_null($sectionTitle) ) - $section = "<span class='searchalttitle'>" - .wfMsg('search-section', $sk->makeKnownLinkObj( $sectionTitle, $sectionText)) - ."</span>"; - - // format text extract - $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>"; - - // format score - if( is_null( $result->getScore() ) ) { - // Search engine doesn't report scoring info - $score = ''; - } else { - $percent = sprintf( '%2.1f', $result->getScore() * 100 ); - $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) ) - . ' - '; - } - - // format description - $byteSize = $result->getByteSize(); - $wordCount = $result->getWordCount(); - $timestamp = $result->getTimestamp(); - $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ), - $sk->formatSize( $byteSize ), - $wordCount ); - $date = $wgLang->timeanddate( $timestamp ); - - // link to related articles if supported - $related = ''; - if( $result->hasRelated() ){ - $st = SpecialPage::getTitleFor( 'Search' ); - $stParams = wfArrayToCGI( $this->powerSearchOptions(), - array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(), - 'fulltext' => wfMsg('search') )); - - $related = ' -- ' . $sk->makeKnownLinkObj( $st, - wfMsg('search-relatedarticle'), $stParams ); - } - - // Include a thumbnail for media files... - if( $t->getNamespace() == NS_FILE ) { - $img = wfFindFile( $t ); - if( $img ) { - $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) ); - if( $thumb ) { - $desc = $img->getShortDesc(); - wfProfileOut( __METHOD__ ); - // Ugly table. :D - // Float doesn't seem to interact well with the bullets. - // Table messes up vertical alignment of the bullet, but I'm - // not sure what more I can do about that. :( - return "<li>" . - '<table class="searchResultImage">' . - '<tr>' . - '<td width="120" align="center">' . - $thumb->toHtml( array( 'desc-link' => true ) ) . - '</td>' . - '<td valign="top">' . - $link . - $extract . - "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" . - '</td>' . - '</tr>' . - '</table>' . - "</li>\n"; - } - } - } - - wfProfileOut( __METHOD__ ); - return "<li>{$link} {$redirect} {$section} {$extract}\n" . - "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" . - "</li>\n"; - - } - - /** - * Show results from other wikis - * - * @param SearchResultSet $matches - */ - function showInterwiki( &$matches, $query ) { - wfProfileIn( __METHOD__ ); - - global $wgContLang; - $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); - - $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".wfMsg('search-interwiki-caption')."</div>\n"; - $off = $this->offset + 1; - $out .= "<ul start='{$off}' class='mw-search-iwresults'>\n"; - - // work out custom project captions - $customCaptions = array(); - $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption> - foreach($customLines as $line){ - $parts = explode(":",$line,2); - if(count($parts) == 2) // validate line - $customCaptions[$parts[0]] = $parts[1]; - } - - - $prev = null; - while( $result = $matches->next() ) { - $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions ); - $prev = $result->getInterwikiPrefix(); - } - // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax).. - $out .= "</ul></div>\n"; - - // convert the whole thing to desired language variant - global $wgContLang; - $out = $wgContLang->convert( $out ); - wfProfileOut( __METHOD__ ); - return $out; - } - - /** - * Show single interwiki link - * - * @param SearchResult $result - * @param string $lastInterwiki - * @param array $terms - * @param string $query - * @param array $customCaptions iw prefix -> caption - */ - function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) { - wfProfileIn( __METHOD__ ); - global $wgUser, $wgContLang, $wgLang; - - if( $result->isBrokenTitle() ) { - wfProfileOut( __METHOD__ ); - return "<!-- Broken link in search result -->\n"; - } - - $t = $result->getTitle(); - $sk = $wgUser->getSkin(); - - $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms)); - - // format redirect if any - $redirectTitle = $result->getRedirectTitle(); - $redirectText = $result->getRedirectSnippet($terms); - $redirect = ''; - if( !is_null($redirectTitle) ) - $redirect = "<span class='searchalttitle'>" - .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText)) - ."</span>"; - - $out = ""; - // display project name - if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()){ - if( key_exists($t->getInterwiki(),$customCaptions) ) - // captions from 'search-interwiki-custom' - $caption = $customCaptions[$t->getInterwiki()]; - else{ - // default is to show the hostname of the other wiki which might suck - // if there are many wikis on one hostname - $parsed = parse_url($t->getFullURL()); - $caption = wfMsg('search-interwiki-default', $parsed['host']); - } - // "more results" link (special page stuff could be localized, but we might not know target lang) - $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search"); - $searchLink = $sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'), - wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search'))); - $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>{$searchLink}</span>{$caption}</div>\n<ul>"; - } - - $out .= "<li>{$link} {$redirect}</li>\n"; - wfProfileOut( __METHOD__ ); - return $out; - } - - - /** - * Generates the power search box at bottom of [[Special:Search]] - * @param $term string: search term - * @return $out string: HTML form - */ - function powerSearchBox( $term ) { - global $wgScript, $wgContLang; - - $namespaces = SearchEngine::searchableNamespaces(); - - // group namespaces into rows according to subject; try not to make too - // many assumptions about namespace numbering - $rows = array(); - foreach( $namespaces as $ns => $name ) { - $subj = MWNamespace::getSubject( $ns ); - if( !array_key_exists( $subj, $rows ) ) { - $rows[$subj] = ""; - } - $name = str_replace( '_', ' ', $name ); - if( '' == $name ) { - $name = wfMsg( 'blanknamespace' ); - } - $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) . - Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) . - Xml::closeElement( 'td' ) . "\n"; - } - $rows = array_values( $rows ); - $numRows = count( $rows ); - - // lay out namespaces in multiple floating two-column tables so they'll - // be arranged nicely while still accommodating different screen widths - $rowsPerTable = 3; // seems to look nice - - // float to the right on RTL wikis - $tableStyle = ( $wgContLang->isRTL() ? - 'float: right; margin: 0 0 1em 1em' : - 'float: left; margin: 0 1em 1em 0' ); - - $tables = ""; - for( $i = 0; $i < $numRows; $i += $rowsPerTable ) { - $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) ); - for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) { - $tables .= "<tr>\n" . $rows[$j] . "</tr>"; - } - $tables .= Xml::closeElement( 'table' ) . "\n"; - } - - $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) ); - $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' ); - $searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) ); - $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' ) ) . "\n"; - $searchTitle = SpecialPage::getTitleFor( 'Search' ); - $searchHiddens = Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n"; - $searchHiddens .= Xml::hidden( 'fulltext', 'Advanced search' ) . "\n"; - - $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) . - Xml::fieldset( wfMsg( 'powersearch-legend' ), - "<p>" . - wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . - "</p>\n" . - $tables . - "<hr style=\"clear: both\" />\n" . - "<p>" . - $redirect . " " . $redirectLabel . - "</p>\n" . - wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) . - " " . - $searchField . - " " . - $searchHiddens . - $searchButton ) . - "</form>"; - - return $out; - } - - function powerSearchFocus() { - global $wgJsMimeType; - return "<script type=\"$wgJsMimeType\">" . - "hookEvent(\"load\", function(){" . - "document.getElementById('powerSearchText').focus();" . - "});" . - "</script>"; - } - - function shortDialog($term) { - global $wgScript; - - $out = Xml::openElement( 'form', array( - 'id' => 'search', - 'method' => 'get', - 'action' => $wgScript - )); - $searchTitle = SpecialPage::getTitleFor( 'Search' ); - $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . ' '; - foreach( SearchEngine::searchableNamespaces() as $ns => $name ) { - if( in_array( $ns, $this->namespaces ) ) { - $out .= Xml::hidden( "ns{$ns}", '1' ); - } + $p = explode( ':', $term ); + if( count( $p ) > 1 ) { + return $p[0] == $allkeyword; } - $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() ); - $out .= Xml::hidden( 'fulltext', 'Search' ); - $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) ); - $out .= Xml::closeElement( 'form' ); - - return $out; + return false; } } + diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php index 2e7d24a5..c41b15c5 100644 --- a/includes/specials/SpecialShortpages.php +++ b/includes/specials/SpecialShortpages.php @@ -74,10 +74,15 @@ class ShortPagesPage extends QueryPage { if ( !$title ) { return '<!-- Invalid title ' . htmlspecialchars( "{$result->namespace}:{$result->title}" ). '-->'; } - $hlink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' ); + $hlink = $skin->linkKnown( + $title, + wfMsgHtml( 'hist' ), + array(), + array( 'action' => 'history' ) + ); $plink = $this->isCached() - ? $skin->makeLinkObj( $title ) - : $skin->makeKnownLinkObj( $title ); + ? $skin->link( $title ) + : $skin->linkKnown( $title ); $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->value ) ) ); return $title->exists() diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php index 4959f107..84ab689a 100644 --- a/includes/specials/SpecialSpecialpages.php +++ b/includes/specials/SpecialSpecialpages.php @@ -55,15 +55,15 @@ function wfSpecialSpecialpages() { $total = count($sortedPages); $count = 0; - $wgOut->wrapWikiMsg( "<h4 class='mw-specialpagesgroup'>$1</h4>\n", "specialpages-group-$group" ); + $wgOut->wrapWikiMsg( "<h4 class=\"mw-specialpagesgroup\" id=\"mw-specialpagesgroup-$group\">$1</h4>\n", "specialpages-group-$group" ); $wgOut->addHTML( "<table style='width: 100%;' class='mw-specialpages-table'><tr>" ); $wgOut->addHTML( "<td width='30%' valign='top'><ul>\n" ); foreach( $sortedPages as $desc => $specialpage ) { list( $title, $restricted ) = $specialpage; - $link = $sk->makeKnownLinkObj( $title , htmlspecialchars( $desc ) ); + $link = $sk->linkKnown( $title , htmlspecialchars( $desc ) ); if( $restricted ) { $includesRestrictedPages = true; - $wgOut->addHTML( "<li class='mw-specialpages-page mw-specialpagerestricted'>{$link}</li>\n" ); + $wgOut->addHTML( "<li class='mw-specialpages-page mw-specialpagerestricted'><strong>{$link}</strong></li>\n" ); } else { $wgOut->addHTML( "<li>{$link}</li>\n" ); } diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php index 109c5c30..2e785b8b 100644 --- a/includes/specials/SpecialStatistics.php +++ b/includes/specials/SpecialStatistics.php @@ -23,7 +23,7 @@ class SpecialStatistics extends SpecialPage { } public function execute( $par ) { - global $wgOut, $wgRequest, $wgMessageCache; + global $wgOut, $wgRequest, $wgMessageCache, $wgMemc; global $wgDisableCounters, $wgMiserMode; $wgMessageCache->loadAllMessages(); @@ -38,6 +38,7 @@ class SpecialStatistics extends SpecialPage { $this->activeUsers = SiteStats::activeUsers(); $this->admins = SiteStats::numberingroup('sysop'); $this->numJobs = SiteStats::jobs(); + $this->hook = ''; # Staticic - views $viewsStats = ''; @@ -47,8 +48,13 @@ class SpecialStatistics extends SpecialPage { # Set active user count if( !$wgMiserMode ) { - $dbw = wfGetDB( DB_MASTER ); - SiteStatsUpdate::cacheUpdate( $dbw ); + $key = wfMemcKey( 'sitestats', 'activeusers-updated' ); + // Re-calculate the count if the last tally is old... + if( !$wgMemc->get($key) ) { + $dbw = wfGetDB( DB_MASTER ); + SiteStatsUpdate::cacheUpdate( $dbw ); + $wgMemc->set( $key, '1', 24*3600 ); // don't update for 1 day + } } # Do raw output @@ -56,10 +62,10 @@ class SpecialStatistics extends SpecialPage { $this->doRawOutput(); } - $text = Xml::openElement( 'table', array( 'class' => 'mw-statistics-table' ) ); + $text = Xml::openElement( 'table', array( 'class' => 'wikitable mw-statistics-table' ) ); # Statistic - pages - $text .= $this->getPageStats(); + $text .= $this->getPageStats(); # Statistic - edits $text .= $this->getEditStats(); @@ -75,6 +81,12 @@ class SpecialStatistics extends SpecialPage { if( !$wgDisableCounters && !$wgMiserMode ) { $text .= $this->getMostViewedPages(); } + + # Statistic - other + $extraStats = array(); + if( wfRunHooks( 'SpecialStatsAddExtra', array( &$extraStats ) ) ) { + $text .= $this->getOtherStats( $extraStats ); + } $text .= Xml::closeElement( 'table' ); @@ -149,14 +161,22 @@ class SpecialStatistics extends SpecialPage { array( 'class' => 'mw-statistics-jobqueue' ) ); } private function getUserStats() { - global $wgLang, $wgRCMaxAge; + global $wgLang, $wgUser, $wgRCMaxAge; + $sk = $wgUser->getSkin(); return Xml::openElement( 'tr' ) . Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-users', array( 'parseinline' ) ) ) . Xml::closeElement( 'tr' ) . $this->formatRow( wfMsgExt( 'statistics-users', array( 'parseinline' ) ), $wgLang->formatNum( $this->users ), array( 'class' => 'mw-statistics-users' ) ) . - $this->formatRow( wfMsgExt( 'statistics-users-active', array( 'parseinline' ) ), + $this->formatRow( wfMsgExt( 'statistics-users-active', array( 'parseinline' ) ) . ' ' . + $sk->link( + SpecialPage::getTitleFor( 'Activeusers' ), + wfMsgHtml( 'listgrouprights-members' ), + array(), + array(), + 'known' + ), $wgLang->formatNum( $this->activeUsers ), array( 'class' => 'mw-statistics-users-active' ), 'statistics-users-active-desc', @@ -184,13 +204,19 @@ class SpecialStatistics extends SpecialPage { } else { $grouppageLocalized = $msg; } - $grouppage = $sk->makeLink( $grouppageLocalized, htmlspecialchars( $groupnameLocalized ) ); - $grouplink = $sk->link( SpecialPage::getTitleFor( 'Listusers' ), + $linkTarget = Title::newFromText( $grouppageLocalized ); + $grouppage = $sk->link( + $linkTarget, + htmlspecialchars( $groupnameLocalized ) + ); + $grouplink = $sk->link( + SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), array(), array( 'group' => $group ), - 'known' ); - # Add a class when a usergroup contains no members to allow hiding these rows + 'known' + ); + # Add a class when a usergroup contains no members to allow hiding these rows $classZero = ''; $countUsers = SiteStats::numberingroup( $groupname ); if( $countUsers == 0 ) { @@ -238,7 +264,9 @@ class SpecialStatistics extends SpecialPage { ) ); if( $res->numRows() > 0 ) { + $text .= Xml::openElement( 'tr' ); $text .= Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-mostpopular', array( 'parseinline' ) ) ); + $text .= Xml::closeElement( 'tr' ); while( $row = $res->fetchObject() ) { $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); if( $title instanceof Title ) { @@ -252,6 +280,26 @@ class SpecialStatistics extends SpecialPage { return $text; } + private function getOtherStats( $stats ) { + global $wgLang; + + if ( !count( $stats ) ) + return ''; + + $return = Xml::openElement( 'tr' ) . + Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-hooks', array( 'parseinline' ) ) ) . + Xml::closeElement( 'tr' ); + + foreach( $stats as $name => $number ) { + $name = htmlspecialchars( $name ); + $number = htmlspecialchars( $number ); + + $return .= $this->formatRow( $name, $wgLang->formatNum( $number ), array( 'class' => 'mw-statistics-hook' ) ); + } + + return $return; + } + /** * Do the action=raw output for this page. Legacy, but we support * it for backwards compatibility diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php index 981eb2ff..57feeae7 100644 --- a/includes/specials/SpecialTags.php +++ b/includes/specials/SpecialTags.php @@ -36,7 +36,7 @@ class SpecialTags extends SpecialPage { $html .= $this->doTagRow( $tag, 0 ); } - $wgOut->addHTML( Xml::tags( 'table', array( 'class' => 'mw-tags-table' ), $html ) ); + $wgOut->addHTML( Xml::tags( 'table', array( 'class' => 'wikitable mw-tags-table' ), $html ) ); } function doTagRow( $tag, $hitcount ) { @@ -49,21 +49,23 @@ class SpecialTags extends SpecialPage { if ( in_array( $tag, $doneTags ) ) { return ''; } + + global $wgLang; $newRow = ''; $newRow .= Xml::tags( 'td', null, Xml::element( 'tt', null, $tag ) ); $disp = ChangeTags::tagDescription( $tag ); - $disp .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), wfMsg( 'tags-edit' ) ) . ')'; + $disp .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), wfMsgHtml( 'tags-edit' ) ) . ')'; $newRow .= Xml::tags( 'td', null, $disp ); $desc = wfMsgExt( "tag-$tag-description", 'parseinline' ); $desc = wfEmptyMsg( "tag-$tag-description", $desc ) ? '' : $desc; - $desc .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), wfMsg( 'tags-edit' ) ) . ')'; + $desc .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), wfMsgHtml( 'tags-edit' ) ) . ')'; $newRow .= Xml::tags( 'td', null, $desc ); - $hitcount = wfMsg( 'tags-hitcount', $hitcount ); - $hitcount = $sk->link( SpecialPage::getTitleFor( 'RecentChanges' ), $hitcount, array(), array( 'tagfilter' => $tag ) ); + $hitcount = wfMsgExt( 'tags-hitcount', array( 'parsemag' ), $wgLang->formatNum( $hitcount ) ); + $hitcount = $sk->link( SpecialPage::getTitleFor( 'Recentchanges' ), $hitcount, array(), array( 'tagfilter' => $tag ) ); $newRow .= Xml::tags( 'td', null, $hitcount ); $doneTags[] = $tag; diff --git a/includes/specials/SpecialUncategorizedtemplates.php b/includes/specials/SpecialUncategorizedtemplates.php index cb2a6d40..7e6fd24b 100644 --- a/includes/specials/SpecialUncategorizedtemplates.php +++ b/includes/specials/SpecialUncategorizedtemplates.php @@ -23,8 +23,6 @@ class UncategorizedTemplatesPage extends UncategorizedPagesPage { /** * Main execution point - * - * @param mixed $par Parameter passed to the page */ function wfSpecialUncategorizedtemplates() { list( $limit, $offset ) = wfCheckLimits(); diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index d97efb59..4db4e633 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -58,16 +58,15 @@ class PageArchive { $title = Title::newFromText( $prefix ); if( $title ) { $ns = $title->getNamespace(); - $encPrefix = $dbr->escapeLike( $title->getDBkey() ); + $prefix = $title->getDBkey(); } else { // Prolly won't work too good // @todo handle bare namespace names cleanly? $ns = 0; - $encPrefix = $dbr->escapeLike( $prefix ); } $conds = array( 'ar_namespace' => $ns, - "ar_title LIKE '$encPrefix%'", + 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ), ); return self::listPages( $dbr, $conds ); } @@ -188,20 +187,7 @@ class PageArchive { 'ar_timestamp' => $dbr->timestamp( $timestamp ) ), __METHOD__ ); if( $row ) { - return new Revision( array( - 'page' => $this->title->getArticleId(), - 'id' => $row->ar_rev_id, - 'text' => ($row->ar_text_id - ? null - : Revision::getRevisionText( $row, 'ar_' ) ), - 'comment' => $row->ar_comment, - 'user' => $row->ar_user, - 'user_text' => $row->ar_user_text, - 'timestamp' => $row->ar_timestamp, - 'minor_edit' => $row->ar_minor_edit, - 'text_id' => $row->ar_text_id, - 'deleted' => $row->ar_deleted, - 'len' => $row->ar_len) ); + return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleId() ) ); } else { return null; } @@ -299,7 +285,7 @@ class PageArchive { if( $row ) { return $this->getTextFromRow( $row ); } else { - return NULL; + return null; } } @@ -345,7 +331,7 @@ class PageArchive { } if( $restoreText ) { - $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress ); + $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress, $comment ); if($textRestored === false) // It must be one of UNDELETE_* return false; } else { @@ -372,7 +358,7 @@ class PageArchive { } if( trim( $comment ) != '' ) - $reason .= ": {$comment}"; + $reason .= wfMsgForContent( 'colon-separator' ) . $comment; $log->addEntry( 'restore', $this->title, $reason ); return array($textRestored, $filesRestored, $reason); @@ -390,7 +376,7 @@ class PageArchive { * * @return mixed number of revisions restored or false on failure */ - private function undeleteRevisions( $timestamps, $unsuppress = false ) { + private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) { if ( wfReadOnly() ) return false; $restoreAll = empty( $timestamps ); @@ -399,13 +385,14 @@ class PageArchive { # Does this page already exist? We'll have to update it... $article = new Article( $this->title ); - $options = 'FOR UPDATE'; + $options = 'FOR UPDATE'; // lock page $page = $dbw->selectRow( 'page', array( 'page_id', 'page_latest' ), array( 'page_namespace' => $this->title->getNamespace(), 'page_title' => $this->title->getDBkey() ), __METHOD__, - $options ); + $options + ); if( $page ) { $makepage = false; # Page already exists. Import the history, and if necessary @@ -462,50 +449,53 @@ class PageArchive { $oldones ), __METHOD__, /* options */ array( 'ORDER BY' => 'ar_timestamp' ) - ); + ); $ret = $dbw->resultObject( $result ); $rev_count = $dbw->numRows( $result ); + if( !$rev_count ) { + wfDebug( __METHOD__.": no revisions to restore\n" ); + return false; // ??? + } + + $ret->seek( $rev_count - 1 ); // move to last + $row = $ret->fetchObject(); // get newest archived rev + $ret->seek( 0 ); // move back if( $makepage ) { + // Check the state of the newest to-be version... + if( !$unsuppress && ($row->ar_deleted & Revision::DELETED_TEXT) ) { + return false; // we can't leave the current revision like this! + } + // Safe to insert now... $newid = $article->insertOn( $dbw ); $pageId = $newid; + } else { + // Check if a deleted revision will become the current revision... + if( $row->ar_timestamp > $previousTimestamp ) { + // Check the state of the newest to-be version... + if( !$unsuppress && ($row->ar_deleted & Revision::DELETED_TEXT) ) { + return false; // we can't leave the current revision like this! + } + } } $revision = null; $restored = 0; while( $row = $ret->fetchObject() ) { - if( $row->ar_text_id ) { - // Revision was deleted in 1.5+; text is in - // the regular text table, use the reference. - // Specify null here so the so the text is - // dereferenced for page length info if needed. - $revText = null; - } else { - // Revision was deleted in 1.4 or earlier. - // Text is squashed into the archive row, and - // a new text table entry will be created for it. - $revText = Revision::getRevisionText( $row, 'ar_' ); - } // Check for key dupes due to shitty archive integrity. if( $row->ar_rev_id ) { $exists = $dbw->selectField( 'revision', '1', array('rev_id' => $row->ar_rev_id), __METHOD__ ); if( $exists ) continue; // don't throw DB errors } - - $revision = new Revision( array( - 'page' => $pageId, - 'id' => $row->ar_rev_id, - 'text' => $revText, - 'comment' => $row->ar_comment, - 'user' => $row->ar_user, - 'user_text' => $row->ar_user_text, - 'timestamp' => $row->ar_timestamp, - 'minor_edit' => $row->ar_minor_edit, - 'text_id' => $row->ar_text_id, - 'deleted' => $unsuppress ? 0 : $row->ar_deleted, - 'len' => $row->ar_len + // Insert one revision at a time...maintaining deletion status + // unless we are specifically removing all restrictions... + $revision = Revision::newFromArchiveRow( $row, + array( + 'page' => $pageId, + 'deleted' => $unsuppress ? 0 : $row->ar_deleted ) ); + $revision->insertOn( $dbw ); $restored++; @@ -529,21 +519,13 @@ class PageArchive { if( $newid || $wasnew ) { // Update site stats, link tables, etc $article->createUpdates( $revision ); - // We don't handle well with top revision deleted - if( $revision->getVisibility() ) { - $dbw->update( 'revision', - array( 'rev_deleted' => 0 ), - array( 'rev_id' => $revision->getId() ), - __METHOD__ - ); - } } if( $newid ) { - wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) ); + wfRunHooks( 'ArticleUndelete', array( &$this->title, true, $comment ) ); Article::onArticleCreate( $this->title ); } else { - wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) ); + wfRunHooks( 'ArticleUndelete', array( &$this->title, false, $comment ) ); Article::onArticleEdit( $this->title ); } @@ -569,7 +551,7 @@ class PageArchive { */ class UndeleteForm { var $mAction, $mTarget, $mTimestamp, $mRestore, $mInvert, $mTargetObj; - var $mTargetTimestamp, $mAllowed, $mComment, $mToken; + var $mTargetTimestamp, $mAllowed, $mCanView, $mComment, $mToken; function UndeleteForm( $request, $par = "" ) { global $wgUser; @@ -594,16 +576,21 @@ class UndeleteForm { $this->mTarget = $par; } if ( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) { - $this->mAllowed = true; - } else { + $this->mAllowed = true; // user can restore + $this->mCanView = true; // user can view content + } elseif ( $wgUser->isAllowed( 'deletedtext' ) ) { + $this->mAllowed = false; // user cannot restore + $this->mCanView = true; // user can view content + } else { // user can only view the list of revisions $this->mAllowed = false; + $this->mCanView = false; $this->mTimestamp = ''; $this->mRestore = false; } if ( $this->mTarget !== "" ) { $this->mTargetObj = Title::newFromURL( $this->mTarget ); } else { - $this->mTargetObj = NULL; + $this->mTargetObj = null; } if( $this->mRestore || $this->mInvert ) { $timestamps = array(); @@ -642,7 +629,7 @@ class UndeleteForm { $this->showList( $result ); } } else { - $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) ); + $wgOut->addWikiMsg( 'undelete-header' ); } return; } @@ -652,8 +639,15 @@ class UndeleteForm { if( $this->mFile !== null ) { $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile ); // Check if user is allowed to see this file - if( !$file->userCan( File::DELETED_FILE ) ) { - $wgOut->permissionRequired( 'suppressrevision' ); + if ( !$file->exists() ) { + $wgOut->addWikiMsg( 'filedelete-nofile', $this->mFile ); + return; + } else if( !$file->userCan( File::DELETED_FILE ) ) { + if( $file->isDeleted( File::DELETED_RESTRICTED ) ) { + $wgOut->permissionRequired( 'suppressrevision' ); + } else { + $wgOut->permissionRequired( 'deletedtext' ); + } return false; } elseif ( !$wgUser->matchEditToken( $this->mToken, $this->mFile ) ) { $this->showFileConfirmationForm( $this->mFile ); @@ -663,6 +657,11 @@ class UndeleteForm { } } if( $this->mRestore && $this->mAction == "submit" ) { + global $wgUploadMaintenance; + if( $wgUploadMaintenance && $this->mTargetObj && $this->mTargetObj->getNamespace() == NS_FILE ) { + $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", array( 'filedelete-maintenance' ) ); + return; + } return $this->undelete(); } if( $this->mInvert && $this->mAction == "submit" ) { @@ -707,8 +706,12 @@ class UndeleteForm { $wgOut->addHTML( "<ul>\n" ); while( $row = $result->fetchObject() ) { $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); - $link = $sk->makeKnownLinkObj( $undelete, htmlspecialchars( $title->getPrefixedText() ), - 'target=' . $title->getPrefixedUrl() ); + $link = $sk->linkKnown( + $undelete, + htmlspecialchars( $title->getPrefixedText() ), + array(), + array( 'target' => $title->getPrefixedText() ) + ); $revs = wfMsgExt( 'undeleterevisions', array( 'parseinline' ), $wgLang->formatNum( $row->count ) ); @@ -741,14 +744,14 @@ class UndeleteForm { return; } else { $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' ); - $wgOut->addHTML( '<br/>' ); + $wgOut->addHTML( '<br />' ); // and we are allowed to see... } } $wgOut->setPageTitle( wfMsg( 'undeletepage' ) ); - $link = $skin->makeKnownLinkObj( + $link = $skin->linkKnown( SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ), htmlspecialchars( $this->mTargetObj->getPrefixedText() ) ); @@ -763,7 +766,7 @@ class UndeleteForm { $wgOut->addHTML( '<hr />' ); } } else { - $wgOut->addHTML( wfMsgHtml( 'undelete-nodiff' ) ); + $wgOut->addWikiMsg( 'undelete-nodiff' ); } } @@ -774,13 +777,36 @@ class UndeleteForm { $t = htmlspecialchars( $wgLang->time( $timestamp, true ) ); $user = $skin->revUserTools( $rev ); - $wgOut->addHTML( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '</p>' ); + if( $this->mPreview ) { + $openDiv = '<div id="mw-undelete-revision" class="mw-warning">'; + } else { + $openDiv = '<div id="mw-undelete-revision">'; + } + + // Revision delete links + $canHide = $wgUser->isAllowed( 'deleterevision' ); + if( $this->mDiff ) { + $revdlink = ''; // diffs already have revision delete links + } else if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) { + if( !$rev->userCan(Revision::DELETED_RESTRICTED ) ) { + $revdlink = $skin->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops + } else { + $query = array( + 'type' => 'archive', + 'target' => $this->mTargetObj->getPrefixedDBkey(), + 'ids' => $rev->getTimestamp() + ); + $revdlink = $skin->revDeleteLink( $query, + $rev->isDeleted( File::DELETED_RESTRICTED ), $canHide ); + } + } else { + $revdlink = ''; + } + $wgOut->addHTML( $openDiv . $revdlink . wfMsgWikiHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '</div>' ); wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ); if( $this->mPreview ) { - $wgOut->addHTML( "<hr />\n" ); - //Hide [edit]s $popts = $wgOut->parserOptions(); $popts->setEditSection( false ); @@ -797,7 +823,7 @@ class UndeleteForm { Xml::openElement( 'div' ) . Xml::openElement( 'form', array( 'method' => 'post', - 'action' => $self->getLocalURL( "action=submit" ) ) ) . + 'action' => $self->getLocalURL( array( 'action' => 'submit' ) ) ) ) . Xml::element( 'input', array( 'type' => 'hidden', 'name' => 'target', @@ -830,7 +856,7 @@ class UndeleteForm { * @return string HTML */ function showDiff( $previousRev, $currentRev ) { - global $wgOut, $wgUser; + global $wgOut; $diffEngine = new DifferenceEngine(); $diffEngine->showDiffStyle(); @@ -852,39 +878,63 @@ class UndeleteForm { $diffEngine->generateDiffBody( $previousRev->getText(), $currentRev->getText() ) . "</table>" . - "</div>\n" ); - + "</div>\n" + ); } private function diffHeader( $rev, $prefix ) { - global $wgUser, $wgLang, $wgLang; + global $wgUser, $wgLang; $sk = $wgUser->getSkin(); $isDeleted = !( $rev->getId() && $rev->getTitle() ); if( $isDeleted ) { - /// @fixme $rev->getTitle() is null for deleted revs...? + /// @todo Fixme: $rev->getTitle() is null for deleted revs...? $targetPage = SpecialPage::getTitleFor( 'Undelete' ); - $targetQuery = 'target=' . - $this->mTargetObj->getPrefixedUrl() . - '×tamp=' . - wfTimestamp( TS_MW, $rev->getTimestamp() ); + $targetQuery = array( + 'target' => $this->mTargetObj->getPrefixedText(), + 'timestamp' => wfTimestamp( TS_MW, $rev->getTimestamp() ) + ); } else { - /// @fixme getId() may return non-zero for deleted revs... + /// @todo Fixme getId() may return non-zero for deleted revs... $targetPage = $rev->getTitle(); - $targetQuery = 'oldid=' . $rev->getId(); + $targetQuery = array( 'oldid' => $rev->getId() ); + } + // Add show/hide deletion links if available + $canHide = $wgUser->isAllowed( 'deleterevision' ); + if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) { + $del = ' '; + if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { + $del .= $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops + } else { + $query = array( + 'type' => 'archive', + 'target' => $this->mTargetObj->getPrefixedDbkey(), + 'ids' => $rev->getTimestamp() + ); + $del .= $sk->revDeleteLink( $query, + $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide ); + } + } else { + $del = ''; } return '<div id="mw-diff-'.$prefix.'title1"><strong>' . - $sk->makeLinkObj( $targetPage, - wfMsgHtml( 'revisionasof', - $wgLang->timeanddate( $rev->getTimestamp(), true ) ), - $targetQuery ) . - ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) . + $sk->link( + $targetPage, + wfMsgHtml( + 'revisionasof', + htmlspecialchars( $wgLang->timeanddate( $rev->getTimestamp(), true ) ), + htmlspecialchars( $wgLang->date( $rev->getTimestamp(), true ) ), + htmlspecialchars( $wgLang->time( $rev->getTimestamp(), true ) ) + ), + array(), + $targetQuery + ) . '</strong></div>' . '<div id="mw-diff-'.$prefix.'title2">' . - $sk->revUserTools( $rev ) . '<br/>' . + $sk->revUserTools( $rev ) . '<br />' . '</div>' . '<div id="mw-diff-'.$prefix.'title3">' . - $sk->revComment( $rev ) . '<br/>' . + $sk->revComment( $rev ) . $del . '<br />' . '</div>'; } @@ -927,8 +977,11 @@ class UndeleteForm { $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); $wgRequest->response()->header( 'Pragma: no-cache' ); - $store = FileStore::get( 'deleted' ); - $store->stream( $key ); + global $IP; + require_once( "$IP/includes/StreamFile.php" ); + $repo = RepoGroup::singleton()->getLocalRepo(); + $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key; + wfStreamFile( $path ); } private function showHistory( ) { @@ -941,7 +994,7 @@ class UndeleteForm { $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) ); } - $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) ); + $wgOut->wrapWikiMsg( "<div class='mw-undelete-pagetitle'>\n$1</div>\n", array ( 'undeletepagetitle', $this->mTargetObj->getPrefixedText() ) ); $archive = new PageArchive( $this->mTargetObj ); /* @@ -951,12 +1004,14 @@ class UndeleteForm { return; } */ + $wgOut->addHTML( '<div class="mw-undelete-history">' ); if ( $this->mAllowed ) { $wgOut->addWikiMsg( "undeletehistory" ); $wgOut->addWikiMsg( "undeleterevdel" ); } else { $wgOut->addWikiMsg( "undeletehistorynoadmin" ); } + $wgOut->addHTML( '</div>' ); # List all stored revisions $revisions = $archive->listRevisions(); @@ -987,7 +1042,7 @@ class UndeleteForm { if ( $this->mAllowed ) { $titleObj = SpecialPage::getTitleFor( "Undelete" ); - $action = $titleObj->getLocalURL( "action=submit" ); + $action = $titleObj->getLocalURL( array( 'action' => 'submit' ) ); # Start the form here $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) ); $wgOut->addHTML( $top ); @@ -1021,7 +1076,7 @@ class UndeleteForm { Xml::fieldset( wfMsg( 'undelete-fieldset-title' ) ) . Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) . "<tr> - <td colspan='2'>" . + <td colspan='2' class='mw-undelete-extrahelp'>" . wfMsgWikiHtml( 'undeleteextrahelp' ) . "</td> </tr> @@ -1091,20 +1146,13 @@ class UndeleteForm { private function formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) { global $wgUser, $wgLang; - $rev = new Revision( array( - 'page' => $this->mTargetObj->getArticleId(), - 'comment' => $row->ar_comment, - 'user' => $row->ar_user, - 'user_text' => $row->ar_user_text, - 'timestamp' => $row->ar_timestamp, - 'minor_edit' => $row->ar_minor_edit, - 'deleted' => $row->ar_deleted, - 'len' => $row->ar_len ) ); - + $rev = Revision::newFromArchiveRow( $row, + array( 'page' => $this->mTargetObj->getArticleId() ) ); $stxt = ''; $ts = wfTimestamp( TS_MW, $row->ar_timestamp ); + // Build checkboxen... if( $this->mAllowed ) { - if( $this->mInvert){ + if( $this->mInvert ) { if( in_array( $ts, $this->mTargetTimestamp ) ) { $checkBox = Xml::check( "ts$ts"); } else { @@ -1113,41 +1161,61 @@ class UndeleteForm { } else { $checkBox = Xml::check( "ts$ts" ); } + } else { + $checkBox = ''; + } + // Build page & diff links... + if( $this->mCanView ) { $titleObj = SpecialPage::getTitleFor( "Undelete" ); - $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk ); # Last link if( !$rev->userCan( Revision::DELETED_TEXT ) ) { + $pageLink = htmlspecialchars( $wgLang->timeanddate( $ts, true ) ); $last = wfMsgHtml('diff'); } else if( $remaining > 0 || ($earliestLiveTime && $ts > $earliestLiveTime) ) { - $last = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml('diff'), - "target=" . $this->mTargetObj->getPrefixedUrl() . "×tamp=$ts&diff=prev" ); + $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk ); + $last = $sk->linkKnown( + $titleObj, + wfMsgHtml('diff'), + array(), + array( + 'target' => $this->mTargetObj->getPrefixedText(), + 'timestamp' => $ts, + 'diff' => 'prev' + ) + ); } else { + $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk ); $last = wfMsgHtml('diff'); } } else { - $checkBox = ''; - $pageLink = $wgLang->timeanddate( $ts, true ); + $pageLink = htmlspecialchars( $wgLang->timeanddate( $ts, true ) ); $last = wfMsgHtml('diff'); } + // User links $userLink = $sk->revUserTools( $rev ); - - if(!is_null($size = $row->ar_len)) { + // Revision text size + if( !is_null($size = $row->ar_len) ) { $stxt = $sk->formatRevisionSize( $size ); } + // Edit summary $comment = $sk->revComment( $rev ); - $revdlink = ''; - if( $wgUser->isAllowed( 'deleterevision' ) ) { + // Revision delete links + $canHide = $wgUser->isAllowed( 'deleterevision' ); + if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) { if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { - // If revision was hidden from sysops - $revdlink = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml('rev-delundel').')' ); + $revdlink = $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops } else { - $query = array( 'target' => $this->mTargetObj->getPrefixedDBkey(), - 'artimestamp[]' => $ts + $query = array( + 'type' => 'archive', + 'target' => $this->mTargetObj->getPrefixedDBkey(), + 'ids' => $ts ); - $revdlink = $sk->revDeleteLink( $query, $rev->isDeleted( Revision::DELETED_RESTRICTED ) ); + $revdlink = $sk->revDeleteLink( $query, + $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide ); } + } else { + $revdlink = ''; } - return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>"; } @@ -1177,17 +1245,22 @@ class UndeleteForm { ')'; $data = htmlspecialchars( $data ); $comment = $this->getFileComment( $file, $sk ); - $revdlink = ''; - if( $wgUser->isAllowed( 'deleterevision' ) ) { + // Add show/hide deletion links if available + $canHide = $wgUser->isAllowed( 'deleterevision' ); + if( $canHide || ($file->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) { if( !$file->userCan(File::DELETED_RESTRICTED ) ) { - // If revision was hidden from sysops - $revdlink = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml('rev-delundel').')' ); + $revdlink = $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops } else { - $query = array( 'target' => $this->mTargetObj->getPrefixedDBkey(), - 'fileid' => $row->fa_id + $query = array( + 'type' => 'filearchive', + 'target' => $this->mTargetObj->getPrefixedDBkey(), + 'ids' => $row->fa_id ); - $revdlink = $sk->revDeleteLink( $query, $file->isDeleted( File::DELETED_RESTRICTED ) ); + $revdlink = $sk->revDeleteLink( $query, + $file->isDeleted( File::DELETED_RESTRICTED ), $canHide ); } + } else { + $revdlink = ''; } return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n"; } @@ -1199,11 +1272,20 @@ class UndeleteForm { function getPageLink( $rev, $titleObj, $ts, $sk ) { global $wgLang; + $time = htmlspecialchars( $wgLang->timeanddate( $ts, true ) ); + if( !$rev->userCan(Revision::DELETED_TEXT) ) { - return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>'; + return '<span class="history-deleted">' . $time . '</span>'; } else { - $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), - "target=".$this->mTargetObj->getPrefixedUrl()."×tamp=$ts" ); + $link = $sk->linkKnown( + $titleObj, + $time, + array(), + array( + 'target' => $this->mTargetObj->getPrefixedText(), + 'timestamp' => $ts + ) + ); if( $rev->isDeleted(Revision::DELETED_TEXT) ) $link = '<span class="history-deleted">' . $link . '</span>'; return $link; @@ -1220,10 +1302,16 @@ class UndeleteForm { if( !$file->userCan(File::DELETED_FILE) ) { return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>'; } else { - $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), - "target=".$this->mTargetObj->getPrefixedUrl(). - "&file=$key" . - "&token=" . urlencode( $wgUser->editToken( $key ) ) ); + $link = $sk->linkKnown( + $titleObj, + $wgLang->timeanddate( $ts, true ), + array(), + array( + 'target' => $this->mTargetObj->getPrefixedText(), + 'file' => $key, + 'token' => $wgUser->editToken( $key ) + ) + ); if( $file->isDeleted(File::DELETED_FILE) ) $link = '<span class="history-deleted">' . $link . '</span>'; return $link; @@ -1282,7 +1370,7 @@ class UndeleteForm { $wgUser, $this->mComment) ); $skin = $wgUser->getSkin(); - $link = $skin->makeKnownLinkObj( $this->mTargetObj ); + $link = $skin->linkKnown( $this->mTargetObj ); $wgOut->addHTML( wfMsgWikiHtml( 'undeletedpage', $link ) ); } else { $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); diff --git a/includes/specials/SpecialUnlockdb.php b/includes/specials/SpecialUnlockdb.php index a3e8a0c4..fe38a48a 100644 --- a/includes/specials/SpecialUnlockdb.php +++ b/includes/specials/SpecialUnlockdb.php @@ -45,7 +45,7 @@ class DBUnlockForm { $wgOut->setPagetitle( wfMsg( "unlockdb" ) ); $wgOut->addWikiMsg( "unlockdbtext" ); - if ( "" != $err ) { + if ( $err != "" ) { $wgOut->setSubtitle( wfMsg( "formerror" ) ); $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" ); } @@ -55,7 +55,7 @@ class DBUnlockForm { $action = $titleObj->escapeLocalURL( "action=submit" ); $token = htmlspecialchars( $wgUser->editToken() ); - $wgOut->addHTML( <<<END + $wgOut->addHTML( <<<HTML <form id="unlockdb" method="post" action="{$action}"> <table border="0"> @@ -74,7 +74,7 @@ class DBUnlockForm { </table> <input type="hidden" name="wpEditToken" value="{$token}" /> </form> -END +HTML ); } diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php index 406f7944..fe7d7a17 100644 --- a/includes/specials/SpecialUnusedcategories.php +++ b/includes/specials/SpecialUnusedcategories.php @@ -34,7 +34,7 @@ class UnusedCategoriesPage extends QueryPage { function formatResult( $skin, $result ) { $title = Title::makeTitle( NS_CATEGORY, $result->title ); - return $skin->makeLinkObj( $title, $title->getText() ); + return $skin->link( $title, $title->getText() ); } } diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php index fa66555d..9d9868f6 100644 --- a/includes/specials/SpecialUnusedimages.php +++ b/includes/specials/SpecialUnusedimages.php @@ -25,9 +25,19 @@ class UnusedimagesPage extends ImageQueryPage { global $wgCountCategorizedImagesAsUsed, $wgDBtype; $dbr = wfGetDB( DB_SLAVE ); - $epoch = $wgDBtype == 'mysql' ? - 'UNIX_TIMESTAMP(img_timestamp)' : - 'EXTRACT(epoch FROM img_timestamp)'; + switch ($wgDBtype) { + case 'mysql': + $epoch = 'UNIX_TIMESTAMP(img_timestamp)'; + break; + case 'oracle': + $epoch = '((trunc(img_timestamp) - to_date(\'19700101\',\'YYYYMMDD\')) * 86400)'; + break; + case 'sqlite': + $epoch = 'img_timestamp'; + break; + default: + $epoch = 'EXTRACT(epoch FROM img_timestamp)'; + } if ( $wgCountCategorizedImagesAsUsed ) { list( $page, $image, $imagelinks, $categorylinks ) = $dbr->tableNamesN( 'page', 'image', 'imagelinks', 'categorylinks' ); diff --git a/includes/specials/SpecialUnusedtemplates.php b/includes/specials/SpecialUnusedtemplates.php index 89acd09c..6ddbab32 100644 --- a/includes/specials/SpecialUnusedtemplates.php +++ b/includes/specials/SpecialUnusedtemplates.php @@ -33,11 +33,18 @@ class UnusedtemplatesPage extends QueryPage { function formatResult( $skin, $result ) { $title = Title::makeTitle( NS_TEMPLATE, $result->title ); - $pageLink = $skin->makeKnownLinkObj( $title, '', 'redirect=no' ); - $wlhLink = $skin->makeKnownLinkObj( + $pageLink = $skin->linkKnown( + $title, + null, + array(), + array( 'redirect' => 'no' ) + ); + $wlhLink = $skin->linkKnown( SpecialPage::getTitleFor( 'Whatlinkshere' ), wfMsgHtml( 'unusedtemplateswlh' ), - 'target=' . $title->getPrefixedUrl() ); + array(), + array( 'target' => $title->getPrefixedText() ) + ); return wfSpecialList( $pageLink, $wlhLink ); } diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php index 64ab3729..483afdaa 100644 --- a/includes/specials/SpecialUnwatchedpages.php +++ b/includes/specials/SpecialUnwatchedpages.php @@ -44,8 +44,16 @@ class UnwatchedpagesPage extends QueryPage { $nt = Title::makeTitle( $result->namespace, $result->title ); $text = $wgContLang->convert( $nt->getPrefixedText() ); - $plink = $skin->makeKnownLinkObj( $nt, htmlspecialchars( $text ) ); - $wlink = $skin->makeKnownLinkObj( $nt, wfMsgHtml( 'watch' ), 'action=watch' ); + $plink = $skin->linkKnown( + $nt, + htmlspecialchars( $text ) + ); + $wlink = $skin->linkKnown( + $nt, + wfMsgHtml( 'watch' ), + array(), + array( 'action' => 'watch' ) + ); return wfSpecialList( $plink, $wlink ); } diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index 4c5bb160..9569945d 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -2,250 +2,139 @@ /** * @file * @ingroup SpecialPage + * @ingroup Upload + * + * Form for handling uploads and special page. + * */ - -/** - * Entry point - */ -function wfSpecialUpload() { - global $wgRequest; - $form = new UploadForm( $wgRequest ); - $form->execute(); -} - -/** - * implements Special:Upload - * @ingroup SpecialPage - */ -class UploadForm { - const SUCCESS = 0; - const BEFORE_PROCESSING = 1; - const LARGE_FILE_SERVER = 2; - const EMPTY_FILE = 3; - const MIN_LENGTH_PARTNAME = 4; - const ILLEGAL_FILENAME = 5; - const PROTECTED_PAGE = 6; - const OVERWRITE_EXISTING_FILE = 7; - const FILETYPE_MISSING = 8; - const FILETYPE_BADTYPE = 9; - const VERIFICATION_ERROR = 10; - const UPLOAD_VERIFICATION_ERROR = 11; - const UPLOAD_WARNING = 12; - const INTERNAL_ERROR = 13; - - /**#@+ - * @access private - */ - var $mComment, $mLicense, $mIgnoreWarning, $mCurlError; - var $mDestName, $mTempPath, $mFileSize, $mFileProps; - var $mCopyrightStatus, $mCopyrightSource, $mReUpload, $mAction, $mUploadClicked; - var $mSrcName, $mSessionKey, $mStashed, $mDesiredDestName, $mRemoveTempFile, $mSourceType; - var $mDestWarningAck, $mCurlDestHandle; - var $mLocalFile; - - # Placeholders for text injection by hooks (must be HTML) - # extensions should take care to _append_ to the present value - var $uploadFormTextTop; - var $uploadFormTextAfterSummary; - - const SESSION_VERSION = 1; - /**#@-*/ - +class SpecialUpload extends SpecialPage { /** * Constructor : initialise object * Get data POSTed through the form and assign them to the object - * @param $request Data posted. + * @param WebRequest $request Data posted. */ - function UploadForm( &$request ) { - global $wgAllowCopyUploads; - $this->mDesiredDestName = $request->getText( 'wpDestFile' ); - $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ); - $this->mComment = $request->getText( 'wpUploadDescription' ); - $this->mForReUpload = $request->getBool( 'wpForReUpload' ); - $this->mReUpload = $request->getCheck( 'wpReUpload' ); - - if( !$request->wasPosted() ) { - # GET requests just give the main form; no data except destination - # filename and description - return; - } + public function __construct( $request = null ) { + global $wgRequest; - # Placeholders for text injection by hooks (empty per default) - $this->uploadFormTextTop = ""; - $this->uploadFormTextAfterSummary = ""; - $this->mUploadClicked = $request->getCheck( 'wpUpload' ); + parent::__construct( 'Upload', 'upload' ); - $this->mLicense = $request->getText( 'wpLicense' ); - $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' ); - $this->mCopyrightSource = $request->getText( 'wpUploadSource' ); - $this->mWatchthis = $request->getBool( 'wpWatchthis' ); - $this->mSourceType = $request->getText( 'wpSourceType' ); - $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' ); - - $this->mAction = $request->getVal( 'action' ); - - $this->mSessionKey = $request->getInt( 'wpSessionKey' ); - if( !empty( $this->mSessionKey ) && - isset( $_SESSION['wsUploadData'][$this->mSessionKey]['version'] ) && - $_SESSION['wsUploadData'][$this->mSessionKey]['version'] == self::SESSION_VERSION ) { - /** - * Confirming a temporarily stashed upload. - * We don't want path names to be forged, so we keep - * them in the session on the server and just give - * an opaque key to the user agent. - */ - $data = $_SESSION['wsUploadData'][$this->mSessionKey]; - $this->mTempPath = $data['mTempPath']; - $this->mFileSize = $data['mFileSize']; - $this->mSrcName = $data['mSrcName']; - $this->mFileProps = $data['mFileProps']; - $this->mCurlError = 0/*UPLOAD_ERR_OK*/; - $this->mStashed = true; - $this->mRemoveTempFile = false; - } else { - /** - *Check for a newly uploaded file. - */ - if( $wgAllowCopyUploads && $this->mSourceType == 'web' ) { - $this->initializeFromUrl( $request ); - } else { - $this->initializeFromUpload( $request ); - } - } + $this->loadRequest( is_null( $request ) ? $wgRequest : $request ); } - /** - * Initialize the uploaded file from PHP data - * @access private - */ - function initializeFromUpload( $request ) { - $this->mTempPath = $request->getFileTempName( 'wpUploadFile' ); - $this->mFileSize = $request->getFileSize( 'wpUploadFile' ); - $this->mSrcName = $request->getFileName( 'wpUploadFile' ); - $this->mCurlError = $request->getUploadError( 'wpUploadFile' ); - $this->mSessionKey = false; - $this->mStashed = false; - $this->mRemoveTempFile = false; // PHP will handle this - } + /** Misc variables **/ + protected $mRequest; // The WebRequest or FauxRequest this form is supposed to handle + protected $mSourceType; + protected $mUpload; + protected $mLocalFile; + protected $mUploadClicked; + + /** User input variables from the "description" section **/ + public $mDesiredDestName; // The requested target file name + protected $mComment; + protected $mLicense; + + /** User input variables from the root section **/ + protected $mIgnoreWarning; + protected $mWatchThis; + protected $mCopyrightStatus; + protected $mCopyrightSource; + + /** Hidden variables **/ + protected $mDestWarningAck; + protected $mForReUpload; // The user followed an "overwrite this file" link + protected $mCancelUpload; // The user clicked "Cancel and return to upload form" button + protected $mTokenOk; + protected $mUploadSuccessful = false; // Subclasses can use this to determine whether a file was uploaded + + /** Text injection points for hooks not using HTMLForm **/ + public $uploadFormTextTop; + public $uploadFormTextAfterSummary; + /** - * Copy a web file to a temporary file - * @access private + * Initialize instance variables from request and create an Upload handler + * + * @param WebRequest $request The request to extract variables from */ - function initializeFromUrl( $request ) { - global $wgTmpDirectory; - $url = $request->getText( 'wpUploadFileURL' ); - $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' ); - - $this->mTempPath = $local_file; - $this->mFileSize = 0; # Will be set by curlCopy - $this->mCurlError = $this->curlCopy( $url, $local_file ); - $pathParts = explode( '/', $url ); - $this->mSrcName = array_pop( $pathParts ); - $this->mSessionKey = false; - $this->mStashed = false; - - // PHP won't auto-cleanup the file - $this->mRemoveTempFile = file_exists( $local_file ); - } + protected function loadRequest( $request ) { + global $wgUser; - /** - * Safe copy from URL - * Returns true if there was an error, false otherwise - */ - private function curlCopy( $url, $dest ) { - global $wgUser, $wgOut, $wgHTTPProxy; + $this->mRequest = $request; + $this->mSourceType = $request->getVal( 'wpSourceType', 'file' ); + $this->mUpload = UploadBase::createFromRequest( $request ); + $this->mUploadClicked = $request->wasPosted() + && ( $request->getCheck( 'wpUpload' ) + || $request->getCheck( 'wpUploadIgnoreWarning' ) ); - if( !$wgUser->isAllowed( 'upload_by_url' ) ) { - $wgOut->permissionRequired( 'upload_by_url' ); - return true; - } + // Guess the desired name from the filename if not provided + $this->mDesiredDestName = $request->getText( 'wpDestFile' ); + if( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) + $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' ); + $this->mComment = $request->getText( 'wpUploadDescription' ); + $this->mLicense = $request->getText( 'wpLicense' ); - # Maybe remove some pasting blanks :-) - $url = trim( $url ); - if( stripos($url, 'http://') !== 0 && stripos($url, 'ftp://') !== 0 ) { - # Only HTTP or FTP URLs - $wgOut->showErrorPage( 'upload-proto-error', 'upload-proto-error-text' ); - return true; - } - # Open temporary file - $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" ); - if( $this->mCurlDestHandle === false ) { - # Could not open temporary file to write in - $wgOut->showErrorPage( 'upload-file-error', 'upload-file-error-text'); - return true; - } + $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' ); + $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ) + || $request->getCheck( 'wpUploadIgnoreWarning' ); + $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $wgUser->isLoggedIn(); + $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' ); + $this->mCopyrightSource = $request->getText( 'wpUploadSource' ); - $ch = curl_init(); - curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug - curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout - curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed - curl_setopt( $ch, CURLOPT_URL, $url); - if( $wgHTTPProxy ) { - curl_setopt( $ch, CURLOPT_PROXY, $wgHTTPProxy ); - } - curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) ); - curl_exec( $ch ); - $error = curl_errno( $ch ) ? true : false; - $errornum = curl_errno( $ch ); - // if ( $error ) print curl_error ( $ch ) ; # Debugging output - curl_close( $ch ); - - fclose( $this->mCurlDestHandle ); - unset( $this->mCurlDestHandle ); - if( $error ) { - unlink( $dest ); - if( wfEmptyMsg( "upload-curl-error$errornum", wfMsg("upload-curl-error$errornum") ) ) - $wgOut->showErrorPage( 'upload-misc-error', 'upload-misc-error-text' ); - else - $wgOut->showErrorPage( "upload-curl-error$errornum", "upload-curl-error$errornum-text" ); - } - return $error; + $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file + $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' ) + || $request->getCheck( 'wpReUpload' ); // b/w compat + + // If it was posted check for the token (no remote POST'ing with user credentials) + $token = $request->getVal( 'wpEditToken' ); + if( $this->mSourceType == 'file' && $token == null ) { + // Skip token check for file uploads as that can't be faked via JS... + // Some client-side tools don't expect to need to send wpEditToken + // with their submissions, as that's new in 1.16. + $this->mTokenOk = true; + } else { + $this->mTokenOk = $wgUser->matchEditToken( $token ); + } + + $this->uploadFormTextTop = ''; + $this->uploadFormTextAfterSummary = ''; } /** - * Callback function for CURL-based web transfer - * Write data to file unless we've passed the length limit; - * if so, abort immediately. - * @access private + * This page can be shown if uploading is enabled. + * Handle permission checking elsewhere in order to be able to show + * custom error messages. + * + * @param User $user + * @return bool */ - function uploadCurlCallback( $ch, $data ) { - global $wgMaxUploadSize; - $length = strlen( $data ); - $this->mFileSize += $length; - if( $this->mFileSize > $wgMaxUploadSize ) { - return 0; - } - fwrite( $this->mCurlDestHandle, $data ); - return $length; + public function userCanExecute( $user ) { + return UploadBase::isEnabled() && parent::userCanExecute( $user ); } /** - * Start doing stuff - * @access public + * Special page entry point */ - function execute() { - global $wgUser, $wgOut; - global $wgEnableUploads; + public function execute( $par ) { + global $wgUser, $wgOut, $wgRequest; - # Check php's file_uploads setting - if( !wfIniGetBool( 'file_uploads' ) ) { - $wgOut->showErrorPage( 'uploaddisabled', 'php-uploaddisabledtext', array( $this->mDesiredDestName ) ); - return; - } + $this->setHeaders(); + $this->outputHeader(); # Check uploading enabled - if( !$wgEnableUploads ) { - $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) ); + if( !UploadBase::isEnabled() ) { + $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext' ); return; } # Check permissions + global $wgGroupPermissions; if( !$wgUser->isAllowed( 'upload' ) ) { - if( !$wgUser->isLoggedIn() ) { + if( !$wgUser->isLoggedIn() && ( $wgGroupPermissions['user']['upload'] + || $wgGroupPermissions['autoconfirmed']['upload'] ) ) { + // Custom message if logged-in users without any special rights can upload $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); } else { $wgOut->permissionRequired( 'upload' ); @@ -259,459 +148,490 @@ class UploadForm { return; } + # Check whether we actually want to allow changing stuff if( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - if( $this->mReUpload ) { - if( !$this->unsaveUploadedFile() ) { + # Unsave the temporary file in case this was a cancelled upload + if ( $this->mCancelUpload ) { + if ( !$this->unsaveUploadedFile() ) + # Something went wrong, so unsaveUploadedFile showed a warning return; - } - # Because it is probably checked and shouldn't be - $this->mIgnoreWarning = false; - - $this->mainUploadForm(); - } else if( 'submit' == $this->mAction || $this->mUploadClicked ) { + } + + # Process upload or show a form + if ( $this->mTokenOk && !$this->mCancelUpload + && ( $this->mUpload && $this->mUploadClicked ) ) { $this->processUpload(); } else { - $this->mainUploadForm(); + # Backwards compatibility hook + if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) + { + wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); + return; + } + + $this->showUploadForm( $this->getUploadForm() ); } - $this->cleanupTempFile(); + # Cleanup + if ( $this->mUpload ) + $this->mUpload->cleanupTempFile(); } /** - * Do the upload - * Checks are made in SpecialUpload::execute() + * Show the main upload form * - * @access private + * @param mixed $form An HTMLForm instance or HTML string to show */ - function processUpload(){ - global $wgUser, $wgOut, $wgFileExtensions, $wgLang; - $details = null; - $value = null; - $value = $this->internalProcessUpload( $details ); - - switch($value) { - case self::SUCCESS: - $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() ); - break; - - case self::BEFORE_PROCESSING: - break; - - case self::LARGE_FILE_SERVER: - $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) ); - break; - - case self::EMPTY_FILE: - $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) ); - break; - - case self::MIN_LENGTH_PARTNAME: - $this->mainUploadForm( wfMsgHtml( 'minlength1' ) ); - break; - - case self::ILLEGAL_FILENAME: - $filtered = $details['filtered']; - $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) ); - break; - - case self::PROTECTED_PAGE: - $wgOut->showPermissionsErrorPage( $details['permissionserrors'] ); - break; + protected function showUploadForm( $form ) { + # Add links if file was previously deleted + if ( !$this->mDesiredDestName ) { + $this->showViewDeletedLinks(); + } + + if ( $form instanceof HTMLForm ) { + $form->show(); + } else { + global $wgOut; + $wgOut->addHTML( $form ); + } + + } - case self::OVERWRITE_EXISTING_FILE: - $errorText = $details['overwrite']; - $this->uploadError( $wgOut->parse( $errorText ) ); - break; + /** + * 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 + * @return UploadForm + */ + protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) { + global $wgOut; + + # Initialize form + $form = new UploadForm( array( + 'watch' => $this->getWatchCheck(), + 'forreupload' => $this->mForReUpload, + 'sessionkey' => $sessionKey, + 'hideignorewarning' => $hideIgnoreWarning, + 'destwarningack' => (bool)$this->mDestWarningAck, + + 'texttop' => $this->uploadFormTextTop, + 'textaftersummary' => $this->uploadFormTextAfterSummary, + 'destfile' => $this->mDesiredDestName, + ) ); + $form->setTitle( $this->getTitle() ); + + # Check the token, but only if necessary + if( !$this->mTokenOk && !$this->mCancelUpload + && ( $this->mUpload && $this->mUploadClicked ) ) { + $form->addPreText( wfMsgExt( 'session_fail_preview', 'parseinline' ) ); + } + + # Add text to form + $form->addPreText( '<div id="uploadtext">' . + wfMsgExt( 'uploadtext', 'parse', array( $this->mDesiredDestName ) ) . + '</div>' ); + # Add upload error message + $form->addPreText( $message ); + + # Add footer to form + $uploadFooter = wfMsgNoTrans( 'uploadfooter' ); + if ( $uploadFooter != '-' && !wfEmptyMsg( 'uploadfooter', $uploadFooter ) ) { + $form->addPostText( '<div id="mw-upload-footer-message">' + . $wgOut->parse( $uploadFooter ) . "</div>\n" ); + } + + return $form; - case self::FILETYPE_MISSING: - $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) ); - break; + } - case self::FILETYPE_BADTYPE: - $finalExt = $details['finalExt']; - $this->uploadError( - wfMsgExt( 'filetype-banned-type', - array( 'parseinline' ), - htmlspecialchars( $finalExt ), - $wgLang->commaList( $wgFileExtensions ), - $wgLang->formatNum( count($wgFileExtensions) ) + /** + * Shows the "view X deleted revivions link"" + */ + protected function showViewDeletedLinks() { + global $wgOut, $wgUser; + + $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); + // Show a subtitle link to deleted revisions (to sysops et al only) + if( $title instanceof Title ) { + $count = $title->isDeleted(); + if ( $count > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) { + $link = wfMsgExt( + $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted', + array( 'parse', 'replaceafter' ), + $wgUser->getSkin()->linkKnown( + SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ), + wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count ) ) ); - break; - - case self::VERIFICATION_ERROR: - $veri = $details['veri']; - $this->uploadError( $veri->toString() ); - break; - - case self::UPLOAD_VERIFICATION_ERROR: - $error = $details['error']; - $this->uploadError( $error ); - break; - - case self::UPLOAD_WARNING: - $warning = $details['warning']; - $this->uploadWarning( $warning ); - break; - - case self::INTERNAL_ERROR: - $internal = $details['internal']; - $this->showError( $internal ); - break; + $wgOut->addHTML( "<div id=\"contentSub2\">{$link}</div>" ); + } + } - default: - throw new MWException( __METHOD__ . ": Unknown value `{$value}`" ); - } + // Show the relevant lines from deletion log (for still deleted files only) + if( $title instanceof Title && $title->isDeletedQuick() && !$title->exists() ) { + $this->showDeletionLog( $wgOut, $title->getPrefixedText() ); + } } /** - * Really do the upload - * Checks are made in SpecialUpload::execute() + * Stashes the upload and shows the main upload form. * - * @param array $resultDetails contains result-specific dict of additional values + * Note: only errors that can be handled by changing the name or + * description should be redirected here. It should be assumed that the + * file itself is sane and has passed UploadBase::verifyFile. This + * essentially means that UploadBase::VERIFICATION_ERROR and + * UploadBase::EMPTY_FILE should not be passed here. * - * @access private + * @param string $message HTML message to be passed to mainUploadForm + */ + protected function showRecoverableUploadError( $message ) { + $sessionKey = $this->mUpload->stashSession(); + $message = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" . + '<div class="error">' . $message . "</div>\n"; + + $form = $this->getUploadForm( $message, $sessionKey ); + $form->setSubmitText( wfMsg( 'upload-tryagain' ) ); + $this->showUploadForm( $form ); + } + /** + * Stashes the upload, shows the main form, but adds an "continue anyway button". + * Also checks whether there are actually warnings to display. + * + * @param array $warnings + * @return boolean true if warnings were displayed, false if there are no + * warnings and the should continue processing like there was no warning */ - function internalProcessUpload( &$resultDetails ) { + protected function showUploadWarning( $warnings ) { global $wgUser; - if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) ) + # 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 ) ) ) { - wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" ); - return self::BEFORE_PROCESSING; + return false; } - /** - * If there was no filename or a zero size given, give up quick. - */ - if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) { - return self::EMPTY_FILE; - } + $sessionKey = $this->mUpload->stashSession(); - /* Check for curl error */ - if( $this->mCurlError ) { - return self::BEFORE_PROCESSING; - } + $sk = $wgUser->getSkin(); - /** - * Chop off any directories in the given filename. Then - * filter out illegal characters, and try to make a legible name - * out of it. We'll strip some silently that Title would die on. - */ - if( $this->mDesiredDestName ) { - $basename = $this->mDesiredDestName; - } else { - $basename = $this->mSrcName; - } - $filtered = wfStripIllegalFilenameChars( $basename ); - - /* Normalize to title form before we do any further processing */ - $nt = Title::makeTitleSafe( NS_FILE, $filtered ); - if( is_null( $nt ) ) { - $resultDetails = array( 'filtered' => $filtered ); - return self::ILLEGAL_FILENAME; + $warningHtml = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" + . '<ul class="warning">'; + foreach( $warnings as $warning => $args ) { + $msg = ''; + if( $warning == 'exists' ) { + $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n"; + } elseif( $warning == 'duplicate' ) { + $msg = self::getDupeWarning( $args ); + } elseif( $warning == 'duplicate-archive' ) { + $msg = "\t<li>" . wfMsgExt( 'file-deleted-duplicate', 'parseinline', + array( Title::makeTitle( NS_FILE, $args )->getPrefixedText() ) ) + . "</li>\n"; + } else { + if ( $args === true ) + $args = array(); + elseif ( !is_array( $args ) ) + $args = array( $args ); + $msg = "\t<li>" . wfMsgExt( $warning, 'parseinline', $args ) . "</li>\n"; + } + $warningHtml .= $msg; } - $filtered = $nt->getDBkey(); + $warningHtml .= "</ul>\n"; + $warningHtml .= wfMsgExt( 'uploadwarning-text', 'parse' ); + + $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true ); + $form->setSubmitText( wfMsg( 'upload-tryagain' ) ); + $form->addButton( 'wpUploadIgnoreWarning', wfMsg( 'ignorewarning' ) ); + $form->addButton( 'wpCancelUpload', wfMsg( 'reuploaddesc' ) ); + + $this->showUploadForm( $form ); - /** - * We'll want to blacklist against *any* 'extension', and use - * only the final one for the whitelist. - */ - list( $partname, $ext ) = $this->splitExtensions( $filtered ); - - if( count( $ext ) ) { - $finalExt = $ext[count( $ext ) - 1]; - } else { - $finalExt = ''; - } + # Indicate that we showed a form + return true; + } - # If there was more than one "extension", reassemble the base - # filename to prevent bogus complaints about length - if( count( $ext ) > 1 ) { - for( $i = 0; $i < count( $ext ) - 1; $i++ ) - $partname .= '.' . $ext[$i]; - } + /** + * Show the upload form with error message, but do not stash the file. + * + * @param string $message + */ + protected function showUploadError( $message ) { + $message = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" . + '<div class="error">' . $message . "</div>\n"; + $this->showUploadForm( $this->getUploadForm( $message ) ); + } - if( strlen( $partname ) < 1 ) { - return self::MIN_LENGTH_PARTNAME; - } + /** + * Do the upload. + * Checks are made in SpecialUpload::execute() + */ + protected function processUpload() { + global $wgUser, $wgOut; - $this->mLocalFile = wfLocalFile( $nt ); - $this->mDestName = $this->mLocalFile->getName(); - - /** - * If the image is protected, non-sysop users won't be able - * to modify it by uploading a new revision. - */ - $permErrors = $nt->getUserPermissionsErrors( 'edit', $wgUser ); - $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $wgUser ); - $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $wgUser ) ); - - if( $permErrors || $permErrorsUpload || $permErrorsCreate ) { - // merge all the problems into one list, avoiding duplicates - $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) ); - $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) ); - $resultDetails = array( 'permissionserrors' => $permErrors ); - return self::PROTECTED_PAGE; + // Verify permissions + $permErrors = $this->mUpload->verifyPermissions( $wgUser ); + if( $permErrors !== true ) { + $wgOut->showPermissionsErrorPage( $permErrors ); + return; } - /** - * In some cases we may forbid overwriting of existing files. - */ - $overwrite = $this->checkOverwrite( $this->mDestName ); - if( $overwrite !== true ) { - $resultDetails = array( 'overwrite' => $overwrite ); - return self::OVERWRITE_EXISTING_FILE; + // Fetch the file if required + $status = $this->mUpload->fetchFile(); + if( !$status->isOK() ) { + $this->showUploadForm( $this->getUploadForm( $wgOut->parse( $status->getWikiText() ) ) ); + return; } - /* Don't allow users to override the blacklist (check file extension) */ - global $wgCheckFileExtensions, $wgStrictFileExtensions; - global $wgFileExtensions, $wgFileBlacklist; - if ($finalExt == '') { - return self::FILETYPE_MISSING; - } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || - ($wgCheckFileExtensions && $wgStrictFileExtensions && - !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) { - $resultDetails = array( 'finalExt' => $finalExt ); - return self::FILETYPE_BADTYPE; + // Deprecated backwards compatibility hook + if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) ) + { + wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" ); + return array( 'status' => UploadBase::BEFORE_PROCESSING ); } - /** - * Look at the contents of the file; if we can recognize the - * type but it's corrupt or data of the wrong type, we should - * probably not accept it. - */ - if( !$this->mStashed ) { - $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $finalExt ); - $this->checkMacBinary(); - $veri = $this->verify( $this->mTempPath, $finalExt ); - - if( $veri !== true ) { //it's a wiki error... - $resultDetails = array( 'veri' => $veri ); - return self::VERIFICATION_ERROR; - } - /** - * Provide an opportunity for extensions to add further checks - */ - $error = ''; - if( !wfRunHooks( 'UploadVerification', - array( $this->mDestName, $this->mTempPath, &$error ) ) ) { - $resultDetails = array( 'error' => $error ); - return self::UPLOAD_VERIFICATION_ERROR; - } + // Upload verification + $details = $this->mUpload->verifyUpload(); + if ( $details['status'] != UploadBase::OK ) { + $this->processVerificationError( $details ); + return; } + $this->mLocalFile = $this->mUpload->getLocalFile(); - /** - * Check for non-fatal conditions - */ - if ( ! $this->mIgnoreWarning ) { - $warning = ''; - - $comparableName = str_replace( ' ', '_', $basename ); - global $wgCapitalLinks, $wgContLang; - if ( $wgCapitalLinks ) { - $comparableName = $wgContLang->ucfirst( $comparableName ); - } - - if( $comparableName !== $filtered ) { - $warning .= '<li>'.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mDestName ) ).'</li>'; - } - - global $wgCheckFileExtensions; - if ( $wgCheckFileExtensions ) { - if ( !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) { - global $wgLang; - $warning .= '<li>' . - wfMsgExt( 'filetype-unwanted-type', - array( 'parseinline' ), - htmlspecialchars( $finalExt ), - $wgLang->commaList( $wgFileExtensions ), - $wgLang->formatNum( count($wgFileExtensions) ) - ) . '</li>'; - } - } - - global $wgUploadSizeWarning; - if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) { - $skin = $wgUser->getSkin(); - $wsize = $skin->formatSize( $wgUploadSizeWarning ); - $asize = $skin->formatSize( $this->mFileSize ); - $warning .= '<li>' . wfMsgHtml( 'large-file', $wsize, $asize ) . '</li>'; - } - if ( $this->mFileSize == 0 ) { - $warning .= '<li>'.wfMsgHtml( 'emptyfile' ).'</li>'; - } - - if ( !$this->mDestWarningAck ) { - $warning .= self::getExistsWarning( $this->mLocalFile ); - } - - $warning .= $this->getDupeWarning( $this->mTempPath, $finalExt, $nt ); - - if( $warning != '' ) { - /** - * Stash the file in a temporary location; the user can choose - * to let it through and we'll complete the upload then. - */ - $resultDetails = array( 'warning' => $warning ); - return self::UPLOAD_WARNING; + // Check warnings if necessary + if( !$this->mIgnoreWarning ) { + $warnings = $this->mUpload->checkWarnings(); + if( $this->showUploadWarning( $warnings ) ) { + return; } } - /** - * Try actually saving the thing... - * It will show an error form on failure. - */ + // Get the page text if this is not a reupload if( !$this->mForReUpload ) { $pageText = self::getInitialPageText( $this->mComment, $this->mLicense, $this->mCopyrightStatus, $this->mCopyrightSource ); - } - - $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, - File::DELETE_SOURCE, $this->mFileProps ); - if ( !$status->isGood() ) { - $resultDetails = array( 'internal' => $status->getWikiText() ); - return self::INTERNAL_ERROR; } else { - if ( $this->mWatchthis ) { - global $wgUser; - $wgUser->addWatch( $this->mLocalFile->getTitle() ); - } - // Success, redirect to description page - $img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere? - wfRunHooks( 'UploadComplete', array( &$this ) ); - return self::SUCCESS; + $pageText = false; } + $status = $this->mUpload->performUpload( $this->mComment, $pageText, $this->mWatchthis, $wgUser ); + if ( !$status->isGood() ) { + $this->showUploadError( $wgOut->parse( $status->getWikiText() ) ); + return; + } + + // Success, redirect to description page + $this->mUploadSuccessful = true; + wfRunHooks( 'SpecialUploadComplete', array( &$this ) ); + $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() ); + } /** - * Do existence checks on a file and produce a warning - * This check is static and can be done pre-upload via AJAX - * Returns an HTML fragment consisting of one or more LI elements if there is a warning - * Returns an empty string if there is no warning + * Get the initial image page text based on a comment and optional file status information */ - static function getExistsWarning( $file ) { - global $wgUser, $wgContLang; - // Check for uppercase extension. We allow these filenames but check if an image - // with lowercase extension exists already - $warning = ''; - $align = $wgContLang->isRtl() ? 'left' : 'right'; - - if( strpos( $file->getName(), '.' ) == false ) { - $partname = $file->getName(); - $rawExtension = ''; + public static function getInitialPageText( $comment = '', $license = '', $copyStatus = '', $source = '' ) { + global $wgUseCopyrightUpload; + if ( $wgUseCopyrightUpload ) { + $licensetxt = ''; + if ( $license != '' ) { + $licensetxt = '== ' . wfMsgForContent( 'license-header' ) . " ==\n" . '{{' . $license . '}}' . "\n"; + } + $pageText = '== ' . wfMsgForContent ( 'filedesc' ) . " ==\n" . $comment . "\n" . + '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" . + "$licensetxt" . + '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ; } else { - $n = strrpos( $file->getName(), '.' ); - $rawExtension = substr( $file->getName(), $n + 1 ); - $partname = substr( $file->getName(), 0, $n ); + if ( $license != '' ) { + $filedesc = $comment == '' ? '' : '== ' . wfMsgForContent ( 'filedesc' ) . " ==\n" . $comment . "\n"; + $pageText = $filedesc . + '== ' . wfMsgForContent ( 'license-header' ) . " ==\n" . '{{' . $license . '}}' . "\n"; + } else { + $pageText = $comment; + } } + return $pageText; + } - $sk = $wgUser->getSkin(); + /** + * See if we should check the 'watch this page' checkbox on the form + * based on the user's preferences and whether we're being asked + * to create a new file or update an existing one. + * + * In the case where 'watch edits' is off but 'watch creations' is on, + * we'll leave the box unchecked. + * + * Note that the page target can be changed *on the form*, so our check + * state can get out of sync. + */ + protected function getWatchCheck() { + global $wgUser; + if( $wgUser->getOption( 'watchdefault' ) ) { + // Watch all edits! + return true; + } - if ( $rawExtension != $file->getExtension() ) { - // We're not using the normalized form of the extension. - // Normal form is lowercase, using most common of alternate - // extensions (eg 'jpg' rather than 'JPEG'). - // - // Check for another file using the normalized form... - $nt_lc = Title::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() ); - $file_lc = wfLocalFile( $nt_lc ); + $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 $local->getTitle()->userIsWatching(); } else { - $file_lc = false; + // New page should get watched if that's our option. + return $wgUser->getOption( 'watchcreations' ); } + } - if( $file->exists() ) { - $dlink = $sk->makeKnownLinkObj( $file->getTitle() ); - if ( $file->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline' ), - $file->getName(), $align, array(), false, true ); - } elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) { - $icon = $file->iconThumb(); - $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' . - $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>'; - } else { - $dlink2 = ''; - } - $warning .= '<li>' . wfMsgExt( 'fileexists', array('parseinline','replaceafter'), $dlink ) . '</li>' . $dlink2; - - } elseif( $file->getTitle()->getArticleID() ) { - $lnk = $sk->makeKnownLinkObj( $file->getTitle(), '', 'redirect=no' ); - $warning .= '<li>' . wfMsgExt( 'filepageexists', array( 'parseinline', 'replaceafter' ), $lnk ) . '</li>'; - } elseif ( $file_lc && $file_lc->exists() ) { - # Check if image with lowercase extension exists. - # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension - $dlink = $sk->makeKnownLinkObj( $nt_lc ); - if ( $file_lc->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline' ), - $nt_lc->getText(), $align, array(), false, true ); - } elseif ( !$file_lc->allowInlineDisplay() && $file_lc->isSafeFile() ) { - $icon = $file_lc->iconThumb(); - $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' . - $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>'; - } else { - $dlink2 = ''; - } + /** + * Provides output to the user for a result of UploadBase::verifyUpload + * + * @param array $details Result of UploadBase::verifyUpload + */ + protected function processVerificationError( $details ) { + global $wgFileExtensions, $wgLang; - $warning .= '<li>' . - wfMsgExt( 'fileexists-extension', 'parsemag', - $file->getTitle()->getPrefixedText(), $dlink ) . - '</li>' . $dlink2; + switch( $details['status'] ) { - } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' ) - && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) ) - { - # Check for filenames like 50px- or 180px-, these are mostly thumbnails - $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension ); - $file_thb = wfLocalFile( $nt_thb ); - if ($file_thb->exists() ) { - # Check if an image without leading '180px-' (or similiar) exists - $dlink = $sk->makeKnownLinkObj( $nt_thb); - if ( $file_thb->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $nt_thb, - wfMsgExt( 'fileexists-thumb', 'parseinline' ), - $nt_thb->getText(), $align, array(), false, true ); - } elseif ( !$file_thb->allowInlineDisplay() && $file_thb->isSafeFile() ) { - $icon = $file_thb->iconThumb(); - $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' . - $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . - $dlink . '</div>'; + /** Statuses that only require name changing **/ + case UploadBase::MIN_LENGTH_PARTNAME: + $this->showRecoverableUploadError( wfMsgHtml( 'minlength1' ) ); + break; + case UploadBase::ILLEGAL_FILENAME: + $this->showRecoverableUploadError( wfMsgExt( 'illegalfilename', + 'parseinline', $details['filtered'] ) ); + break; + case UploadBase::OVERWRITE_EXISTING_FILE: + $this->showRecoverableUploadError( wfMsgExt( $details['overwrite'], + 'parseinline' ) ); + break; + case UploadBase::FILETYPE_MISSING: + $this->showRecoverableUploadError( wfMsgExt( 'filetype-missing', + 'parseinline' ) ); + break; + + /** Statuses that require reuploading **/ + case UploadBase::EMPTY_FILE: + $this->showUploadForm( $this->getUploadForm( wfMsgHtml( 'emptyfile' ) ) ); + break; + case UploadBase::FILETYPE_BADTYPE: + $finalExt = $details['finalExt']; + $this->showUploadError( + wfMsgExt( 'filetype-banned-type', + array( 'parseinline' ), + htmlspecialchars( $finalExt ), + implode( + wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ), + $wgFileExtensions + ), + $wgLang->formatNum( count( $wgFileExtensions ) ) + ) + ); + break; + case UploadBase::VERIFICATION_ERROR: + unset( $details['status'] ); + $code = array_shift( $details['details'] ); + $this->showUploadError( wfMsgExt( $code, 'parseinline', $details['details'] ) ); + break; + case UploadBase::HOOK_ABORTED: + if ( is_array( $details['error'] ) ) { # allow hooks to return error details in an array + $args = $details['error']; + $error = array_shift( $args ); } else { - $dlink2 = ''; + $error = $details['error']; + $args = null; } - $warning .= '<li>' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) . - '</li>' . $dlink2; - } else { - # Image w/o '180px-' does not exists, but we do not like these filenames - $warning .= '<li>' . wfMsgExt( 'file-thumbnail-no', 'parseinline' , - substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '</li>'; - } + $this->showUploadError( wfMsgExt( $error, 'parseinline', $args ) ); + break; + default: + throw new MWException( __METHOD__ . ": Unknown value `{$details['status']}`" ); } + } - $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist(); - # Do the match - foreach( $filenamePrefixBlacklist as $prefix ) { - if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) { - $warning .= '<li>' . wfMsgExt( 'filename-bad-prefix', 'parseinline', $prefix ) . '</li>'; - break; - } + /** + * Remove a temporarily kept file stashed by saveTempUploadedFile(). + * @access private + * @return success + */ + protected function unsaveUploadedFile() { + global $wgOut; + if ( !( $this->mUpload instanceof UploadFromStash ) ) + return true; + $success = $this->mUpload->unsaveUploadedFile(); + if ( ! $success ) { + $wgOut->showFileDeleteError( $this->mUpload->getTempPath() ); + return false; + } else { + return true; } + } + + /*** Functions for formatting warnings ***/ + + /** + * 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 + */ + public static function getExistsWarning( $exists ) { + global $wgUser, $wgContLang; + + if ( !$exists ) + return ''; - if ( $file->wasDeleted() && !$file->exists() ) { + $file = $exists['file']; + $filename = $file->getTitle()->getPrefixedText(); + $warning = ''; + + $sk = $wgUser->getSkin(); + + if( $exists['warning'] == 'exists' ) { + // Exact match + $warning = wfMsgExt( 'fileexists', 'parseinline', $filename ); + } elseif( $exists['warning'] == 'page-exists' ) { + // Page exists but file does not + $warning = wfMsgExt( 'filepageexists', 'parseinline', $filename ); + } elseif ( $exists['warning'] == 'exists-normalized' ) { + $warning = wfMsgExt( 'fileexists-extension', 'parseinline', $filename, + $exists['normalizedFile']->getTitle()->getPrefixedText() ); + } elseif ( $exists['warning'] == 'thumb' ) { + // Swapped argument order compared with other messages for backwards compatibility + $warning = wfMsgExt( 'fileexists-thumbnail-yes', 'parseinline', + $exists['thumbFile']->getTitle()->getPrefixedText(), $filename ); + } elseif ( $exists['warning'] == 'thumb-name' ) { + // Image w/o '180px-' does not exists, but we do not like these filenames + $name = $file->getName(); + $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 ); + $warning = wfMsgExt( 'file-thumbnail-no', 'parseinline', $badPart ); + } elseif ( $exists['warning'] == 'bad-prefix' ) { + $warning = wfMsgExt( 'filename-bad-prefix', 'parseinline', $exists['prefix'] ); + } elseif ( $exists['warning'] == 'was-deleted' ) { # If the file existed before and was deleted, warn the user of this - # Don't bother doing so if the file exists now, however $ltitle = SpecialPage::getTitleFor( 'Log' ); - $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), - 'type=delete&page=' . $file->getTitle()->getPrefixedUrl() ); - $warning .= '<li>' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '</li>'; + $llink = $sk->linkKnown( + $ltitle, + wfMsgHtml( 'deletionlog' ), + array(), + array( + 'type' => 'delete', + 'page' => $filename + ) + ); + $warning = wfMsgWikiHtml( 'filewasdeleted', $llink ); } + return $warning; } @@ -721,7 +641,7 @@ class UploadForm { * @param string local filename, e.g. 'file exists', 'non-descriptive filename' * @return array list of warning messages */ - static function ajaxGetExistsWarning( $filename ) { + public static function ajaxGetExistsWarning( $filename ) { $file = wfFindFile( $filename ); if( !$file ) { // Force local file so we have an object to do further checks against @@ -730,297 +650,181 @@ class UploadForm { } $s = ' '; if ( $file ) { - $warning = self::getExistsWarning( $file ); + $exists = UploadBase::getExistsWarning( $file ); + $warning = self::getExistsWarning( $exists ); if ( $warning !== '' ) { - $s = "<ul>$warning</ul>"; + $s = "<div>$warning</div>"; } } return $s; } /** - * Render a preview of a given license for the AJAX preview on upload - * - * @param string $license - * @return string - */ - public static function ajaxGetLicensePreview( $license ) { - global $wgParser, $wgUser; - $text = '{{' . $license . '}}'; - $title = Title::makeTitle( NS_FILE, 'Sample.jpg' ); - $options = ParserOptions::newFromUser( $wgUser ); - - // Expand subst: first, then live templates... - $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options ); - $output = $wgParser->parse( $text, $title, $options ); - - return $output->getText(); - } - - /** - * Check for duplicate files and throw up a warning before the upload - * completes. + * Construct a warning and a gallery from an array of duplicate files. */ - function getDupeWarning( $tempfile, $extension, $destinationTitle ) { - $hash = File::sha1Base36( $tempfile ); - $dupes = RepoGroup::singleton()->findBySha1( $hash ); - $archivedImage = new ArchivedFile( null, 0, $hash.".$extension" ); + public static function getDupeWarning( $dupes ) { if( $dupes ) { global $wgOut; $msg = "<gallery>"; foreach( $dupes as $file ) { $title = $file->getTitle(); - # Don't throw the warning when the titles are the same, it's a reupload - # and highly redundant. - if ( !$title->equals( $destinationTitle ) || !$this->mForReUpload ) { - $msg .= $title->getPrefixedText() . - "|" . $title->getText() . "\n"; - } + $msg .= $title->getPrefixedText() . + "|" . $title->getText() . "\n"; } $msg .= "</gallery>"; return "<li>" . wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) . $wgOut->parse( $msg ) . "</li>\n"; - } elseif ( $archivedImage->getID() > 0 ) { - global $wgOut; - $name = Title::makeTitle( NS_FILE, $archivedImage->getName() )->getPrefixedText(); - return Xml::tags( 'li', null, wfMsgExt( 'file-deleted-duplicate', array( 'parseinline' ), array( $name ) ) ); } else { return ''; } } - /** - * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]] - * - * @return array list of prefixes - */ - public static function getFilenamePrefixBlacklist() { - $blacklist = array(); - $message = wfMsgForContent( 'filename-prefix-blacklist' ); - if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) { - $lines = explode( "\n", $message ); - foreach( $lines as $line ) { - // Remove comment lines - $comment = substr( trim( $line ), 0, 1 ); - if ( $comment == '#' || $comment == '' ) { - continue; - } - // Remove additional comments after a prefix - $comment = strpos( $line, '#' ); - if ( $comment > 0 ) { - $line = substr( $line, 0, $comment-1 ); - } - $blacklist[] = trim( $line ); - } - } - return $blacklist; - } +} - /** - * Stash a file in a temporary directory for later processing - * after the user has confirmed it. - * - * If the user doesn't explicitly cancel or accept, these files - * can accumulate in the temp directory. - * - * @param string $saveName - the destination filename - * @param string $tempName - the source temporary file to save - * @return string - full path the stashed file, or false on failure - * @access private - */ - function saveTempUploadedFile( $saveName, $tempName ) { - global $wgOut; - $repo = RepoGroup::singleton()->getLocalRepo(); - $status = $repo->storeTemp( $saveName, $tempName ); - if ( !$status->isGood() ) { - $this->showError( $status->getWikiText() ); - return false; - } else { - return $status->value; - } - } +/** + * Sub class of HTMLForm that provides the form section of SpecialUpload + */ +class UploadForm extends HTMLForm { + protected $mWatch; + protected $mForReUpload; + protected $mSessionKey; + protected $mHideIgnoreWarning; + protected $mDestWarningAck; + protected $mDestFile; + + protected $mTextTop; + protected $mTextAfterSummary; + + protected $mSourceIds; - /** - * Stash a file in a temporary directory for later processing, - * and save the necessary descriptive info into the session. - * Returns a key value which will be passed through a form - * to pick up the path info on a later invocation. - * - * @return int - * @access private - */ - function stashSession() { - $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath ); + public function __construct( $options = array() ) { + global $wgLang; - if( !$stash ) { - # Couldn't save the file. - return false; - } + $this->mWatch = !empty( $options['watch'] ); + $this->mForReUpload = !empty( $options['forreupload'] ); + $this->mSessionKey = isset( $options['sessionkey'] ) + ? $options['sessionkey'] : ''; + $this->mHideIgnoreWarning = !empty( $options['hideignorewarning'] ); + $this->mDestWarningAck = !empty( $options['destwarningack'] ); + + $this->mTextTop = $options['texttop']; + $this->mTextAfterSummary = $options['textaftersummary']; + $this->mDestFile = isset( $options['destfile'] ) ? $options['destfile'] : ''; - $key = mt_rand( 0, 0x7fffffff ); - $_SESSION['wsUploadData'][$key] = array( - 'mTempPath' => $stash, - 'mFileSize' => $this->mFileSize, - 'mSrcName' => $this->mSrcName, - 'mFileProps' => $this->mFileProps, - 'version' => self::SESSION_VERSION, - ); - return $key; - } + $sourceDescriptor = $this->getSourceSection(); + $descriptor = $sourceDescriptor + + $this->getDescriptionSection() + + $this->getOptionsSection(); - /** - * Remove a temporarily kept file stashed by saveTempUploadedFile(). - * @access private - * @return success - */ - function unsaveUploadedFile() { - global $wgOut; - if( !$this->mTempPath ) return true; // nothing to delete - $repo = RepoGroup::singleton()->getLocalRepo(); - $success = $repo->freeTemp( $this->mTempPath ); - if ( ! $success ) { - $wgOut->showFileDeleteError( $this->mTempPath ); - return false; - } else { - return true; - } - } + wfRunHooks( 'UploadFormInitDescriptor', array( &$descriptor ) ); + parent::__construct( $descriptor, 'upload' ); - /* -------------------------------------------------------------- */ + # Set some form properties + $this->setSubmitText( wfMsg( 'uploadbtn' ) ); + $this->setSubmitName( 'wpUpload' ); + $this->setSubmitTooltip( 'upload' ); + $this->setId( 'mw-upload-form' ); + + # Build a list of IDs for javascript insertion + $this->mSourceIds = array(); + foreach ( $sourceDescriptor as $key => $field ) { + if ( !empty( $field['id'] ) ) + $this->mSourceIds[] = $field['id']; + } - /** - * @param string $error as HTML - * @access private - */ - function uploadError( $error ) { - global $wgOut; - $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" ); - $wgOut->addHTML( '<span class="error">' . $error . '</span>' ); } /** - * There's something wrong with this file, not enough to reject it - * totally but we require manual intervention to save it for real. - * Stash it away, then present a form asking to confirm or cancel. - * - * @param string $warning as HTML - * @access private + * Get the descriptor of the fieldset that contains the file source + * selection. The section is 'source' + * + * @return array Descriptor array */ - function uploadWarning( $warning ) { - global $wgOut; - global $wgUseCopyrightUpload; - - $this->mSessionKey = $this->stashSession(); - if( !$this->mSessionKey ) { - # Couldn't save file; an error has been displayed so let's go. - return; + protected function getSourceSection() { + global $wgLang, $wgUser, $wgRequest; + + if ( $this->mSessionKey ) { + return array( + 'wpSessionKey' => array( + 'type' => 'hidden', + 'default' => $this->mSessionKey, + ), + 'wpSourceType' => array( + 'type' => 'hidden', + 'default' => 'Stash', + ), + ); } - $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" ); - $wgOut->addHTML( '<ul class="warning">' . $warning . "</ul>\n" ); + $canUploadByUrl = UploadFromUrl::isEnabled() && $wgUser->isAllowed( 'upload_by_url' ); + $radio = $canUploadByUrl; + $selectedSourceType = strtolower( $wgRequest->getText( 'wpSourceType', 'File' ) ); - $titleObj = SpecialPage::getTitleFor( 'Upload' ); - - if ( $wgUseCopyrightUpload ) { - $copyright = Xml::hidden( 'wpUploadCopyStatus', $this->mCopyrightStatus ) . "\n" . - Xml::hidden( 'wpUploadSource', $this->mCopyrightSource ) . "\n"; - } else { - $copyright = ''; + $descriptor = array(); + if ( $this->mTextTop ) { + $descriptor['UploadFormTextTop'] = array( + 'type' => 'info', + 'section' => 'source', + 'default' => $this->mTextTop, + 'raw' => true, + ); + } + + $descriptor['UploadFile'] = array( + 'class' => 'UploadSourceField', + 'section' => 'source', + 'type' => 'file', + 'id' => 'wpUploadFile', + 'label-message' => 'sourcefilename', + 'upload-type' => 'File', + 'radio' => &$radio, + 'help' => wfMsgExt( 'upload-maxfilesize', + array( 'parseinline', 'escapenoentities' ), + $wgLang->formatSize( + wfShorthandToInteger( ini_get( 'upload_max_filesize' ) ) + ) + ) . ' ' . wfMsgHtml( 'upload_source_file' ), + 'checked' => $selectedSourceType == 'file', + ); + if ( $canUploadByUrl ) { + global $wgMaxUploadSize; + $descriptor['UploadFileURL'] = array( + 'class' => 'UploadSourceField', + 'section' => 'source', + 'id' => 'wpUploadFileURL', + 'label-message' => 'sourceurl', + 'upload-type' => 'url', + 'radio' => &$radio, + 'help' => wfMsgExt( 'upload-maxfilesize', + array( 'parseinline', 'escapenoentities' ), + $wgLang->formatSize( $wgMaxUploadSize ) + ) . ' ' . wfMsgHtml( 'upload_source_url' ), + 'checked' => $selectedSourceType == 'url', + ); } + wfRunHooks( 'UploadFormSourceDescriptors', array( &$descriptor, &$radio, $selectedSourceType ) ); - $wgOut->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), - 'enctype' => 'multipart/form-data', 'id' => 'uploadwarning' ) ) . "\n" . - Xml::hidden( 'wpIgnoreWarning', '1' ) . "\n" . - Xml::hidden( 'wpSessionKey', $this->mSessionKey ) . "\n" . - Xml::hidden( 'wpUploadDescription', $this->mComment ) . "\n" . - Xml::hidden( 'wpLicense', $this->mLicense ) . "\n" . - Xml::hidden( 'wpDestFile', $this->mDesiredDestName ) . "\n" . - Xml::hidden( 'wpWatchthis', $this->mWatchthis ) . "\n" . - "{$copyright}<br />" . - Xml::submitButton( wfMsg( 'ignorewarning' ), array ( 'name' => 'wpUpload', 'id' => 'wpUpload', 'checked' => 'checked' ) ) . ' ' . - Xml::submitButton( wfMsg( 'reuploaddesc' ), array ( 'name' => 'wpReUpload', 'id' => 'wpReUpload' ) ) . - Xml::closeElement( 'form' ) . "\n" + $descriptor['Extensions'] = array( + 'type' => 'info', + 'section' => 'source', + 'default' => $this->getExtensionsMessage(), + 'raw' => true, ); + return $descriptor; } + /** - * Displays the main upload form, optionally with a highlighted - * error message up at the top. - * - * @param string $msg as HTML - * @access private + * Get the messages indicating which extensions are preferred and prohibitted. + * + * @return string HTML string containing the message */ - function mainUploadForm( $msg='' ) { - global $wgOut, $wgUser, $wgLang, $wgMaxUploadSize; - global $wgUseCopyrightUpload, $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview; - global $wgRequest, $wgAllowCopyUploads; - global $wgStylePath, $wgStyleVersion; - - $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck; - $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview; - - $adc = wfBoolToStr( $useAjaxDestCheck ); - $alp = wfBoolToStr( $useAjaxLicensePreview ); - $autofill = wfBoolToStr( $this->mDesiredDestName == '' ); - - $wgOut->addScript( "<script type=\"text/javascript\"> -wgAjaxUploadDestCheck = {$adc}; -wgAjaxLicensePreview = {$alp}; -wgUploadAutoFill = {$autofill}; -</script>" ); - $wgOut->addScriptFile( 'upload.js' ); - $wgOut->addScriptFile( 'edit.js' ); // For <charinsert> support - - if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) - { - wfDebug( "Hook 'UploadForm:initial' broke output of the upload form\n" ); - return false; - } - - if( $this->mDesiredDestName ) { - $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); - // Show a subtitle link to deleted revisions (to sysops et al only) - if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) { - $link = wfMsgExt( - $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted', - array( 'parse', 'replaceafter' ), - $wgUser->getSkin()->makeKnownLinkObj( - SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ), - wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count ) - ) - ); - $wgOut->addHTML( "<div id=\"contentSub2\">{$link}</div>" ); - } - - // Show the relevant lines from deletion log (for still deleted files only) - if( $title instanceof Title && $title->isDeletedQuick() && !$title->exists() ) { - $this->showDeletionLog( $wgOut, $title->getPrefixedText() ); - } - } - - $cols = intval($wgUser->getOption( 'cols' )); - - if( $wgUser->getOption( 'editwidth' ) ) { - $width = " style=\"width:100%\""; - } else { - $width = ''; - } - - if ( '' != $msg ) { - $sub = wfMsgHtml( 'uploaderror' ); - $wgOut->addHTML( "<h2>{$sub}</h2>\n" . - "<span class='error'>{$msg}</span>\n" ); - } - $wgOut->addHTML( '<div id="uploadtext">' ); - $wgOut->addWikiMsg( 'uploadtext', $this->mDesiredDestName ); - $wgOut->addHTML( "</div>\n" ); - + 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, + global $wgLang, $wgCheckFileExtensions, $wgStrictFileExtensions, $wgFileExtensions, $wgFileBlacklist; $allowedExtensions = ''; @@ -1045,805 +849,211 @@ wgUploadAutoFill = {$autofill}; # Everything is permitted. $extensionsList = ''; } + return $extensionsList; + } - # Get the maximum file size from php.ini as $wgMaxUploadSize works for uploads from URL via CURL only - # See http://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize for possible values of upload_max_filesize - $val = trim( ini_get( 'upload_max_filesize' ) ); - $last = strtoupper( ( substr( $val, -1 ) ) ); - switch( $last ) { - case 'G': - $val2 = substr( $val, 0, -1 ) * 1024 * 1024 * 1024; - break; - case 'M': - $val2 = substr( $val, 0, -1 ) * 1024 * 1024; - break; - case 'K': - $val2 = substr( $val, 0, -1 ) * 1024; - break; - default: - $val2 = $val; - } - $val2 = $wgAllowCopyUploads ? min( $wgMaxUploadSize, $val2 ) : $val2; - $maxUploadSize = '<div id="mw-upload-maxfilesize">' . - wfMsgExt( 'upload-maxfilesize', array( 'parseinline', 'escapenoentities' ), - $wgLang->formatSize( $val2 ) ) . - "</div>\n"; - - $sourcefilename = wfMsgExt( 'sourcefilename', array( 'parseinline', 'escapenoentities' ) ); - $destfilename = wfMsgExt( 'destfilename', array( 'parseinline', 'escapenoentities' ) ); - - $msg = $this->mForReUpload ? 'filereuploadsummary' : 'fileuploadsummary'; - $summary = wfMsgExt( $msg, 'parseinline' ); - - $licenses = new Licenses(); - $license = wfMsgExt( 'license', array( 'parseinline' ) ); - $nolicense = wfMsgHtml( 'nolicense' ); - $licenseshtml = $licenses->getHtml(); - - $ulb = wfMsgHtml( 'uploadbtn' ); - - - $titleObj = SpecialPage::getTitleFor( 'Upload' ); - - $encDestName = htmlspecialchars( $this->mDesiredDestName ); - - $watchChecked = $this->watchCheck() ? 'checked="checked"' : ''; - # Re-uploads should not need "file exist already" warnings - $warningChecked = ($this->mIgnoreWarning || $this->mForReUpload) ? 'checked="checked"' : ''; - - // Prepare form for upload or upload/copy - if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_url' ) ) { - $filename_form = - "<input type='radio' id='wpSourceTypeFile' name='wpSourceType' value='file' " . - "onchange='toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\")' checked='checked' />" . - "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " . - "onfocus='" . - "toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\");" . - "toggle_element_check(\"wpSourceTypeFile\",\"wpSourceTypeURL\")' " . - "onchange='fillDestFilename(\"wpUploadFile\")' size='60' />" . - wfMsgHTML( 'upload_source_file' ) . "<br/>" . - "<input type='radio' id='wpSourceTypeURL' name='wpSourceType' value='web' " . - "onchange='toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\")' />" . - "<input tabindex='1' type='text' name='wpUploadFileURL' id='wpUploadFileURL' " . - "onfocus='" . - "toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\");" . - "toggle_element_check(\"wpSourceTypeURL\",\"wpSourceTypeFile\")' " . - "onchange='fillDestFilename(\"wpUploadFileURL\")' size='60' disabled='disabled' />" . - wfMsgHtml( 'upload_source_url' ) ; - } else { - $filename_form = - "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " . - ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . - "size='60' />" . - "<input type='hidden' name='wpSourceType' value='file' />" ; - } - if ( $useAjaxDestCheck ) { - $warningRow = "<tr><td colspan='2' id='wpDestFile-warning'> </td></tr>"; - $destOnkeyup = 'onkeyup="wgUploadWarningObj.keypress();"'; - } else { - $warningRow = ''; - $destOnkeyup = ''; - } - $encComment = htmlspecialchars( $this->mComment ); - + /** + * Get the descriptor of the fieldset that contains the file description + * input. The section is 'description' + * + * @return array Descriptor array + */ + protected function getDescriptionSection() { + global $wgUser, $wgOut; - $wgOut->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL(), - 'enctype' => 'multipart/form-data', 'id' => 'mw-upload-form' ) ) . - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'upload' ) ) . - Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-upload-table' ) ) . - "<tr> - {$this->uploadFormTextTop} - <td class='mw-label'> - <label for='wpUploadFile'>{$sourcefilename}</label> - </td> - <td class='mw-input'> - {$filename_form} - </td> - </tr> - <tr> - <td></td> - <td> - {$maxUploadSize} - {$extensionsList} - </td> - </tr> - <tr> - <td class='mw-label'> - <label for='wpDestFile'>{$destfilename}</label> - </td> - <td class='mw-input'>" + $cols = intval( $wgUser->getOption( 'cols' ) ); + if( $wgUser->getOption( 'editwidth' ) ) { + $wgOut->addInlineStyle( '#mw-htmlform-description { width: 100%; }' ); + } + + $descriptor = array( + 'DestFile' => array( + 'type' => 'text', + 'section' => 'description', + 'id' => 'wpDestFile', + 'label-message' => 'destfilename', + 'size' => 60, + 'default' => $this->mDestFile, + # FIXME: hack to work around poor handling of the 'default' option in HTMLForm + 'nodata' => strval( $this->mDestFile ) !== '', + ), + 'UploadDescription' => array( + 'type' => 'textarea', + 'section' => 'description', + 'id' => 'wpUploadDescription', + 'label-message' => $this->mForReUpload + ? 'filereuploadsummary' + : 'fileuploadsummary', + 'cols' => $cols, + 'rows' => 8, + ) ); - if( $this->mForReUpload ) { - $wgOut->addHTML( - Xml::hidden( 'wpDestFile', $this->mDesiredDestName, array('id'=>'wpDestFile','tabindex'=>2) ) . - "<tt>" . - $encDestName . - "</tt>" - ); - } - else { - $wgOut->addHTML( - "<input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='60' - value=\"{$encDestName}\" onchange='toggleFilenameFiller()' $destOnkeyup />" + if ( $this->mTextAfterSummary ) { + $descriptor['UploadFormTextAfterSummary'] = array( + 'type' => 'info', + 'section' => 'description', + 'default' => $this->mTextAfterSummary, + 'raw' => true, ); } - - $wgOut->addHTML( - "</td> - </tr> - <tr> - <td class='mw-label'> - <label for='wpUploadDescription'>{$summary}</label> - </td> - <td class='mw-input'> - <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6' - cols='{$cols}'{$width}>$encComment</textarea> - {$this->uploadFormTextAfterSummary} - </td> - </tr> - <tr>" + $descriptor += array( + 'EditTools' => array( + 'type' => 'edittools', + 'section' => 'description', + ), + 'License' => array( + 'type' => 'select', + 'class' => 'Licenses', + 'section' => 'description', + 'id' => 'wpLicense', + 'label-message' => 'license', + ), ); - # Re-uploads should not need license info - if ( !$this->mForReUpload && $licenseshtml != '' ) { - global $wgStylePath; - $wgOut->addHTML( " - <td class='mw-label'> - <label for='wpLicense'>$license</label> - </td> - <td class='mw-input'> - <select name='wpLicense' id='wpLicense' tabindex='4' - onchange='licenseSelectorCheck()'> - <option value=''>$nolicense</option> - $licenseshtml - </select> - </td> - </tr> - <tr>" - ); - if( $useAjaxLicensePreview ) { - $wgOut->addHTML( " - <td></td> - <td id=\"mw-license-preview\"></td> - </tr> - <tr>" - ); - } - } + if ( $this->mForReUpload ) + $descriptor['DestFile']['readonly'] = true; - if ( !$this->mForReUpload && $wgUseCopyrightUpload ) { - $filestatus = wfMsgExt( 'filestatus', 'escapenoentities' ); - $copystatus = htmlspecialchars( $this->mCopyrightStatus ); - $filesource = wfMsgExt( 'filesource', 'escapenoentities' ); - $uploadsource = htmlspecialchars( $this->mCopyrightSource ); - - $wgOut->addHTML( " - <td class='mw-label' style='white-space: nowrap;'> - <label for='wpUploadCopyStatus'>$filestatus</label></td> - <td class='mw-input'> - <input tabindex='5' type='text' name='wpUploadCopyStatus' id='wpUploadCopyStatus' - value=\"$copystatus\" size='60' /> - </td> - </tr> - <tr> - <td class='mw-label'> - <label for='wpUploadCopyStatus'>$filesource</label> - </td> - <td class='mw-input'> - <input tabindex='6' type='text' name='wpUploadSource' id='wpUploadCopyStatus' - value=\"$uploadsource\" size='60' /> - </td> - </tr> - <tr>" + global $wgUseCopyrightUpload; + if ( $wgUseCopyrightUpload ) { + $descriptor['UploadCopyStatus'] = array( + 'type' => 'text', + 'section' => 'description', + 'id' => 'wpUploadCopyStatus', + 'label-message' => 'filestatus', + ); + $descriptor['UploadSource'] = array( + 'type' => 'text', + 'section' => 'description', + 'id' => 'wpUploadSource', + 'label-message' => 'filesource', ); } - $wgOut->addHTML( " - <td></td> - <td> - <input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' /> - <label for='wpWatchthis'>" . wfMsgHtml( 'watchthisupload' ) . "</label> - <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' $warningChecked /> - <label for='wpIgnoreWarning'>" . wfMsgHtml( 'ignorewarnings' ) . "</label> - </td> - </tr> - $warningRow - <tr> - <td></td> - <td class='mw-input'> - <input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\"" . - $wgUser->getSkin()->tooltipAndAccesskey( 'upload' ) . " /> - </td> - </tr> - <tr> - <td></td> - <td class='mw-input'>" - ); - $wgOut->addHTML( '<div class="mw-editTools">' ); - $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); - $wgOut->addHTML( '</div>' ); - $wgOut->addHTML( " - </td> - </tr>" . - Xml::closeElement( 'table' ) . - Xml::hidden( 'wpDestFileWarningAck', '', array( 'id' => 'wpDestFileWarningAck' ) ) . - Xml::hidden( 'wpForReUpload', $this->mForReUpload, array( 'id' => 'wpForReUpload' ) ) . - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ) - ); - $uploadfooter = wfMsgNoTrans( 'uploadfooter' ); - if( $uploadfooter != '-' && !wfEmptyMsg( 'uploadfooter', $uploadfooter ) ){ - $wgOut->addWikiText( '<div id="mw-upload-footer-message">' . $uploadfooter . '</div>' ); - } - } - - /* -------------------------------------------------------------- */ - - /** - * See if we should check the 'watch this page' checkbox on the form - * based on the user's preferences and whether we're being asked - * to create a new file or update an existing one. - * - * In the case where 'watch edits' is off but 'watch creations' is on, - * we'll leave the box unchecked. - * - * Note that the page target can be changed *on the form*, so our check - * state can get out of sync. - */ - function watchCheck() { - global $wgUser; - if( $wgUser->getOption( 'watchdefault' ) ) { - // Watch all edits! - 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 $local->getTitle()->userIsWatching(); - } else { - // New page should get watched if that's our option. - return $wgUser->getOption( 'watchcreations' ); - } - } - - /** - * Split a file into a base name and all dot-delimited 'extensions' - * on the end. Some web server configurations will fall back to - * earlier pseudo-'extensions' to determine type and execute - * scripts, so the blacklist needs to check them all. - * - * @return array - */ - public function splitExtensions( $filename ) { - $bits = explode( '.', $filename ); - $basename = array_shift( $bits ); - return array( $basename, $bits ); - } - - /** - * Perform case-insensitive match against a list of file extensions. - * Returns true if the extension is in the list. - * - * @param string $ext - * @param array $list - * @return bool - */ - function checkFileExtension( $ext, $list ) { - return in_array( strtolower( $ext ), $list ); - } - - /** - * Perform case-insensitive match against a list of file extensions. - * Returns true if any of the extensions are in the list. - * - * @param array $ext - * @param array $list - * @return bool - */ - public function checkFileExtensionList( $ext, $list ) { - foreach( $ext as $e ) { - if( in_array( strtolower( $e ), $list ) ) { - return true; - } - } - return false; - } - - /** - * Verifies that it's ok to include the uploaded file - * - * @param string $tmpfile the full path of the temporary file to verify - * @param string $extension The filename extension that the file is to be served with - * @return mixed true of the file is verified, a WikiError object otherwise. - */ - function verify( $tmpfile, $extension ) { - #magically determine mime type - $magic = MimeMagic::singleton(); - $mime = $magic->guessMimeType($tmpfile,false); - - - #check mime type, if desired - global $wgVerifyMimeType; - if ($wgVerifyMimeType) { - wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n"); - #check mime type against file extension - if( !self::verifyExtension( $mime, $extension ) ) { - return new WikiErrorMsg( 'uploadcorrupt' ); - } - - #check mime type blacklist - global $wgMimeTypeBlacklist; - if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist) ) { - if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { - return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) ); - } - - # Check IE type - $fp = fopen( $tmpfile, 'rb' ); - $chunk = fread( $fp, 256 ); - fclose( $fp ); - $extMime = $magic->guessTypesForExtension( $extension ); - $ieTypes = $magic->getIEMimeTypes( $tmpfile, $chunk, $extMime ); - foreach ( $ieTypes as $ieType ) { - if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) { - return new WikiErrorMsg( 'filetype-bad-ie-mime', $ieType ); - } - } - } - } - - #check for htmlish code and javascript - if( $this->detectScript ( $tmpfile, $mime, $extension ) ) { - return new WikiErrorMsg( 'uploadscripted' ); - } - if( $extension == 'svg' || $mime == 'image/svg+xml' ) { - if( $this->detectScriptInSvg( $tmpfile ) ) { - return new WikiErrorMsg( 'uploadscripted' ); - } - } - - /** - * Scan the uploaded file for viruses - */ - $virus= $this->detectVirus($tmpfile); - if ( $virus ) { - return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) ); - } - - wfDebug( __METHOD__.": all clear; passing.\n" ); - return true; + return $descriptor; } /** - * Checks if the mime type of the uploaded file matches the file extension. - * - * @param string $mime the mime type of the uploaded file - * @param string $extension The filename extension that the file is to be served with - * @return bool + * Get the descriptor of the fieldset that contains the upload options, + * such as "watch this file". The section is 'options' + * + * @return array Descriptor array */ - static function verifyExtension( $mime, $extension ) { - $magic = MimeMagic::singleton(); - - if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) - if ( ! $magic->isRecognizableExtension( $extension ) ) { - wfDebug( __METHOD__.": passing file with unknown detected mime type; " . - "unrecognized extension '$extension', can't verify\n" ); - return true; - } else { - wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ". - "recognized extension '$extension', so probably invalid file\n" ); - return false; - } - - $match= $magic->isMatchingExtension($extension,$mime); - - if ($match===NULL) { - wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" ); - return true; - } elseif ($match===true) { - wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" ); - - #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it! - return true; - - } else { - wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" ); - return false; - } - } - - - /** - * Heuristic for detecting files that *could* contain JavaScript instructions or - * things that may look like HTML to a browser and are thus - * potentially harmful. The present implementation will produce false positives in some situations. - * - * @param string $file Pathname to the temporary upload file - * @param string $mime The mime type of the file - * @param string $extension The extension of the file - * @return bool true if the file contains something looking like embedded scripts - */ - function detectScript($file, $mime, $extension) { - global $wgAllowTitlesInSVG; - - #ugly hack: for text files, always look at the entire file. - #For binarie field, just check the first K. - - if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file ); - else { - $fp = fopen( $file, 'rb' ); - $chunk = fread( $fp, 1024 ); - fclose( $fp ); - } + protected function getOptionsSection() { + global $wgUser, $wgOut; - $chunk= strtolower( $chunk ); - - if (!$chunk) return false; - - #decode from UTF-16 if needed (could be used for obfuscation). - if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE"; - elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE"; - else $enc= NULL; - - if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk); - - $chunk= trim($chunk); - - #FIXME: convert from UTF-16 if necessarry! - - wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n"); - - #check for HTML doctype - if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true; - - /** - * Internet Explorer for Windows performs some really stupid file type - * autodetection which can cause it to interpret valid image files as HTML - * and potentially execute JavaScript, creating a cross-site scripting - * attack vectors. - * - * Apple's Safari browser also performs some unsafe file type autodetection - * which can cause legitimate files to be interpreted as HTML if the - * web server is not correctly configured to send the right content-type - * (or if you're really uploading plain text and octet streams!) - * - * Returns true if IE is likely to mistake the given file for HTML. - * Also returns true if Safari would mistake the given file for HTML - * when served with a generic content-type. - */ - - $tags = array( - '<a href', - '<body', - '<head', - '<html', #also in safari - '<img', - '<pre', - '<script', #also in safari - '<table' + if( $wgUser->isLoggedIn() ) { + $descriptor = array( + 'Watchthis' => array( + 'type' => 'check', + 'id' => 'wpWatchthis', + 'label-message' => 'watchthisupload', + 'section' => 'options', + 'default' => $wgUser->getOption( 'watchcreations' ), + ) ); - if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) { - $tags[] = '<title'; } - - foreach( $tags as $tag ) { - if( false !== strpos( $chunk, $tag ) ) { - return true; - } + if( !$this->mHideIgnoreWarning ) { + $descriptor['IgnoreWarning'] = array( + 'type' => 'check', + 'id' => 'wpIgnoreWarning', + 'label-message' => 'ignorewarnings', + 'section' => 'options', + ); } - /* - * look for javascript - */ - - #resolve entity-refs to look at attributes. may be harsh on big files... cache result? - $chunk = Sanitizer::decodeCharReferences( $chunk ); - - #look for script-types - if (preg_match('!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim',$chunk)) return true; - - #look for html-style script-urls - if (preg_match('!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true; - - #look for css-style script-urls - if (preg_match('!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true; - - wfDebug("SpecialUpload::detectScript: no scripts found\n"); - return false; - } - - function detectScriptInSvg( $filename ) { - $check = new XmlTypeCheck( $filename, array( $this, 'checkSvgScriptCallback' ) ); - return $check->filterMatch; - } - - /** - * @todo Replace this with a whitelist filter! - */ - function checkSvgScriptCallback( $element, $attribs ) { - $stripped = $this->stripXmlNamespace( $element ); - - if( $stripped == 'script' ) { - wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" ); - return true; - } + $descriptor['wpDestFileWarningAck'] = array( + 'type' => 'hidden', + 'id' => 'wpDestFileWarningAck', + 'default' => $this->mDestWarningAck ? '1' : '', + ); - foreach( $attribs as $attrib => $value ) { - $stripped = $this->stripXmlNamespace( $attrib ); - if( substr( $stripped, 0, 2 ) == 'on' ) { - wfDebug( __METHOD__ . ": Found script attribute '$attrib'='value' in uploaded file.\n" ); - return true; - } - if( $stripped == 'href' && strpos( strtolower( $value ), 'javascript:' ) !== false ) { - wfDebug( __METHOD__ . ": Found script href attribute '$attrib'='$value' in uploaded file.\n" ); - return true; - } - } - } - - private function stripXmlNamespace( $name ) { - // 'http://www.w3.org/2000/svg:script' -> 'script' - $parts = explode( ':', strtolower( $name ) ); - return array_pop( $parts ); - } - - /** - * Generic wrapper function for a virus scanner program. - * This relies on the $wgAntivirus and $wgAntivirusSetup variables. - * $wgAntivirusRequired may be used to deny upload if the scan fails. - * - * @param string $file Pathname to the temporary upload file - * @return mixed false if not virus is found, NULL if the scan fails or is disabled, - * or a string containing feedback from the virus scanner if a virus was found. - * If textual feedback is missing but a virus was found, this function returns true. - */ - function detectVirus($file) { - global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut; - - if ( !$wgAntivirus ) { - wfDebug( __METHOD__.": virus scanner disabled\n"); - return NULL; - } - - if ( !$wgAntivirusSetup[$wgAntivirus] ) { - wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" ); - $wgOut->wrapWikiMsg( '<div class="error">$1</div>', array( 'virus-badscanner', $wgAntivirus ) ); - return wfMsg('virus-unknownscanner') . " $wgAntivirus"; - } - - # look up scanner configuration - $command = $wgAntivirusSetup[$wgAntivirus]["command"]; - $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"]; - $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ? - $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null; - - if ( strpos( $command,"%f" ) === false ) { - # simple pattern: append file to scan - $command .= " " . wfEscapeShellArg( $file ); - } else { - # complex pattern: replace "%f" with file to scan - $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); - } - - wfDebug( __METHOD__.": running virus scan: $command \n" ); - - # execute virus scanner - $exitCode = false; - - #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too. - # that does not seem to be worth the pain. - # Ask me (Duesentrieb) about it if it's ever needed. - $output = array(); - if ( wfIsWindows() ) { - exec( "$command", $output, $exitCode ); - } else { - exec( "$command 2>&1", $output, $exitCode ); - } - - # map exit code to AV_xxx constants. - $mappedCode = $exitCode; - if ( $exitCodeMap ) { - if ( isset( $exitCodeMap[$exitCode] ) ) { - $mappedCode = $exitCodeMap[$exitCode]; - } elseif ( isset( $exitCodeMap["*"] ) ) { - $mappedCode = $exitCodeMap["*"]; - } - } - - if ( $mappedCode === AV_SCAN_FAILED ) { - # scan failed (code was mapped to false by $exitCodeMap) - wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" ); - - if ( $wgAntivirusRequired ) { - return wfMsg('virus-scanfailed', array( $exitCode ) ); - } else { - return NULL; - } - } else if ( $mappedCode === AV_SCAN_ABORTED ) { - # scan failed because filetype is unknown (probably imune) - wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" ); - return NULL; - } else if ( $mappedCode === AV_NO_VIRUS ) { - # no virus found - wfDebug( __METHOD__.": file passed virus scan.\n" ); - return false; - } else { - $output = join( "\n", $output ); - $output = trim( $output ); - - if ( !$output ) { - $output = true; #if there's no output, return true - } elseif ( $msgPattern ) { - $groups = array(); - if ( preg_match( $msgPattern, $output, $groups ) ) { - if ( $groups[1] ) { - $output = $groups[1]; - } - } - } - - wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output \n" ); - return $output; + if ( $this->mForReUpload ) { + $descriptor['wpForReUpload'] = array( + 'type' => 'hidden', + 'id' => 'wpForReUpload', + 'default' => '1', + ); } - } - - /** - * Check if the temporary file is MacBinary-encoded, as some uploads - * from Internet Explorer on Mac OS Classic and Mac OS X will be. - * If so, the data fork will be extracted to a second temporary file, - * which will then be checked for validity and either kept or discarded. - * - * @access private - */ - function checkMacBinary() { - $macbin = new MacBinary( $this->mTempPath ); - if( $macbin->isValid() ) { - $dataFile = tempnam( wfTempDir(), "WikiMacBinary" ); - $dataHandle = fopen( $dataFile, 'wb' ); - - wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" ); - $macbin->extractData( $dataHandle ); - $this->mTempPath = $dataFile; - $this->mFileSize = $macbin->dataForkLength(); + return $descriptor; - // We'll have to manually remove the new file if it's not kept. - $this->mRemoveTempFile = true; - } - $macbin->close(); } /** - * If we've modified the upload file we need to manually remove it - * on exit to clean up. - * @access private + * Add the upload JS and show the form. */ - function cleanupTempFile() { - if ( $this->mRemoveTempFile && $this->mTempPath && file_exists( $this->mTempPath ) ) { - wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" ); - unlink( $this->mTempPath ); - } + public function show() { + $this->addUploadJS(); + parent::show(); } /** - * Check if there's an overwrite conflict and, if so, if restrictions - * forbid this user from performing the upload. - * - * @return mixed true on success, WikiError on failure - * @access private - */ - function checkOverwrite( $name ) { - $img = wfFindFile( $name ); - - $error = ''; - if( $img ) { - global $wgUser, $wgOut; - if( $img->isLocal() ) { - if( !self::userCanReUpload( $wgUser, $img->name ) ) { - $error = 'fileexists-forbidden'; - } - } else { - if( !$wgUser->isAllowed( 'reupload' ) || - !$wgUser->isAllowed( 'reupload-shared' ) ) { - $error = "fileexists-shared-forbidden"; - } - } - } - - if( $error ) { - $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) ); - return $errorText; - } - - // Rockin', go ahead and upload - return true; - } - - /** - * Check if a user is the last uploader - * - * @param User $user - * @param string $img, image name - * @return bool + * Add upload JS to $wgOut + * + * @param bool $autofill Whether or not to autofill the destination + * filename text box */ - public static function userCanReUpload( User $user, $img ) { - if( $user->isAllowed( 'reupload' ) ) - return true; // non-conditional - if( !$user->isAllowed( 'reupload-own' ) ) - return false; + protected function addUploadJS( ) { + global $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview, $wgEnableAPI; + global $wgOut; - $dbr = wfGetDB( DB_SLAVE ); - $row = $dbr->selectRow('image', - /* SELECT */ 'img_user', - /* WHERE */ array( 'img_name' => $img ) + $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck; + $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview && $wgEnableAPI; + + $scriptVars = array( + 'wgAjaxUploadDestCheck' => $useAjaxDestCheck, + 'wgAjaxLicensePreview' => $useAjaxLicensePreview, + 'wgUploadAutoFill' => !$this->mForReUpload && + // If we received mDestFile from the request, don't autofill + // the wpDestFile textbox + $this->mDestFile === '', + 'wgUploadSourceIds' => $this->mSourceIds, ); - if ( !$row ) - return false; - return $user->getId() == $row->img_user; + $wgOut->addScript( Skin::makeVariablesScript( $scriptVars ) ); + + // For <charinsert> support + $wgOut->addScriptFile( 'edit.js' ); + $wgOut->addScriptFile( 'upload.js' ); } /** - * Display an error with a wikitext description + * Empty function; submission is handled elsewhere. + * + * @return bool false */ - function showError( $description ) { - global $wgOut; - $wgOut->setPageTitle( wfMsg( "internalerror" ) ); - $wgOut->setRobotPolicy( "noindex,nofollow" ); - $wgOut->setArticleRelated( false ); - $wgOut->enableClientCache( false ); - $wgOut->addWikiText( $description ); + function trySubmit() { + return false; } - /** - * Get the initial image page text based on a comment and optional file status information - */ - static function getInitialPageText( $comment, $license, $copyStatus, $source ) { - global $wgUseCopyrightUpload; - if ( $wgUseCopyrightUpload ) { - if ( $license != '' ) { - $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; - } - $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" . - '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" . - "$licensetxt" . - '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ; - } else { - if ( $license != '' ) { - $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n"; - $pageText = $filedesc . - '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; - } else { - $pageText = $comment; - } - } - return $pageText; - } +} - /** - * If there are rows in the deletion log for this file, show them, - * along with a nice little note for the user - * - * @param OutputPage $out - * @param string filename - */ - private function showDeletionLog( $out, $filename ) { - global $wgUser; - $loglist = new LogEventsList( $wgUser->getSkin(), $out ); - $pager = new LogPager( $loglist, 'delete', false, $filename ); - if( $pager->getNumRows() > 0 ) { - $out->addHTML( '<div class="mw-warning-with-logexcerpt">' ); - $out->addWikiMsg( 'upload-wasdeleted' ); - $out->addHTML( - $loglist->beginLogEventsList() . - $pager->getBody() . - $loglist->endLogEventsList() +/** + * A form field that contains a radio box in the label + */ +class UploadSourceField extends HTMLTextField { + function getLabelHtml() { + $id = "wpSourceType{$this->mParams['upload-type']}"; + $label = Html::rawElement( 'label', array( 'for' => $id ), $this->mLabel ); + + if ( !empty( $this->mParams['radio'] ) ) { + $attribs = array( + 'name' => 'wpSourceType', + 'type' => 'radio', + 'id' => $id, + 'value' => $this->mParams['upload-type'], ); - $out->addHTML( '</div>' ); + if ( !empty( $this->mParams['checked'] ) ) + $attribs['checked'] = 'checked'; + $label .= Html::element( 'input', $attribs ); } + + return Html::rawElement( 'td', array( 'class' => 'mw-label' ), $label ); + } + function getSize() { + return isset( $this->mParams['size'] ) + ? $this->mParams['size'] + : 60; } } + diff --git a/includes/specials/SpecialUploadMogile.php b/includes/specials/SpecialUploadMogile.php deleted file mode 100644 index 7ff8fda6..00000000 --- a/includes/specials/SpecialUploadMogile.php +++ /dev/null @@ -1,135 +0,0 @@ -<?php -/** - * @file - * @ingroup SpecialPage - */ - -/** - * You will need the extension MogileClient to use this special page. - */ -require_once( 'MogileFS.php' ); - -/** - * Entry point - */ -function wfSpecialUploadMogile() { - global $wgRequest; - $form = new UploadFormMogile( $wgRequest ); - $form->execute(); -} - -/** - * Extends Special:Upload with MogileFS. - * @ingroup SpecialPage - */ -class UploadFormMogile extends UploadForm { - /** - * Move the uploaded file from its temporary location to the final - * destination. If a previous version of the file exists, move - * it into the archive subdirectory. - * - * @todo If the later save fails, we may have disappeared the original file. - * - * @param string $saveName - * @param string $tempName full path to the temporary file - * @param bool $useRename Not used in this implementation - */ - function saveUploadedFile( $saveName, $tempName, $useRename = false ) { - global $wgOut; - $mfs = MogileFS::NewMogileFS(); - - $this->mSavedFile = "image!{$saveName}"; - - if( $mfs->getPaths( $this->mSavedFile )) { - $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}"; - if( !$mfs->rename( $this->mSavedFile, "archive!{$this->mUploadOldVersion}" ) ) { - $wgOut->showFileRenameError( $this->mSavedFile, - "archive!{$this->mUploadOldVersion}" ); - return false; - } - } else { - $this->mUploadOldVersion = ''; - } - - if ( $this->mStashed ) { - if (!$mfs->rename($tempName,$this->mSavedFile)) { - $wgOut->showFileRenameError($tempName, $this->mSavedFile ); - return false; - } - } else { - if ( !$mfs->saveFile($this->mSavedFile,'normal',$tempName )) { - $wgOut->showFileCopyError( $tempName, $this->mSavedFile ); - return false; - } - unlink($tempName); - } - return true; - } - - /** - * Stash a file in a temporary directory for later processing - * after the user has confirmed it. - * - * If the user doesn't explicitly cancel or accept, these files - * can accumulate in the temp directory. - * - * @param string $saveName - the destination filename - * @param string $tempName - the source temporary file to save - * @return string - full path the stashed file, or false on failure - * @access private - */ - function saveTempUploadedFile( $saveName, $tempName ) { - global $wgOut; - - $stash = 'stash!' . gmdate( "YmdHis" ) . '!' . $saveName; - $mfs = MogileFS::NewMogileFS(); - if ( !$mfs->saveFile( $stash, 'normal', $tempName ) ) { - $wgOut->showFileCopyError( $tempName, $stash ); - return false; - } - unlink($tempName); - return $stash; - } - - /** - * Stash a file in a temporary directory for later processing, - * and save the necessary descriptive info into the session. - * Returns a key value which will be passed through a form - * to pick up the path info on a later invocation. - * - * @return int - * @access private - */ - function stashSession() { - $stash = $this->saveTempUploadedFile( - $this->mUploadSaveName, $this->mUploadTempName ); - - if( !$stash ) { - # Couldn't save the file. - return false; - } - - $key = mt_rand( 0, 0x7fffffff ); - $_SESSION['wsUploadData'][$key] = array( - 'mUploadTempName' => $stash, - 'mUploadSize' => $this->mUploadSize, - 'mOname' => $this->mOname ); - return $key; - } - - /** - * Remove a temporarily kept file stashed by saveTempUploadedFile(). - * @access private - * @return success - */ - function unsaveUploadedFile() { - global $wgOut; - $mfs = MogileFS::NewMogileFS(); - if ( ! $mfs->delete( $this->mUploadTempName ) ) { - $wgOut->showFileDeleteError( $this->mUploadTempName ); - return false; - } else { - return true; - } - } -} diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php index 8616ae28..8b8d0e9e 100644 --- a/includes/specials/SpecialUserlogin.php +++ b/includes/specials/SpecialUserlogin.php @@ -35,21 +35,23 @@ class LoginForm { const CREATE_BLOCKED = 9; const THROTTLED = 10; const USER_BLOCKED = 11; - const NEED_TOKEN = 12; - const WRONG_TOKEN = 13; + const NEED_TOKEN = 12; + const WRONG_TOKEN = 13; var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted; var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword; - var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage, $mSkipCookieCheck; - var $mToken; + var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage; + var $mSkipCookieCheck, $mReturnToQuery, $mToken; + + private $mExtUser = null; /** * Constructor - * @param WebRequest $request A WebRequest object passed by reference + * @param $request WebRequest: a WebRequest object passed by reference + * @param $par String: subpage parameter */ function LoginForm( &$request, $par = '' ) { - global $wgLang, $wgAllowRealName, $wgEnableEmail; - global $wgAuth, $wgRedirectOnLogin; + global $wgAuth, $wgHiddenPrefs, $wgEnableEmail, $wgRedirectOnLogin; $this->mType = ( $par == 'signup' ) ? $par : $request->getText( 'type' ); # Check for [[Special:Userlogin/signup]] $this->mName = $request->getText( 'wpName' ); @@ -57,6 +59,7 @@ class LoginForm { $this->mRetype = $request->getText( 'wpRetype' ); $this->mDomain = $request->getText( 'wpDomain' ); $this->mReturnTo = $request->getVal( 'returnto' ); + $this->mReturnToQuery = $request->getVal( 'returntoquery' ); $this->mCookieCheck = $request->getVal( 'wpCookieCheck' ); $this->mPosted = $request->wasPosted(); $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' ); @@ -73,6 +76,7 @@ class LoginForm { if ( $wgRedirectOnLogin ) { $this->mReturnTo = $wgRedirectOnLogin; + $this->mReturnToQuery = ''; } if( $wgEnableEmail ) { @@ -80,7 +84,7 @@ class LoginForm { } else { $this->mEmail = ''; } - if( $wgAllowRealName ) { + if( !in_array( 'realname', $wgHiddenPrefs ) ) { $this->mRealName = $request->getText( 'wpRealName' ); } else { $this->mRealName = ''; @@ -92,8 +96,10 @@ class LoginForm { $wgAuth->setDomain( $this->mDomain ); # When switching accounts, it sucks to get automatically logged out - if( $this->mReturnTo == $wgLang->specialPage( 'Userlogout' ) ) { + $returnToTitle = Title::newFromText( $this->mReturnTo ); + if( is_object( $returnToTitle ) && $returnToTitle->isSpecial( 'Userlogout' ) ) { $this->mReturnTo = ''; + $this->mReturnToQuery = ''; } } @@ -121,14 +127,14 @@ class LoginForm { function addNewAccountMailPassword() { global $wgOut; - if ('' == $this->mEmail) { + if ( $this->mEmail == '' ) { $this->mainLoginForm( wfMsg( 'noemail', htmlspecialchars( $this->mName ) ) ); return; } $u = $this->addNewaccountInternal(); - if ($u == NULL) { + if ($u == null) { return; } @@ -162,7 +168,7 @@ class LoginForm { # Create the account and abort if there's a problem doing so $u = $this->addNewAccountInternal(); - if( $u == NULL ) + if( $u == null ) return; # If we showed up language selection links, and one was in use, be @@ -191,7 +197,7 @@ class LoginForm { if( $wgUser->isAnon() ) { $wgUser = $u; $wgUser->setCookies(); - wfRunHooks( 'AddNewAccount', array( $wgUser ) ); + wfRunHooks( 'AddNewAccount', array( $wgUser, false ) ); $wgUser->addNewUserLogEntry(); if( $this->hasSessionCookie() ) { return $this->successfulCreation(); @@ -207,7 +213,7 @@ class LoginForm { $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addHTML( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) ); $wgOut->returnToMain( false, $self ); - wfRunHooks( 'AddNewAccount', array( $u ) ); + wfRunHooks( 'AddNewAccount', array( $u, false ) ); $u->addNewUserLogEntry(); return true; } @@ -218,7 +224,6 @@ class LoginForm { */ function addNewAccountInternal() { global $wgUser, $wgOut; - global $wgEnableSorbs, $wgProxyWhitelist; global $wgMemc, $wgAccountCreationThrottle; global $wgAuth, $wgMinimalPasswordLength; global $wgEmailConfirmToEdit; @@ -234,7 +239,7 @@ class LoginForm { // cation server before they create an account (otherwise, they can // create a local account and login as any domain user). We only need // to check this for domains that aren't local. - if( 'local' != $this->mDomain && '' != $this->mDomain ) { + if( 'local' != $this->mDomain && $this->mDomain != '' ) { if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mName ) || !$wgAuth->authenticate( $this->mName, $this->mPassword ) ) ) { $this->mainLoginForm( wfMsg( 'wrongpassword' ) ); return false; @@ -275,9 +280,7 @@ class LoginForm { } $ip = wfGetIP(); - if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) && - $wgUser->inSorbsBlacklist( $ip ) ) - { + if ( $wgUser->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) { $this->mainLoginForm( wfMsg( 'sorbs_create_account_reason' ) . ' (' . htmlspecialchars( $ip ) . ')' ); return false; } @@ -285,7 +288,7 @@ class LoginForm { # Now create a dummy user ($u) and check if it is valid $name = trim( $this->mName ); $u = User::newFromName( $name, 'creatable' ); - if ( is_null( $u ) ) { + if ( !is_object( $u ) ) { $this->mainLoginForm( wfMsg( 'noname' ) ); return false; } @@ -301,9 +304,10 @@ class LoginForm { } # check for minimal password length - if ( !$u->isValidPassword( $this->mPassword ) ) { + $valid = $u->getPasswordValidity( $this->mPassword ); + if ( $valid !== true ) { if ( !$this->mCreateaccountMail ) { - $this->mainLoginForm( wfMsgExt( 'passwordtooshort', array( 'parsemag' ), $wgMinimalPasswordLength ) ); + $this->mainLoginForm( wfMsgExt( $valid, array( 'parsemag' ), $wgMinimalPasswordLength ) ); return false; } else { # do not force a password for account creation by email @@ -383,6 +387,14 @@ class LoginForm { $wgAuth->initUser( $u, $autocreate ); + if ( $this->mExtUser ) { + $this->mExtUser->linkToLocal( $u->getId() ); + $email = $this->mExtUser->getPref( 'emailaddress' ); + if ( $email && !$this->mEmail ) { + $u->setEmail( $email ); + } + } + $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 ); $u->saveSettings(); @@ -399,15 +411,13 @@ class LoginForm { * This may create a local account as a side effect if the * authentication plugin allows transparent local account * creation. - * - * @public */ - function authenticateUserData() { + public function authenticateUserData() { global $wgUser, $wgAuth; - if ( '' == $this->mName ) { + if ( $this->mName == '' ) { return self::NO_NAME; } - + // We require a login token to prevent login CSRF // Handle part of this before incrementing the throttle so // token-less login attempts don't count towards the throttle @@ -422,17 +432,17 @@ class LoginForm { if ( !$this->mToken ) { return self::NEED_TOKEN; } - + global $wgPasswordAttemptThrottle; - $throttleCount=0; - if ( is_array($wgPasswordAttemptThrottle) ) { + $throttleCount = 0; + if ( is_array( $wgPasswordAttemptThrottle ) ) { $throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) ); $count = $wgPasswordAttemptThrottle['count']; $period = $wgPasswordAttemptThrottle['seconds']; global $wgMemc; - $throttleCount = $wgMemc->get($throttleKey); + $throttleCount = $wgMemc->get( $throttleKey ); if ( !$throttleCount ) { $wgMemc->add( $throttleKey, 1, $period ); // start counter } else if ( $throttleCount < $count ) { @@ -457,8 +467,13 @@ class LoginForm { wfDebug( __METHOD__.": already logged in as {$this->mName}\n" ); return self::SUCCESS; } + + $this->mExtUser = ExternalUser::newFromName( $this->mName ); + + # TODO: Allow some magic here for invalid external names, e.g., let the + # user choose a different wiki name. $u = User::newFromName( $this->mName ); - if( is_null( $u ) || !User::isUsableName( $u->getName() ) ) { + if( !( $u instanceof User ) || !User::isUsableName( $u->getName() ) ) { return self::ILLEGAL; } @@ -471,6 +486,15 @@ class LoginForm { $isAutoCreated = true; } } else { + global $wgExternalAuthType, $wgAutocreatePolicy; + if ( $wgExternalAuthType && $wgAutocreatePolicy != 'never' + && is_object( $this->mExtUser ) + && $this->mExtUser->authenticate( $this->mPassword ) ) { + # The external user and local user have the same name and + # password, so we assume they're the same. + $this->mExtUser->linkToLocal( $u->getID() ); + } + $u->load(); } @@ -480,6 +504,7 @@ class LoginForm { return $abort; } + global $wgBlockDisablesLogin; if (!$u->checkPassword( $this->mPassword )) { if( $u->checkTemporaryPassword( $this->mPassword ) ) { // The e-mailed temporary password should not be used for actu- @@ -508,8 +533,11 @@ class LoginForm { // faces etc will probably just fail cleanly here. $retval = self::RESET_PASS; } else { - $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS; + $retval = ($this->mPassword == '') ? self::EMPTY_PASS : self::WRONG_PASS; } + } elseif ( $wgBlockDisablesLogin && $u->isBlocked() ) { + // If we've enabled it, make it so that a blocked user cannot login + $retval = self::USER_BLOCKED; } else { $wgAuth->updateUser( $u ); $wgUser = $u; @@ -536,26 +564,40 @@ class LoginForm { * @return integer Status code */ function attemptAutoCreate( $user ) { - global $wgAuth, $wgUser; + global $wgAuth, $wgUser, $wgAutocreatePolicy; + + if ( $wgUser->isBlockedFromCreateAccount() ) { + wfDebug( __METHOD__.": user is blocked from account creation\n" ); + return self::CREATE_BLOCKED; + } + /** * If the external authentication plugin allows it, automatically cre- * ate a new account for users that are externally defined but have not * yet logged in. */ - 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; - } - if ( $wgUser->isBlockedFromCreateAccount() ) { - wfDebug( __METHOD__.": user is blocked from account creation\n" ); - return self::CREATE_BLOCKED; + if ( $this->mExtUser ) { + # mExtUser is neither null nor false, so use the new ExternalAuth + # system. + if ( $wgAutocreatePolicy == 'never' ) { + return self::NOT_EXISTS; + } + if ( !$this->mExtUser->authenticate( $this->mPassword ) ) { + return self::WRONG_PLUGIN_PASS; + } + } else { + # Old AuthPlugin. + 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; + } } wfDebug( __METHOD__.": creating account\n" ); @@ -566,8 +608,7 @@ class LoginForm { function processLogin() { global $wgUser, $wgAuth; - switch ($this->authenticateUserData()) - { + switch ( $this->authenticateUserData() ) { case self::SUCCESS: # We've verified now, update the real record if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) { @@ -630,6 +671,10 @@ class LoginForm { case self::THROTTLED: $this->mainLoginForm( wfMsg( 'login-throttled' ) ); break; + case self::USER_BLOCKED: + $this->mainLoginForm( wfMsgExt( 'login-userblocked', + array( 'parsemag', 'escape' ), $this->mName ) ); + break; default: throw new MWException( "Unhandled case value" ); } @@ -664,6 +709,13 @@ class LoginForm { $this->mainLoginForm( wfMsg( 'blocked-mailpassword' ) ); return; } + + # Check for hooks + $error = null; + if ( ! wfRunHooks( 'UserLoginMailPassword', array( $this->mName, &$error ) ) ) { + $this->mainLoginForm( $error ); + return; + } # If the user doesn't have a login token yet, set one. if ( !self::getLoginToken() ) { @@ -684,12 +736,12 @@ class LoginForm { return; } - if ( '' == $this->mName ) { + if ( $this->mName == '' ) { $this->mainLoginForm( wfMsg( 'noname' ) ); return; } $u = User::newFromName( $this->mName ); - if( is_null( $u ) ) { + if( !$u instanceof User ) { $this->mainLoginForm( wfMsg( 'noname' ) ); return; } @@ -725,17 +777,17 @@ class LoginForm { /** - * @param object user - * @param bool throttle - * @param string message name of email title - * @param string message name of email text - * @return mixed true on success, WikiError on failure + * @param $u User object + * @param $throttle Boolean + * @param $emailTitle String: message name of email title + * @param $emailText String: message name of email text + * @return Mixed: true on success, WikiError on failure * @private */ function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) { global $wgServer, $wgScript, $wgUser, $wgNewPasswordExpiry; - if ( '' == $u->getEmail() ) { + if ( $u->getEmail() == '' ) { return new WikiError( wfMsg( 'noemail', $u->getName() ) ); } $ip = wfGetIP(); @@ -748,10 +800,10 @@ class LoginForm { $np = $u->randomPassword(); $u->setNewpassword( $np, $throttle ); $u->saveSettings(); - - $m = wfMsgExt( $emailText, array( 'parsemag' ), $ip, $u->getName(), $np, + $userLanguage = $u->getOption( 'language' ); + $m = wfMsgExt( $emailText, array( 'parsemag', 'language' => $userLanguage ), $ip, $u->getName(), $np, $wgServer . $wgScript, round( $wgNewPasswordExpiry / 86400 ) ); - $result = $u->sendMail( wfMsg( $emailTitle ), $m ); + $result = $u->sendMail( wfMsgExt( $emailTitle, array( 'parsemag', 'language' => $userLanguage ) ), $m ); return $result; } @@ -781,8 +833,7 @@ class LoginForm { if ( !$titleObj instanceof Title ) { $titleObj = Title::newMainPage(); } - - $wgOut->redirect( $titleObj->getFullURL() ); + $wgOut->redirect( $titleObj->getFullURL( $this->mReturnToQuery ) ); } } @@ -815,7 +866,7 @@ class LoginForm { $wgOut->addHTML( $injected_html ); if ( !empty( $this->mReturnTo ) ) { - $wgOut->returnToMain( null, $this->mReturnTo ); + $wgOut->returnToMain( null, $this->mReturnTo, $this->mReturnToQuery ); } else { $wgOut->returnToMain( null ); } @@ -868,7 +919,7 @@ class LoginForm { * @private */ function mainLoginForm( $msg, $msgtype = 'error' ) { - global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail; + global $wgUser, $wgOut, $wgHiddenPrefs, $wgEnableEmail; global $wgCookiePrefix, $wgLoginLanguageSelector; global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration; @@ -890,7 +941,7 @@ class LoginForm { } } - if ( '' == $this->mName ) { + if ( $this->mName == '' ) { if ( $wgUser->isLoggedIn() ) { $this->mName = $wgUser->getName(); } else { @@ -914,6 +965,9 @@ class LoginForm { if ( !empty( $this->mReturnTo ) ) { $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo ); + if ( !empty( $this->mReturnToQuery ) ) + $returnto .= '&returntoquery=' . + wfUrlencode( $this->mReturnToQuery ); $q .= $returnto; $linkq .= $returnto; } @@ -928,7 +982,7 @@ class LoginForm { # Don't show a "create account" link if the user can't if( $this->showCreateOrLoginLink( $wgUser ) ) - $template->set( 'link', wfMsgHtml( $linkmsg, $link ) ); + $template->set( 'link', wfMsgWikiHtml( $linkmsg, $link ) ); else $template->set( 'link', '' ); @@ -944,7 +998,7 @@ class LoginForm { $template->set( 'message', $msg ); $template->set( 'messagetype', $msgtype ); $template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() ); - $template->set( 'userealname', $wgAllowRealName ); + $template->set( 'userealname', !in_array( 'realname', $wgHiddenPrefs ) ); $template->set( 'useemail', $wgEnableEmail ); $template->set( 'emailrequired', $wgEmailConfirmToEdit ); $template->set( 'canreset', $wgAuth->allowPasswordChange() ); @@ -971,14 +1025,20 @@ class LoginForm { } // Give authentication and captcha plugins a chance to modify the form - $wgAuth->modifyUITemplate( $template ); + $wgAuth->modifyUITemplate( $template, $this->mType ); if ( $this->mType == 'signup' ) { wfRunHooks( 'UserCreateForm', array( &$template ) ); } else { wfRunHooks( 'UserLoginForm', array( &$template ) ); } - $wgOut->setPageTitle( wfMsg( 'userlogin' ) ); + //Changes the title depending on permissions for creating account + if ( $wgUser->isAllowed( 'createaccount' ) ) { + $wgOut->setPageTitle( wfMsg( 'userlogin' ) ); + } else { + $wgOut->setPageTitle( wfMsg( 'userloginnocreate' ) ); + } + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); $wgOut->disallowUserJs(); // just in case... @@ -1080,8 +1140,6 @@ class LoginForm { * @private */ function onCookieRedirectCheck( $type ) { - global $wgUser; - if ( !$this->hasSessionCookie() ) { if ( $type == 'new' ) { return $this->mainLoginForm( wfMsgExt( 'nocookiesnew', array( 'parseinline' ) ) ); @@ -1139,12 +1197,17 @@ class LoginForm { function makeLanguageSelectorLink( $text, $lang ) { global $wgUser; $self = SpecialPage::getTitleFor( 'Userlogin' ); - $attr[] = 'uselang=' . $lang; + $attr = array( 'uselang' => $lang ); if( $this->mType == 'signup' ) - $attr[] = 'type=signup'; + $attr['type'] = 'signup'; if( $this->mReturnTo ) - $attr[] = 'returnto=' . $this->mReturnTo; + $attr['returnto'] = $this->mReturnTo; $skin = $wgUser->getSkin(); - return $skin->makeKnownLinkObj( $self, htmlspecialchars( $text ), implode( '&', $attr ) ); + return $skin->linkKnown( + $self, + htmlspecialchars( $text ), + array(), + $attr + ); } } diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php index 3d497bd7..e23df612 100644 --- a/includes/specials/SpecialUserlogout.php +++ b/includes/specials/SpecialUserlogout.php @@ -10,6 +10,16 @@ function wfSpecialUserlogout() { global $wgUser, $wgOut; + /** + * Some satellite ISPs use broken precaching schemes that log people out straight after + * they're logged in (bug 17790). Luckily, there's a way to detect such requests. + */ + if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&' ) !== false ) { + wfDebug( "Special:Userlogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" ); + wfHttpError( 400, wfMsg( 'loginerror' ), wfMsg( 'suspicious-userlogout' ) ); + return; + } + $oldName = $wgUser->getName(); $wgUser->logout(); $wgOut->setRobotPolicy( 'noindex,nofollow' ); diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index 90619109..36caf9a6 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -34,8 +34,8 @@ class UserrightsPage extends SpecialPage { return !empty( $available['add'] ) or !empty( $available['remove'] ) or ( ( $this->isself || !$checkIfSelf ) and - (!empty( $available['add-self'] ) - or !empty( $available['remove-self'] ))); + ( !empty( $available['add-self'] ) + or !empty( $available['remove-self'] ) ) ); } /** @@ -44,10 +44,10 @@ class UserrightsPage extends SpecialPage { * * @param $par Mixed: string if any subpage provided, else null */ - function execute( $par ) { + public function execute( $par ) { // If the visitor doesn't have permissions to assign or remove // any groups, it's a bit silly to give them the user search prompt. - global $wgUser, $wgRequest; + global $wgUser, $wgRequest, $wgOut; if( $par ) { $this->mTarget = $par; @@ -55,32 +55,41 @@ class UserrightsPage extends SpecialPage { $this->mTarget = $wgRequest->getVal( 'user' ); } - if (!$this->mTarget) { + /* + * If the user is blocked and they only have "partial" access + * (e.g. they don't have the userrights permission), then don't + * allow them to use Special:UserRights. + */ + if( $wgUser->isBlocked() && !$wgUser->isAllowed( 'userrights' ) ) { + $wgOut->blockedPage(); + return; + } + + $available = $this->changeableGroups(); + + if ( !$this->mTarget ) { /* * If the user specified no target, and they can only * edit their own groups, automatically set them as the * target. */ - $available = $this->changeableGroups(); - if (empty($available['add']) && empty($available['remove'])) + if ( !count( $available['add'] ) && !count( $available['remove'] ) ) $this->mTarget = $wgUser->getName(); } - if ($this->mTarget == $wgUser->getName()) + if ( $this->mTarget == $wgUser->getName() ) $this->isself = true; if( !$this->userCanChangeRights( $wgUser, true ) ) { // fixme... there may be intermediate groups we can mention. - global $wgOut; - $wgOut->showPermissionsErrorPage( array( + $wgOut->showPermissionsErrorPage( array( array( $wgUser->isAnon() ? 'userrights-nologin' - : 'userrights-notallowed' ) ); + : 'userrights-notallowed' ) ) ); return; } if ( wfReadOnly() ) { - global $wgOut; $wgOut->readOnlyPage(); return; } @@ -90,7 +99,8 @@ class UserrightsPage extends SpecialPage { $this->setHeaders(); // show the general form - $this->switchForm(); + if ( count( $available['add'] ) || count( $available['remove'] ) ) + $this->switchForm(); if( $wgRequest->wasPosted() ) { // save settings @@ -102,9 +112,7 @@ class UserrightsPage extends SpecialPage { $this->mTarget, $reason ); - - global $wgOut; - + $url = $this->getSuccessURL(); $wgOut->redirect( $url ); return; @@ -117,7 +125,7 @@ class UserrightsPage extends SpecialPage { $this->editUserGroupsForm( $this->mTarget ); } } - + function getSuccessURL() { return $this->getTitle( $this->mTarget )->getFullURL(); } @@ -130,11 +138,12 @@ class UserrightsPage extends SpecialPage { * @param $reason String: reason for group change * @return null */ - function saveUserGroups( $username, $reason = '') { + function saveUserGroups( $username, $reason = '' ) { global $wgRequest, $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; $user = $this->fetchUser( $username ); - if( !$user ) { + if( $user instanceof WikiErrorMsg ) { + $wgOut->addWikiMsgArray( $user->getMessageKey(), $user->getMessageArgs() ); return; } @@ -144,38 +153,58 @@ class UserrightsPage extends SpecialPage { // This could possibly create a highly unlikely race condition if permissions are changed between // when the form is loaded and when the form is saved. Ignoring it for the moment. - foreach ($allgroups as $group) { + foreach ( $allgroups as $group ) { // We'll tell it to remove all unchecked groups, and add all checked groups. // Later on, this gets filtered for what can actually be removed - if ($wgRequest->getCheck( "wpGroup-$group" )) { + if ( $wgRequest->getCheck( "wpGroup-$group" ) ) { $addgroup[] = $group; } else { $removegroup[] = $group; } } + + $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason ); + } + + /** + * Save user groups changes in the database. + * + * @param $user User object + * @param $add Array of groups to add + * @param $remove Array of groups to remove + * @param $reason String: reason for group change + * @return Array: Tuple of added, then removed groups + */ + function doSaveUserGroups( $user, $add, $remove, $reason = '' ) { + global $wgUser; // Validate input set... + $isself = ( $user->getName() == $wgUser->getName() ); + $groups = $user->getGroups(); $changeable = $this->changeableGroups(); - $addable = array_merge( $changeable['add'], $this->isself ? $changeable['add-self'] : array() ); - $removable = array_merge( $changeable['remove'], $this->isself ? $changeable['remove-self'] : array() ); - - $removegroup = array_unique( - array_intersect( (array)$removegroup, $removable ) ); - $addgroup = array_unique( - array_intersect( (array)$addgroup, $addable ) ); + $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : array() ); + $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : array() ); + + $remove = array_unique( + array_intersect( (array)$remove, $removable, $groups ) ); + $add = array_unique( array_diff( + array_intersect( (array)$add, $addable ), + $groups ) + ); $oldGroups = $user->getGroups(); $newGroups = $oldGroups; + // remove then add groups - if( $removegroup ) { - $newGroups = array_diff($newGroups, $removegroup); - foreach( $removegroup as $group ) { + if( $remove ) { + $newGroups = array_diff( $newGroups, $remove ); + foreach( $remove as $group ) { $user->removeGroup( $group ); } } - if( $addgroup ) { - $newGroups = array_merge($newGroups, $addgroup); - foreach( $addgroup as $group ) { + if( $add ) { + $newGroups = array_merge( $newGroups, $add ); + foreach( $add as $group ) { $user->addGroup( $group ); } } @@ -186,26 +215,24 @@ class UserrightsPage extends SpecialPage { wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) ); wfDebug( 'newGroups: ' . print_r( $newGroups, true ) ); - if( $user instanceof User ) { - // hmmm - wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) ); - } + wfRunHooks( 'UserRights', array( &$user, $add, $remove ) ); if( $newGroups != $oldGroups ) { - $this->addLogEntry( $user, $oldGroups, $newGroups ); + $this->addLogEntry( $user, $oldGroups, $newGroups, $reason ); } + return array( $add, $remove ); } - + + /** * Add a rights log entry for an action. */ - function addLogEntry( $user, $oldGroups, $newGroups ) { - global $wgRequest; + function addLogEntry( $user, $oldGroups, $newGroups, $reason ) { $log = new LogPage( 'rights' ); $log->addEntry( 'rights', $user->getUserPage(), - $wgRequest->getText( 'user-reason' ), + $reason, array( $this->makeGroupNameListForLog( $oldGroups ), $this->makeGroupNameListForLog( $newGroups ) @@ -221,7 +248,8 @@ class UserrightsPage extends SpecialPage { global $wgOut; $user = $this->fetchUser( $username ); - if( !$user ) { + if( $user instanceof WikiErrorMsg ) { + $wgOut->addWikiMsgArray( $user->getMessageKey(), $user->getMessageArgs() ); return; } @@ -239,10 +267,10 @@ class UserrightsPage extends SpecialPage { * return a user (or proxy) object for manipulating it. * * Side effects: error output for invalid access - * @return mixed User, UserRightsProxy, or null + * @return mixed User, UserRightsProxy, or WikiErrorMsg */ - function fetchUser( $username ) { - global $wgOut, $wgUser, $wgUserrightsInterwikiDelimiter; + public function fetchUser( $username ) { + global $wgUser, $wgUserrightsInterwikiDelimiter; $parts = explode( $wgUserrightsInterwikiDelimiter, $username ); if( count( $parts ) < 2 ) { @@ -250,20 +278,21 @@ class UserrightsPage extends SpecialPage { $database = ''; } else { list( $name, $database ) = array_map( 'trim', $parts ); - - if( !$wgUser->isAllowed( 'userrights-interwiki' ) ) { - $wgOut->addWikiMsg( 'userrights-no-interwiki' ); - return null; - } - if( !UserRightsProxy::validDatabase( $database ) ) { - $wgOut->addWikiMsg( 'userrights-nodatabase', $database ); - return null; + + if( $database == wfWikiID() ) { + $database = ''; + } else { + if( !$wgUser->isAllowed( 'userrights-interwiki' ) ) { + return new WikiErrorMsg( 'userrights-no-interwiki' ); + } + if( !UserRightsProxy::validDatabase( $database ) ) { + return new WikiErrorMsg( 'userrights-nodatabase', $database ); + } } } if( $name == '' ) { - $wgOut->addWikiMsg( 'nouserspecified' ); - return false; + return new WikiErrorMsg( 'nouserspecified' ); } if( $name{0} == '#' ) { @@ -278,8 +307,13 @@ class UserrightsPage extends SpecialPage { } if( !$name ) { - $wgOut->addWikiMsg( 'noname' ); - return null; + return new WikiErrorMsg( 'noname' ); + } + } else { + $name = User::getCanonicalName( $name ); + if( !$name ) { + // invalid name + return new WikiErrorMsg( 'nosuchusershort', $username ); } } @@ -290,8 +324,7 @@ class UserrightsPage extends SpecialPage { } if( !$user || $user->isAnon() ) { - $wgOut->addWikiMsg( 'nosuchusershort', $username ); - return null; + return new WikiErrorMsg( 'nosuchusershort', $username ); } return $user; @@ -339,14 +372,16 @@ class UserrightsPage extends SpecialPage { * @return Array: Tuple of addable, then removable groups */ protected function splitGroups( $groups ) { - list($addable, $removable, $addself, $removeself) = array_values( $this->changeableGroups() ); + list( $addable, $removable, $addself, $removeself ) = array_values( $this->changeableGroups() ); $removable = array_intersect( - array_merge( $this->isself ? $removeself : array(), $removable ), - $groups ); // Can't remove groups the user doesn't have - $addable = array_diff( - array_merge( $this->isself ? $addself : array(), $addable ), - $groups ); // Can't add groups the user does have + array_merge( $this->isself ? $removeself : array(), $removable ), + $groups + ); // Can't remove groups the user doesn't have + $addable = array_diff( + array_merge( $this->isself ? $addself : array(), $addable ), + $groups + ); // Can't add groups the user does have return array( $addable, $removable ); } @@ -364,10 +399,21 @@ class UserrightsPage extends SpecialPage { foreach( $groups as $group ) $list[] = self::buildGroupLink( $group ); + $autolist = array(); + if ( $user instanceof User ) { + foreach( Autopromote::getAutopromoteGroups( $user ) as $group ) { + $autolist[] = self::buildGroupLink( $group ); + } + } + $grouplist = ''; if( count( $list ) > 0 ) { $grouplist = wfMsgHtml( 'userrights-groupsmember' ); - $grouplist = '<p>' . $grouplist . ' ' . $wgLang->listToText( $list ) . '</p>'; + $grouplist = '<p>' . $grouplist . ' ' . $wgLang->listToText( $list ) . "</p>\n"; + } + if( count( $autolist ) > 0 ) { + $autogrouplistintro = wfMsgHtml( 'userrights-groupsmember-auto' ); + $grouplist .= '<p>' . $autogrouplistintro . ' ' . $wgLang->listToText( $autolist ) . "</p>\n"; } $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL(), 'name' => 'editGroup', 'id' => 'mw-userrights-form2' ) ) . @@ -409,17 +455,17 @@ class UserrightsPage extends SpecialPage { private static function buildGroupLink( $group ) { static $cache = array(); if( !isset( $cache[$group] ) ) - $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupName( $group ) ); + $cache[$group] = User::makeGroupLinkHtml( $group, htmlspecialchars( User::getGroupName( $group ) ) ); return $cache[$group]; } - + /** * Returns an array of all groups that may be edited * @return array Array of groups that may be edited. */ - protected static function getAllGroups() { - return User::getAllGroups(); - } + protected static function getAllGroups() { + return User::getAllGroups(); + } /** * Adds a table with checkboxes where you can select what groups to add/remove @@ -431,11 +477,11 @@ class UserrightsPage extends SpecialPage { $allgroups = $this->getAllGroups(); $ret = ''; - $column = 1; - $settable_col = ''; - $unsettable_col = ''; + # Put all column info into an associative array so that extensions can + # more easily manage it. + $columns = array( 'unchangeable' => array(), 'changeable' => array() ); - foreach ($allgroups as $group) { + foreach( $allgroups as $group ) { $set = in_array( $group, $usergroups ); # Should the checkbox be disabled? $disabled = !( @@ -443,53 +489,54 @@ class UserrightsPage extends SpecialPage { ( !$set && $this->canAdd( $group ) ) ); # Do we need to point out that this action is irreversible? $irreversible = !$disabled && ( - ($set && !$this->canAdd( $group )) || - (!$set && !$this->canRemove( $group ) ) ); - - $attr = $disabled ? array( 'disabled' => 'disabled' ) : array(); - $text = $irreversible - ? wfMsgHtml( 'userrights-irreversible-marker', User::getGroupMember( $group ) ) - : User::getGroupMember( $group ); - $checkbox = Xml::checkLabel( $text, "wpGroup-$group", - "wpGroup-$group", $set, $attr ); - $checkbox = $disabled ? Xml::tags( 'span', array( 'class' => 'mw-userrights-disabled' ), $checkbox ) : $checkbox; - - if ($disabled) { - $unsettable_col .= "$checkbox<br />\n"; + ( $set && !$this->canAdd( $group ) ) || + ( !$set && !$this->canRemove( $group ) ) ); + + $checkbox = array( + 'set' => $set, + 'disabled' => $disabled, + 'irreversible' => $irreversible + ); + + if( $disabled ) { + $columns['unchangeable'][$group] = $checkbox; } else { - $settable_col .= "$checkbox<br />\n"; + $columns['changeable'][$group] = $checkbox; } } - if ($column) { - $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) . - "<tr> -"; - if( $settable_col !== '' ) { - $ret .= xml::element( 'th', null, wfMsg( 'userrights-changeable-col' ) ); - } - if( $unsettable_col !== '' ) { - $ret .= xml::element( 'th', null, wfMsg( 'userrights-unchangeable-col' ) ); - } - $ret.= "</tr> - <tr> -"; - if( $settable_col !== '' ) { - $ret .= -" <td style='vertical-align:top;'> - $settable_col - </td> -"; - } - if( $unsettable_col !== '' ) { - $ret .= -" <td style='vertical-align:top;'> - $unsettable_col - </td> -"; + # Build the HTML table + $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) . + "<tr>\n"; + foreach( $columns as $name => $column ) { + if( $column === array() ) + continue; + $ret .= xml::element( 'th', null, wfMsg( 'userrights-' . $name . '-col' ) ); + } + $ret.= "</tr>\n<tr>\n"; + foreach( $columns as $column ) { + if( $column === array() ) + continue; + $ret .= "\t<td style='vertical-align:top;'>\n"; + foreach( $column as $group => $checkbox ) { + $attr = $checkbox['disabled'] ? array( 'disabled' => 'disabled' ) : array(); + + if ( $checkbox['irreversible'] ) { + $text = htmlspecialchars( wfMsg( 'userrights-irreversible-marker', + User::getGroupMember( $group ) ) ); + } else { + $text = htmlspecialchars( User::getGroupMember( $group ) ); + } + $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group, + "wpGroup-" . $group, $checkbox['set'], $attr ); + $ret .= "\t\t" . ( $checkbox['disabled'] + ? Xml::tags( 'span', array( 'class' => 'mw-userrights-disabled' ), $checkboxHtml ) + : $checkboxHtml + ) . "<br />\n"; } - $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' ); + $ret .= "\t</td>\n"; } + $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' ); return $ret; } @@ -502,7 +549,7 @@ class UserrightsPage extends SpecialPage { // $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'] ) ); } /** @@ -511,116 +558,17 @@ class UserrightsPage extends SpecialPage { */ 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 an array of the groups that the user can add/remove. + * Returns $wgUser->changeableGroups() * * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) ) */ function changeableGroups() { global $wgUser; - - if( $wgUser->isAllowed( 'userrights' ) ) { - // This group gives the right to modify everything (reverse- - // compatibility with old "userrights lets you change - // everything") - // Using array_merge to make the groups reindexed - $all = array_merge( User::getAllGroups() ); - return array( - 'add' => $all, - 'remove' => $all, - 'add-self' => array(), - 'remove-self' => array() - ); - } - - // Okay, it's not so simple, we will have to go through the arrays - $groups = array( - 'add' => array(), - 'remove' => array(), - 'add-self' => array(), - 'remove-self' => array() ); - $addergroups = $wgUser->getEffectiveGroups(); - - foreach ($addergroups as $addergroup) { - $groups = array_merge_recursive( - $groups, $this->changeableByGroup($addergroup) - ); - $groups['add'] = array_unique( $groups['add'] ); - $groups['remove'] = array_unique( $groups['remove'] ); - $groups['add-self'] = array_unique( $groups['add-self'] ); - $groups['remove-self'] = array_unique( $groups['remove-self'] ); - } - - // Run a hook because we can - wfRunHooks( 'UserrightsChangeableGroups', array( $this, $wgUser, $addergroups, &$groups ) ); - - return $groups; - } - - /** - * Returns an array of the groups that a particular group can add/remove. - * - * @param $group String: the group to check for whether it can add/remove - * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) ) - */ - private function changeableByGroup( $group ) { - global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; - - $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() ); - if( empty($wgAddGroups[$group]) ) { - // Don't add anything to $groups - } elseif( $wgAddGroups[$group] === true ) { - // You get everything - $groups['add'] = User::getAllGroups(); - } elseif( is_array($wgAddGroups[$group]) ) { - $groups['add'] = $wgAddGroups[$group]; - } - - // Same thing for remove - if( empty($wgRemoveGroups[$group]) ) { - } elseif($wgRemoveGroups[$group] === true ) { - $groups['remove'] = User::getAllGroups(); - } elseif( is_array($wgRemoveGroups[$group]) ) { - $groups['remove'] = $wgRemoveGroups[$group]; - } - - // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility - if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) { - foreach($wgGroupsAddToSelf as $key => $value) { - if( is_int($key) ) { - $wgGroupsAddToSelf['user'][] = $value; - } - } - } - - if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) { - foreach($wgGroupsRemoveFromSelf as $key => $value) { - if( is_int($key) ) { - $wgGroupsRemoveFromSelf['user'][] = $value; - } - } - } - - // Now figure out what groups the user can add to him/herself - if( empty($wgGroupsAddToSelf[$group]) ) { - } elseif( $wgGroupsAddToSelf[$group] === true ) { - // No idea WHY this would be used, but it's there - $groups['add-self'] = User::getAllGroups(); - } elseif( is_array($wgGroupsAddToSelf[$group]) ) { - $groups['add-self'] = $wgGroupsAddToSelf[$group]; - } - - if( empty($wgGroupsRemoveFromSelf[$group]) ) { - } elseif( $wgGroupsRemoveFromSelf[$group] === true ) { - $groups['remove-self'] = User::getAllGroups(); - } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) { - $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; - } - - return $groups; + return $wgUser->changeableGroups(); } /** diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php index 95e06f4b..7da6023e 100644 --- a/includes/specials/SpecialVersion.php +++ b/includes/specials/SpecialVersion.php @@ -12,21 +12,29 @@ class SpecialVersion extends SpecialPage { private $firstExtOpened = true; + static $viewvcUrls = array( + 'svn+ssh://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki', + 'http://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki', + # Doesn't work at the time of writing but maybe some day: + 'https://svn.wikimedia.org/viewvc/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki', + ); + function __construct(){ - parent::__construct( 'Version' ); + parent::__construct( 'Version' ); } /** * main() */ function execute( $par ) { - global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks; + global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks, $wgContLang; $wgMessageCache->loadAllMessages(); $this->setHeaders(); $this->outputHeader(); - $wgOut->addHTML( '<div dir="ltr">' ); + $wgOut->addHTML( Xml::openElement( 'div', + array( 'dir' => $wgContLang->getDir() ) ) ); $text = $this->MediaWikiCredits() . $this->softwareInformation() . @@ -47,13 +55,19 @@ class SpecialVersion extends SpecialPage { * @return wiki text showing the license information */ static function MediaWikiCredits() { - $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ) . - "__NOTOC__ + global $wgContLang; + + $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ); + + // This text is always left-to-right. + $ret .= '<div dir="ltr">'; + $ret .= "__NOTOC__ This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''', - copyright (C) 2001-2009 Magnus Manske, Brion Vibber, Lee Daniel Crocker, + copyright © 2001-2010 Magnus Manske, Brion Vibber, Lee Daniel Crocker, Tim Starling, Erik Möller, Gabriel Wicke, Ævar Arnfjörð Bjarmason, Niklas Laxström, Domas Mituzas, Rob Church, Yuri Astrakhan, Aryeh Gregor, - Aaron Schulz and others. + Aaron Schulz, Andrew Garrett, Raimond Spekking, Alexandre Emsenhuber, + Siebrand Mazeland, Chad Horohoe and others. MediaWiki is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -70,6 +84,7 @@ class SpecialVersion extends SpecialPage { Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA or [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online]. "; + $ret .= '</div>'; return str_replace( "\t\t", '', $ret ) . "\n"; } @@ -80,25 +95,30 @@ class SpecialVersion extends SpecialPage { static function softwareInformation() { $dbr = wfGetDB( DB_SLAVE ); - return Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) . - Xml::openElement( 'table', array( 'id' => 'sv-software' ) ) . + // Put the software in an array of form 'name' => 'version'. All messages should + // be loaded here, so feel free to use wfMsg*() in the 'name'. Raw HTML or wikimarkup + // can be used + $software = array(); + $software['[http://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked(); + $software['[http://www.php.net/ PHP]'] = phpversion() . " (" . php_sapi_name() . ")"; + $software[$dbr->getSoftwareLink()] = $dbr->getServerVersion(); + + // Allow a hook to add/remove items + wfRunHooks( 'SoftwareInfo', array( &$software ) ); + + $out = Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) . + Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-software' ) ) . "<tr> <th>" . wfMsg( 'version-software-product' ) . "</th> <th>" . wfMsg( 'version-software-version' ) . "</th> - </tr>\n - <tr> - <td>[http://www.mediawiki.org/ MediaWiki]</td> - <td>" . self::getVersionLinked() . "</td> - </tr>\n - <tr> - <td>[http://www.php.net/ PHP]</td> - <td>" . phpversion() . " (" . php_sapi_name() . ")</td> - </tr>\n - <tr> - <td>" . $dbr->getSoftwareLink() . "</td> - <td>" . $dbr->getServerVersion() . "</td> - </tr>\n" . - Xml::closeElement( 'table' ); + </tr>\n"; + foreach( $software as $name => $version ) { + $out .= "<tr> + <td>" . $name . "</td> + <td>" . $version . "</td> + </tr>\n"; + } + return $out . Xml::closeElement( 'table' ); } /** @@ -106,27 +126,52 @@ class SpecialVersion extends SpecialPage { * * @return mixed */ - public static function getVersion() { + public static function getVersion( $flags = '' ) { global $wgVersion, $IP; wfProfileIn( __METHOD__ ); - $svn = self::getSvnRevision( $IP ); - $version = $svn ? "$wgVersion (r$svn)" : $wgVersion; + + $info = self::getSvnInfo( $IP ); + if ( !$info ) { + $version = $wgVersion; + } elseif( $flags === 'nodb' ) { + $version = "$wgVersion (r{$info['checkout-rev']})"; + } else { + $version = $wgVersion . ' ' . + wfMsg( + 'version-svn-revision', + isset( $info['directory-rev'] ) ? $info['directory-rev'] : '', + $info['checkout-rev'] + ); + } + wfProfileOut( __METHOD__ ); return $version; } /** - * Return a string of the MediaWiki version with a link to SVN revision if - * available + * Return a wikitext-formatted string of the MediaWiki version with a link to + * the SVN revision if available * * @return mixed */ public static function getVersionLinked() { global $wgVersion, $IP; wfProfileIn( __METHOD__ ); - $svn = self::getSvnRevision( $IP ); - $viewvc = 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/?pathrev='; - $version = $svn ? "$wgVersion ([{$viewvc}{$svn} r$svn])" : $wgVersion; + $info = self::getSvnInfo( $IP ); + if ( isset( $info['checkout-rev'] ) ) { + $linkText = wfMsg( + 'version-svn-revision', + isset( $info['directory-rev'] ) ? $info['directory-rev'] : '', + $info['checkout-rev'] + ); + if ( isset( $info['viewvc-url'] ) ) { + $version = "$wgVersion [{$info['viewvc-url']} $linkText]"; + } else { + $version = "$wgVersion $linkText"; + } + } else { + $version = $wgVersion; + } wfProfileOut( __METHOD__ ); return $version; } @@ -148,64 +193,40 @@ class SpecialVersion extends SpecialPage { wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) ); $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) . - Xml::openElement( 'table', array( 'id' => 'sv-ext' ) ); + Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-ext' ) ); foreach ( $extensionTypes as $type => $text ) { if ( isset ( $wgExtensionCredits[$type] ) && count ( $wgExtensionCredits[$type] ) ) { - $out .= $this->openExtType( $text ); + $out .= $this->openExtType( $text, 'credits-' . $type ); usort( $wgExtensionCredits[$type], array( $this, 'compare' ) ); foreach ( $wgExtensionCredits[$type] as $extension ) { - $version = null; - $subVersion = ''; - if ( isset( $extension['version'] ) ) { - $version = $extension['version']; - } - if ( isset( $extension['svn-revision'] ) && - preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/', - $extension['svn-revision'], $m ) ) { - $subVersion = 'r' . $m[1]; - } - - if( $version && $subVersion ) { - $version = $version . ' [' . $subVersion . ']'; - } elseif ( !$version && $subVersion ) { - $version = $subVersion; - } - - $out .= $this->formatCredits( - isset ( $extension['name'] ) ? $extension['name'] : '', - $version, - isset ( $extension['author'] ) ? $extension['author'] : '', - isset ( $extension['url'] ) ? $extension['url'] : null, - isset ( $extension['description'] ) ? $extension['description'] : '', - isset ( $extension['descriptionmsg'] ) ? $extension['descriptionmsg'] : '' - ); + $out .= $this->formatCredits( $extension ); } } } if ( count( $wgExtensionFunctions ) ) { - $out .= $this->openExtType( wfMsg( 'version-extension-functions' ) ); - $out .= '<tr><td colspan="3">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n"; + $out .= $this->openExtType( wfMsg( 'version-extension-functions' ), 'extension-functions' ); + $out .= '<tr><td colspan="4">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n"; } if ( $cnt = count( $tags = $wgParser->getTags() ) ) { for ( $i = 0; $i < $cnt; ++$i ) $tags[$i] = "<{$tags[$i]}>"; - $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ) ); - $out .= '<tr><td colspan="3">' . $this->listToText( $tags ). "</td></tr>\n"; + $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ), 'parser-tags' ); + $out .= '<tr><td colspan="4">' . $this->listToText( $tags ). "</td></tr>\n"; } if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) { - $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ) ); - $out .= '<tr><td colspan="3">' . $this->listToText( $fhooks ) . "</td></tr>\n"; + $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ), 'parser-function-hooks' ); + $out .= '<tr><td colspan="4">' . $this->listToText( $fhooks ) . "</td></tr>\n"; } if ( count( $wgSkinExtensionFunctions ) ) { - $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ) ); - $out .= '<tr><td colspan="3">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n"; + $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ), 'skin-extension-functions' ); + $out .= '<tr><td colspan="4">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n"; } $out .= Xml::closeElement( 'table' ); return $out; @@ -223,23 +244,72 @@ class SpecialVersion extends SpecialPage { } } - function formatCredits( $name, $version = null, $author = null, $url = null, $description = null, $descriptionMsg = null ) { - $extension = isset( $url ) ? "[$url $name]" : $name; - $version = isset( $version ) ? "(" . wfMsg( 'version-version' ) . " $version)" : ''; + function formatCredits( $extension ) { + $name = isset( $extension['name'] ) ? $extension['name'] : '[no name]'; + if ( isset( $extension['path'] ) ) { + $svnInfo = self::getSvnInfo( dirname($extension['path']) ); + $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null; + $checkoutRev = isset( $svnInfo['checkout-rev'] ) ? $svnInfo['checkout-rev'] : null; + $viewvcUrl = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : null; + } else { + $directoryRev = null; + $checkoutRev = null; + $viewvcUrl = null; + } + + # Make main link (or just the name if there is no URL) + if ( isset( $extension['url'] ) ) { + $mainLink = "[{$extension['url']} $name]"; + } else { + $mainLink = $name; + } + if ( isset( $extension['version'] ) ) { + $versionText = '<span class="mw-version-ext-version">' . + wfMsg( 'version-version', $extension['version'] ) . + '</span>'; + } else { + $versionText = ''; + } - # Look for a localized description - if( isset( $descriptionMsg ) ) { - $msg = wfMsg( $descriptionMsg ); - if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) { - $description = $msg; + # Make subversion text/link + if ( $checkoutRev ) { + $svnText = wfMsg( 'version-svn-revision', $directoryRev, $checkoutRev ); + $svnText = isset( $viewvcUrl ) ? "[$viewvcUrl $svnText]" : $svnText; + } else { + $svnText = false; + } + + # Make description text + $description = isset ( $extension['description'] ) ? $extension['description'] : ''; + if( isset ( $extension['descriptionmsg'] ) ) { + # Look for a localized description + $descriptionMsg = $extension['descriptionmsg']; + if( is_array( $descriptionMsg ) ) { + $descriptionMsgKey = $descriptionMsg[0]; // Get the message key + array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only + array_map( "htmlspecialchars", $descriptionMsg ); // For sanity + $msg = wfMsg( $descriptionMsgKey, $descriptionMsg ); + } else { + $msg = wfMsg( $descriptionMsg ); } + if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) { + $description = $msg; + } } - return "<tr> - <td><em>$extension $version</em></td> - <td>$description</td> - <td>" . $this->listToText( (array)$author ) . "</td> + if ( $svnText !== false ) { + $extNameVer = "<tr> + <td><em>$mainLink $versionText</em></td> + <td><em>$svnText</em></td>"; + } else { + $extNameVer = "<tr> + <td colspan=\"2\"><em>$mainLink $versionText</em></td>"; + } + $author = isset ( $extension['author'] ) ? $extension['author'] : array(); + $extDescAuthor = "<td>$description</td> + <td>" . $this->listToText( (array)$author, false ) . "</td> </tr>\n"; + return $extNameVer . $extDescAuthor; } /** @@ -253,7 +323,7 @@ class SpecialVersion extends SpecialPage { ksort( $myWgHooks ); $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) . - Xml::openElement( 'table', array( 'id' => 'sv-hooks' ) ) . + Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) ) . "<tr> <th>" . wfMsg( 'version-hook-name' ) . "</th> <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th> @@ -271,19 +341,20 @@ class SpecialVersion extends SpecialPage { return ''; } - private function openExtType($text, $name = null) { - $opt = array( 'colspan' => 3 ); + private function openExtType( $text, $name = null ) { + $opt = array( 'colspan' => 4 ); $out = ''; - if(!$this->firstExtOpened) { + if( !$this->firstExtOpened ) { // Insert a spacing line $out .= '<tr class="sv-space">' . Xml::element( 'td', $opt ) . "</tr>\n"; } $this->firstExtOpened = false; - if($name) { $opt['id'] = "sv-$name"; } + if( $name ) + $opt['id'] = "sv-$name"; - $out .= "<tr>" . Xml::element( 'th', $opt, $text) . "</tr>\n"; + $out .= "<tr>" . Xml::element( 'th', $opt, $text ) . "</tr>\n"; return $out; } @@ -298,9 +369,10 @@ class SpecialVersion extends SpecialPage { /** * @param array $list + * @param bool $sort * @return string */ - function listToText( $list ) { + function listToText( $list, $sort = true ) { $cnt = count( $list ); if ( $cnt == 1 ) { @@ -310,7 +382,9 @@ class SpecialVersion extends SpecialPage { return ''; } else { global $wgLang; - sort( $list ); + if ( $sort ) { + sort( $list ); + } return $wgLang->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) ); } } @@ -338,12 +412,20 @@ class SpecialVersion extends SpecialPage { } /** - * Retrieve the revision number of a Subversion working directory. + * Get an associative array of information about a given path, from its .svn + * subdirectory. Returns false on error, such as if the directory was not + * checked out with subversion. * - * @param string $dir - * @return mixed revision number as int, or false if not a SVN checkout + * Returned keys are: + * Required: + * checkout-rev The revision which was checked out + * Optional: + * directory-rev The revision when the directory was last modified + * url The subversion URL of the directory + * repo-url The base URL of the repository + * viewvc-url A ViewVC URL pointing to the checked-out revision */ - public static function getSvnRevision( $dir ) { + public static function getSvnInfo( $dir ) { // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html $entries = $dir . '/.svn/entries'; @@ -351,10 +433,13 @@ class SpecialVersion extends SpecialPage { return false; } - $content = file( $entries ); + $lines = file( $entries ); + if ( !count( $lines ) ) { + return false; + } // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4) - if( preg_match( '/^<\?xml/', $content[0] ) ) { + if( preg_match( '/^<\?xml/', $lines[0] ) ) { // subversion is release <= 1.3 if( !function_exists( 'simplexml_load_file' ) ) { // We could fall back to expat... YUCK @@ -371,15 +456,52 @@ class SpecialVersion extends SpecialPage { if( $xml->entry[0]['name'] == '' ) { // The directory entry should always have a revision marker. if( $entry['revision'] ) { - return intval( $entry['revision'] ); + return array( 'checkout-rev' => intval( $entry['revision'] ) ); } } } } return false; + } + + // subversion is release 1.4 or above + if ( count( $lines ) < 11 ) { + return false; + } + $info = array( + 'checkout-rev' => intval( trim( $lines[3] ) ), + 'url' => trim( $lines[4] ), + 'repo-url' => trim( $lines[5] ), + 'directory-rev' => intval( trim( $lines[10] ) ) + ); + if ( isset( self::$viewvcUrls[$info['repo-url']] ) ) { + $viewvc = str_replace( + $info['repo-url'], + self::$viewvcUrls[$info['repo-url']], + $info['url'] + ); + $pathRelativeToRepo = substr( $info['url'], strlen( $info['repo-url'] ) ); + $viewvc .= '/?pathrev='; + $viewvc .= urlencode( $info['checkout-rev'] ); + $info['viewvc-url'] = $viewvc; + } + return $info; + } + + /** + * Retrieve the revision number of a Subversion working directory. + * + * @param String $dir Directory of the svn checkout + * @return int revision number as int + */ + public static function getSvnRevision( $dir ) { + $info = self::getSvnInfo( $dir ); + if ( $info === false ) { + return false; + } elseif ( isset( $info['checkout-rev'] ) ) { + return $info['checkout-rev']; } else { - // subversion is release 1.4 - return intval( $content[3] ); + return false; } } diff --git a/includes/specials/SpecialWantedcategories.php b/includes/specials/SpecialWantedcategories.php index 7497f9be..5e5a4f17 100644 --- a/includes/specials/SpecialWantedcategories.php +++ b/includes/specials/SpecialWantedcategories.php @@ -13,20 +13,12 @@ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ -class WantedCategoriesPage extends QueryPage { +class WantedCategoriesPage extends WantedQueryPage { function getName() { return 'Wantedcategories'; } - function isExpensive() { - return true; - } - - function isSyndicated() { - return false; - } - function getSQL() { $dbr = wfGetDB( DB_SLAVE ); list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' ); @@ -45,32 +37,21 @@ class WantedCategoriesPage extends QueryPage { "; } - function sortDescending() { return true; } - - /** - * Fetch user page links and cache their existence - */ - function preprocessResults( $db, $res ) { - $batch = new LinkBatch; - while ( $row = $db->fetchObject( $res ) ) - $batch->add( $row->namespace, $row->title ); - $batch->execute(); - - // Back to start for display - if ( $db->numRows( $res ) > 0 ) - // If there are no rows we get an error seeking. - $db->dataSeek( $res, 0 ); - } - function formatResult( $skin, $result ) { global $wgLang, $wgContLang; $nt = Title::makeTitle( $result->namespace, $result->title ); - $text = $wgContLang->convert( $nt->getText() ); + $text = htmlspecialchars( $wgContLang->convert( $nt->getText() ) ); $plink = $this->isCached() ? - $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) : - $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) ); + $skin->link( $nt, $text ) : + $skin->link( + $nt, + $text, + array(), + array(), + array( 'broken' ) + ); $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'), $wgLang->formatNum( $result->value ) ); diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php index 4957531e..189b9d8b 100644 --- a/includes/specials/SpecialWantedfiles.php +++ b/includes/specials/SpecialWantedfiles.php @@ -13,20 +13,12 @@ * @copyright Copyright © 2008, Soxred93 * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ -class WantedFilesPage extends QueryPage { +class WantedFilesPage extends WantedQueryPage { function getName() { return 'Wantedfiles'; } - function isExpensive() { - return true; - } - - function isSyndicated() { - return false; - } - function getSQL() { $dbr = wfGetDB( DB_SLAVE ); list( $imagelinks, $page ) = $dbr->tableNamesN( 'imagelinks', 'page' ); @@ -44,55 +36,6 @@ class WantedFilesPage extends QueryPage { GROUP BY il_to "; } - - function sortDescending() { return true; } - - /** - * Fetch user page links and cache their existence - */ - function preprocessResults( $db, $res ) { - $batch = new LinkBatch; - while ( $row = $db->fetchObject( $res ) ) - $batch->add( $row->namespace, $row->title ); - $batch->execute(); - - // Back to start for display - if ( $db->numRows( $res ) > 0 ) - // If there are no rows we get an error seeking. - $db->dataSeek( $res, 0 ); - } - - function formatResult( $skin, $result ) { - global $wgLang, $wgContLang; - - $nt = Title::makeTitle( $result->namespace, $result->title ); - $text = $wgContLang->convert( $nt->getText() ); - - $plink = $this->isCached() ? - $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) : - $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) ); - - return wfSpecialList( - $plink, - $this->makeWlhLink( $nt, $skin, $result ) - ); - } - - /** - * Make a "what links here" link for a given title - * - * @param Title $title Title to make the link for - * @param Skin $skin Skin to use - * @param object $result Result row - * @return string - */ - private function makeWlhLink( $title, $skin, $result ) { - global $wgLang; - $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' ); - $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $result->value ) ); - return $skin->link( $wlh, $label, array(), array( 'target' => $title->getPrefixedText() ) ); - } } /** diff --git a/includes/specials/SpecialWantedpages.php b/includes/specials/SpecialWantedpages.php index 7307b335..eeca87ab 100644 --- a/includes/specials/SpecialWantedpages.php +++ b/includes/specials/SpecialWantedpages.php @@ -8,7 +8,7 @@ * implements Special:Wantedpages * @ingroup SpecialPage */ -class WantedPagesPage extends QueryPage { +class WantedPagesPage extends WantedQueryPage { var $nlinks; function WantedPagesPage( $inc = false, $nlinks = true ) { @@ -20,11 +20,6 @@ class WantedPagesPage extends QueryPage { return 'Wantedpages'; } - function isExpensive() { - return true; - } - function isSyndicated() { return false; } - function getSQL() { global $wgWantedPagesThreshold; $count = $wgWantedPagesThreshold - 1; @@ -32,83 +27,23 @@ class WantedPagesPage extends QueryPage { $pagelinks = $dbr->tableName( 'pagelinks' ); $page = $dbr->tableName( 'page' ); $sql = "SELECT 'Wantedpages' AS type, - pl_namespace AS namespace, - pl_title AS title, - COUNT(*) AS value - FROM $pagelinks - LEFT JOIN $page AS pg1 - ON pl_namespace = pg1.page_namespace AND pl_title = pg1.page_title - LEFT JOIN $page AS pg2 - ON pl_from = pg2.page_id - WHERE pg1.page_namespace IS NULL - AND pl_namespace NOT IN ( 2, 3 ) - AND pg2.page_namespace != 8 - GROUP BY pl_namespace, pl_title - HAVING COUNT(*) > $count"; + pl_namespace AS namespace, + pl_title AS title, + COUNT(*) AS value + FROM $pagelinks + LEFT JOIN $page AS pg1 + ON pl_namespace = pg1.page_namespace AND pl_title = pg1.page_title + LEFT JOIN $page AS pg2 + ON pl_from = pg2.page_id + WHERE pg1.page_namespace IS NULL + AND pl_namespace NOT IN ( " . NS_USER . ", ". NS_USER_TALK . ") + AND pg2.page_namespace != " . NS_MEDIAWIKI . " + GROUP BY pl_namespace, pl_title + HAVING COUNT(*) > $count"; wfRunHooks( 'WantedPages::getSQL', array( &$this, &$sql ) ); return $sql; } - - /** - * Cache page existence for performance - */ - function preprocessResults( $db, $res ) { - $batch = new LinkBatch; - while ( $row = $db->fetchObject( $res ) ) - $batch->add( $row->namespace, $row->title ); - $batch->execute(); - - // Back to start for display - if ( $db->numRows( $res ) > 0 ) - // If there are no rows we get an error seeking. - $db->dataSeek( $res, 0 ); - } - - /** - * Format an individual result - * - * @param $skin Skin to use for UI elements - * @param $result Result row - * @return string - */ - public function formatResult( $skin, $result ) { - $title = Title::makeTitleSafe( $result->namespace, $result->title ); - if( $title instanceof Title ) { - if( $this->isCached() ) { - $pageLink = $title->exists() - ? '<s>' . $skin->makeLinkObj( $title ) . '</s>' - : $skin->makeBrokenLinkObj( $title ); - } else { - $pageLink = $skin->makeBrokenLinkObj( $title ); - } - return wfSpecialList( $pageLink, $this->makeWlhLink( $title, $skin, $result ) ); - } else { - $tsafe = htmlspecialchars( $result->title ); - return wfMsg( 'wantedpages-badtitle', $tsafe ); - } - } - - /** - * Make a "what links here" link for a specified result if required - * - * @param $title Title to make the link for - * @param $skin Skin to use - * @param $result Result row - * @return string - */ - private function makeWlhLink( $title, $skin, $result ) { - global $wgLang; - if( $this->nlinks ) { - $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' ); - $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $result->value ) ); - return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() ); - } else { - return null; - } - } - } /** diff --git a/includes/specials/SpecialWantedtemplates.php b/includes/specials/SpecialWantedtemplates.php index 7dd9a262..329d7a3f 100644 --- a/includes/specials/SpecialWantedtemplates.php +++ b/includes/specials/SpecialWantedtemplates.php @@ -15,20 +15,12 @@ * @copyright Copyright © 2008, Danny B. * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ -class WantedTemplatesPage extends QueryPage { +class WantedTemplatesPage extends WantedQueryPage { function getName() { return 'Wantedtemplates'; } - function isExpensive() { - return true; - } - - function isSyndicated() { - return false; - } - function getSQL() { $dbr = wfGetDB( DB_SLAVE ); list( $templatelinks, $page ) = $dbr->tableNamesN( 'templatelinks', 'page' ); @@ -45,55 +37,6 @@ class WantedTemplatesPage extends QueryPage { GROUP BY tl_namespace, tl_title "; } - - function sortDescending() { return true; } - - /** - * Fetch user page links and cache their existence - */ - function preprocessResults( $db, $res ) { - $batch = new LinkBatch; - while ( $row = $db->fetchObject( $res ) ) - $batch->add( $row->namespace, $row->title ); - $batch->execute(); - - // Back to start for display - if ( $db->numRows( $res ) > 0 ) - // If there are no rows we get an error seeking. - $db->dataSeek( $res, 0 ); - } - - function formatResult( $skin, $result ) { - global $wgLang, $wgContLang; - - $nt = Title::makeTitle( $result->namespace, $result->title ); - $text = $wgContLang->convert( $nt->getText() ); - - $plink = $this->isCached() ? - $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) : - $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) ); - - return wfSpecialList( - $plink, - $this->makeWlhLink( $nt, $skin, $result ) - ); - } - - /** - * Make a "what links here" link for a given title - * - * @param Title $title Title to make the link for - * @param Skin $skin Skin to use - * @param object $result Result row - * @return string - */ - private function makeWlhLink( $title, $skin, $result ) { - global $wgLang; - $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' ); - $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $result->value ) ); - return $skin->link( $wlh, $label, array(), array( 'target' => $title->getPrefixedText() ) ); - } } /** diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index b14577b5..c32af2ae 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -12,7 +12,25 @@ function wfSpecialWatchlist( $par ) { global $wgUser, $wgOut, $wgLang, $wgRequest; global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker; - global $wgEnotifWatchlist; + + // Add feed links + $wlToken = $wgUser->getOption( 'watchlisttoken' ); + if (!$wlToken) { + $wlToken = sha1( mt_rand() . microtime( true ) ); + $wgUser->setOption( 'watchlisttoken', $wlToken ); + $wgUser->saveSettings(); + } + + global $wgServer, $wgScriptPath, $wgFeedClasses; + $apiParams = array( 'action' => 'feedwatchlist', 'allrev' => 'allrev', + 'wlowner' => $wgUser->getName(), 'wltoken' => $wlToken ); + $feedTemplate = wfScript('api').'?'; + + foreach( $wgFeedClasses as $format => $class ) { + $theseParams = $apiParams + array( 'feedformat' => $format ); + $url = $feedTemplate . wfArrayToCGI( $theseParams ); + $wgOut->addFeedLink( $format, $url ); + } $skin = $wgUser->getSkin(); $specialTitle = SpecialPage::getTitleFor( 'Watchlist' ); @@ -21,8 +39,12 @@ function wfSpecialWatchlist( $par ) { # Anons don't get a watchlist if( $wgUser->isAnon() ) { $wgOut->setPageTitle( wfMsg( 'watchnologin' ) ); - $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), - wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() ); + $llink = $skin->linkKnown( + SpecialPage::getTitleFor( 'Userlogin' ), + wfMsgHtml( 'loginreqlink' ), + array(), + array( 'returnto' => $specialTitle->getPrefixedText() ) + ); $wgOut->addHTML( wfMsgWikiHtml( 'watchlistanontext', $llink ) ); return; } @@ -248,42 +270,17 @@ function wfSpecialWatchlist( $par ) { $cutofflinks = "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n"; - # Spit out some control panel links $thisTitle = SpecialPage::getTitleFor( 'Watchlist' ); - $skin = $wgUser->getSkin(); - $showLinktext = wfMsgHtml( 'show' ); - $hideLinktext = wfMsgHtml( 'hide' ); - # Hide/show minor edits - $label = $hideMinor ? $showLinktext : $hideLinktext; - $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults ); - $links[] = wfMsgHtml( 'rcshowhideminor', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); - - # Hide/show bot edits - $label = $hideBots ? $showLinktext : $hideLinktext; - $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults ); - $links[] = wfMsgHtml( 'rcshowhidebots', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); - - # Hide/show anonymous edits - $label = $hideAnons ? $showLinktext : $hideLinktext; - $linkBits = wfArrayToCGI( array( 'hideAnons' => 1 - (int)$hideAnons ), $nondefaults ); - $links[] = wfMsgHtml( 'rcshowhideanons', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); - - # Hide/show logged in edits - $label = $hideLiu ? $showLinktext : $hideLinktext; - $linkBits = wfArrayToCGI( array( 'hideLiu' => 1 - (int)$hideLiu ), $nondefaults ); - $links[] = wfMsgHtml( 'rcshowhideliu', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); - - # Hide/show own edits - $label = $hideOwn ? $showLinktext : $hideLinktext; - $linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults ); - $links[] = wfMsgHtml( 'rcshowhidemine', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); - - # Hide/show patrolled edits + # Spit out some control panel links + $links[] = wlShowHideLink( $nondefaults, 'rcshowhideminor', 'hideMinor', $hideMinor ); + $links[] = wlShowHideLink( $nondefaults, 'rcshowhidebots', 'hideBots', $hideBots ); + $links[] = wlShowHideLink( $nondefaults, 'rcshowhideanons', 'hideAnons', $hideAnons ); + $links[] = wlShowHideLink( $nondefaults, 'rcshowhideliu', 'hideLiu', $hideLiu ); + $links[] = wlShowHideLink( $nondefaults, 'rcshowhidemine', 'hideOwn', $hideOwn ); + if( $wgUser->useRCPatrol() ) { - $label = $hidePatrolled ? $showLinktext : $hideLinktext; - $linkBits = wfArrayToCGI( array( 'hidePatrolled' => 1 - (int)$hidePatrolled ), $nondefaults ); - $links[] = wfMsgHtml( 'rcshowhidepatr', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) ); + $links[] = wlShowHideLink( $nondefaults, 'rcshowhidepatr', 'hidePatrolled', $hidePatrolled ); } # Namespace filter and put the whole form together. @@ -311,6 +308,8 @@ function wfSpecialWatchlist( $par ) { $form .= Xml::closeElement( 'fieldset' ); $wgOut->addHTML( $form ); + $wgOut->addHTML( ChangesList::flagLegend() ); + # If there's nothing to show, stop here if( $numRows == 0 ) { $wgOut->addWikiMsg( 'watchnochange' ); @@ -334,7 +333,8 @@ function wfSpecialWatchlist( $par ) { $dbr->dataSeek( $res, 0 ); $list = ChangesList::newFromUser( $wgUser ); - + $list->setWatchlistDivs(); + $s = $list->beginRecentChangesList(); $counter = 1; while ( $obj = $dbr->fetchObject( $res ) ) { @@ -368,23 +368,53 @@ function wfSpecialWatchlist( $par ) { $wgOut->addHTML( $s ); } +function wlShowHideLink( $options, $message, $name, $value ) { + global $wgUser; + + $showLinktext = wfMsgHtml( 'show' ); + $hideLinktext = wfMsgHtml( 'hide' ); + $title = SpecialPage::getTitleFor( 'Watchlist' ); + $skin = $wgUser->getSkin(); + + $label = $value ? $showLinktext : $hideLinktext; + $options[$name] = 1 - (int) $value; + + return wfMsgHtml( $message, $skin->linkKnown( $title, $label, array(), $options ) ); +} + + function wlHoursLink( $h, $page, $options = array() ) { global $wgUser, $wgLang, $wgContLang; + $sk = $wgUser->getSkin(); - $s = $sk->makeKnownLink( - $wgContLang->specialPage( $page ), - $wgLang->formatNum( $h ), - wfArrayToCGI( array('days' => ($h / 24.0)), $options ) ); + $title = Title::newFromText( $wgContLang->specialPage( $page ) ); + $options['days'] = ($h / 24.0); + + $s = $sk->linkKnown( + $title, + $wgLang->formatNum( $h ), + array(), + $options + ); + return $s; } function wlDaysLink( $d, $page, $options = array() ) { global $wgUser, $wgLang, $wgContLang; + $sk = $wgUser->getSkin(); - $s = $sk->makeKnownLink( - $wgContLang->specialPage( $page ), - ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) ), - wfArrayToCGI( array('days' => $d), $options ) ); + $title = Title::newFromText( $wgContLang->specialPage( $page ) ); + $options['days'] = $d; + $message = ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) ); + + $s = $sk->linkKnown( + $title, + $message, + array(), + $options + ); + return $s; } diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php index 3f485bd8..b63c0eee 100644 --- a/includes/specials/SpecialWhatlinkshere.php +++ b/includes/specials/SpecialWhatlinkshere.php @@ -7,40 +7,29 @@ */ /** - * Entry point - * @param $par String: An article name ?? - */ -function wfSpecialWhatlinkshere($par = NULL) { - global $wgRequest; - $page = new WhatLinksHerePage( $wgRequest, $par ); - $page->execute(); -} - -/** * implements Special:Whatlinkshere * @ingroup SpecialPage */ -class WhatLinksHerePage { - // Stored data - protected $par; +class SpecialWhatLinksHere extends SpecialPage { // Stored objects protected $opts, $target, $selfTitle; // Stored globals - protected $skin, $request; + protected $skin; protected $limits = array( 20, 50, 100, 250, 500 ); - function WhatLinksHerePage( $request, $par = null ) { + public function __construct() { + parent::__construct( 'Whatlinkshere' ); global $wgUser; - $this->request = $request; $this->skin = $wgUser->getSkin(); - $this->par = $par; } - function execute() { - global $wgOut; + function execute( $par ) { + global $wgOut, $wgRequest; + + $this->setHeaders(); $opts = new FormOptions(); @@ -54,12 +43,12 @@ class WhatLinksHerePage { $opts->add( 'hidelinks', false ); $opts->add( 'hideimages', false ); - $opts->fetchValuesFromRequest( $this->request ); + $opts->fetchValuesFromRequest( $wgRequest ); $opts->validateIntBounds( 'limit', 0, 5000 ); // Give precedence to subpage syntax - if ( isset($this->par) ) { - $opts->setValue( 'target', $this->par ); + if ( isset($par) ) { + $opts->setValue( 'target', $par ); } // Bind to member variable @@ -271,8 +260,18 @@ class WhatLinksHerePage { } } - $suppressRedirect = $row->page_is_redirect ? 'redirect=no' : ''; - $link = $this->skin->makeKnownLinkObj( $nt, '', $suppressRedirect ); + if( $row->page_is_redirect ) { + $query = array( 'redirect' => 'no' ); + } else { + $query = array(); + } + + $link = $this->skin->linkKnown( + $nt, + null, + array(), + $query + ); // Display properties (redirect or template) $propsText = ''; @@ -306,12 +305,21 @@ class WhatLinksHerePage { if ( $title === null ) $title = SpecialPage::getTitleFor( 'Whatlinkshere' ); - $targetText = $target->getPrefixedUrl(); - return $this->skin->makeKnownLinkObj( $title, $text, 'target=' . $targetText ); + return $this->skin->linkKnown( + $title, + $text, + array(), + array( 'target' => $target->getPrefixedText() ) + ); } function makeSelfLink( $text, $query ) { - return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query ); + return $this->skin->linkKnown( + $this->selfTitle, + $text, + array(), + $query + ); } function getPrevNext( $prevId, $nextId ) { @@ -326,18 +334,18 @@ class WhatLinksHerePage { if ( 0 != $prevId ) { $overrides = array( 'from' => $this->opts->getValue( 'back' ) ); - $prev = $this->makeSelfLink( $prev, wfArrayToCGI( $overrides, $changed ) ); + $prev = $this->makeSelfLink( $prev, array_merge( $changed, $overrides ) ); } if ( 0 != $nextId ) { $overrides = array( 'from' => $nextId, 'back' => $prevId ); - $next = $this->makeSelfLink( $next, wfArrayToCGI( $overrides, $changed ) ); + $next = $this->makeSelfLink( $next, array_merge( $changed, $overrides ) ); } $limitLinks = array(); foreach ( $this->limits as $limit ) { $prettyLimit = $wgLang->formatNum( $limit ); $overrides = array( 'limit' => $limit ); - $limitLinks[] = $this->makeSelfLink( $prettyLimit, wfArrayToCGI( $overrides, $changed ) ); + $limitLinks[] = $this->makeSelfLink( $prettyLimit, array_merge( $changed, $overrides ) ); } $nums = $wgLang->pipeList( $limitLinks ); @@ -346,7 +354,7 @@ class WhatLinksHerePage { } function whatlinkshereForm() { - global $wgScript, $wgTitle; + global $wgScript; // We get nicer value from the title object $this->opts->consumeValue( 'target' ); @@ -360,7 +368,7 @@ class WhatLinksHerePage { $f = Xml::openElement( 'form', array( 'action' => $wgScript ) ); # Values that should not be forgotten - $f .= Xml::hidden( 'title', $wgTitle->getPrefixedText() ); + $f .= Xml::hidden( 'title', SpecialPage::getTitleFor( 'Whatlinkshere' )->getPrefixedText() ); foreach ( $this->opts->getUnconsumedValues() as $name => $value ) { $f .= Xml::hidden( $name, $value ); } @@ -388,6 +396,11 @@ class WhatLinksHerePage { return $f; } + /** + * Create filter panel + * + * @return string HTML fieldset and filter panel with the show/hide links + */ function getFilterPanel() { global $wgLang; $show = wfMsgHtml( 'show' ); @@ -400,11 +413,14 @@ class WhatLinksHerePage { $types = array( 'hidetrans', 'hidelinks', 'hideredirs' ); if( $this->target->getNamespace() == NS_FILE ) $types[] = 'hideimages'; + + // Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans', 'whatlinkshere-hidelinks', 'whatlinkshere-hideimages' + // To be sure they will be find by grep foreach( $types as $type ) { $chosen = $this->opts->getValue( $type ); - $msg = wfMsgHtml( "whatlinkshere-{$type}", $chosen ? $show : $hide ); + $msg = $chosen ? $show : $hide; $overrides = array( $type => !$chosen ); - $links[] = $this->makeSelfLink( $msg, wfArrayToCGI( $overrides, $changed ) ); + $links[] = wfMsgHtml( "whatlinkshere-{$type}", $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) ); } return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), $wgLang->pipeList( $links ) ); } diff --git a/includes/specials/SpecialWithoutinterwiki.php b/includes/specials/SpecialWithoutinterwiki.php index 2092e43b..a5d60d2f 100644 --- a/includes/specials/SpecialWithoutinterwiki.php +++ b/includes/specials/SpecialWithoutinterwiki.php @@ -53,7 +53,7 @@ class WithoutInterwikiPage extends PageQueryPage { function getSQL() { $dbr = wfGetDB( DB_SLAVE ); list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' ); - $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : ''; + $prefix = $this->prefix ? 'AND page_title' . $dbr->buildLike( $this->prefix , $dbr->anyString() ) : ''; return "SELECT 'Withoutinterwiki' AS type, page_namespace AS namespace, @@ -75,13 +75,10 @@ class WithoutInterwikiPage extends PageQueryPage { } function wfSpecialWithoutinterwiki() { - global $wgRequest, $wgContLang, $wgCapitalLinks; + global $wgRequest, $wgContLang; list( $limit, $offset ) = wfCheckLimits(); - if( $wgCapitalLinks ) { - $prefix = $wgContLang->ucfirst( $wgRequest->getVal( 'prefix' ) ); - } else { - $prefix = $wgRequest->getVal( 'prefix' ); - } + // Only searching the mainspace anyway + $prefix = Title::capitalize( $wgRequest->getVal( 'prefix' ), NS_MAIN ); $wip = new WithoutInterwikiPage(); $wip->setPrefix( $prefix ); $wip->doQuery( $offset, $limit ); |