summaryrefslogtreecommitdiff
path: root/includes/specials
diff options
context:
space:
mode:
Diffstat (limited to 'includes/specials')
-rw-r--r--includes/specials/SpecialAllmessages.php217
-rw-r--r--includes/specials/SpecialAllpages.php404
-rw-r--r--includes/specials/SpecialAncientpages.php60
-rw-r--r--includes/specials/SpecialBlankpage.php6
-rw-r--r--includes/specials/SpecialBlockip.php494
-rw-r--r--includes/specials/SpecialBlockme.php37
-rw-r--r--includes/specials/SpecialBooksources.php110
-rw-r--r--includes/specials/SpecialBrokenRedirects.php93
-rw-r--r--includes/specials/SpecialCategories.php112
-rw-r--r--includes/specials/SpecialConfirmemail.php139
-rw-r--r--includes/specials/SpecialContributions.php470
-rw-r--r--includes/specials/SpecialDeadendpages.php62
-rw-r--r--includes/specials/SpecialDisambiguations.php108
-rw-r--r--includes/specials/SpecialDoubleRedirects.php103
-rw-r--r--includes/specials/SpecialEmailuser.php286
-rw-r--r--includes/specials/SpecialExport.php284
-rw-r--r--includes/specials/SpecialFewestrevisions.php74
-rw-r--r--includes/specials/SpecialFileDuplicateSearch.php135
-rw-r--r--includes/specials/SpecialFilepath.php53
-rw-r--r--includes/specials/SpecialImagelist.php161
-rw-r--r--includes/specials/SpecialImport.php1154
-rw-r--r--includes/specials/SpecialIpblocklist.php427
-rw-r--r--includes/specials/SpecialListgrouprights.php112
-rw-r--r--includes/specials/SpecialListredirects.php58
-rw-r--r--includes/specials/SpecialListusers.php235
-rw-r--r--includes/specials/SpecialLockdb.php131
-rw-r--r--includes/specials/SpecialLog.php65
-rw-r--r--includes/specials/SpecialLonelypages.php58
-rw-r--r--includes/specials/SpecialLongpages.php31
-rw-r--r--includes/specials/SpecialMIMEsearch.php138
-rw-r--r--includes/specials/SpecialMergeHistory.php448
-rw-r--r--includes/specials/SpecialMostcategories.php58
-rw-r--r--includes/specials/SpecialMostimages.php54
-rw-r--r--includes/specials/SpecialMostlinked.php95
-rw-r--r--includes/specials/SpecialMostlinkedcategories.php78
-rw-r--r--includes/specials/SpecialMostlinkedtemplates.php132
-rw-r--r--includes/specials/SpecialMostrevisions.php64
-rw-r--r--includes/specials/SpecialMovepage.php452
-rw-r--r--includes/specials/SpecialNewimages.php211
-rw-r--r--includes/specials/SpecialNewpages.php437
-rw-r--r--includes/specials/SpecialPopularpages.php67
-rw-r--r--includes/specials/SpecialPreferences.php1126
-rw-r--r--includes/specials/SpecialPrefixindex.php152
-rw-r--r--includes/specials/SpecialProtectedpages.php309
-rw-r--r--includes/specials/SpecialProtectedtitles.php216
-rw-r--r--includes/specials/SpecialRandompage.php100
-rw-r--r--includes/specials/SpecialRandomredirect.php19
-rw-r--r--includes/specials/SpecialRecentchanges.php662
-rw-r--r--includes/specials/SpecialRecentchangeslinked.php178
-rw-r--r--includes/specials/SpecialResetpass.php167
-rw-r--r--includes/specials/SpecialRevisiondelete.php1474
-rw-r--r--includes/specials/SpecialSearch.php651
-rw-r--r--includes/specials/SpecialShortpages.php98
-rw-r--r--includes/specials/SpecialSpecialpages.php82
-rw-r--r--includes/specials/SpecialStatistics.php93
-rw-r--r--includes/specials/SpecialUncategorizedcategories.php30
-rw-r--r--includes/specials/SpecialUncategorizedimages.php48
-rw-r--r--includes/specials/SpecialUncategorizedpages.php55
-rw-r--r--includes/specials/SpecialUncategorizedtemplates.php33
-rw-r--r--includes/specials/SpecialUndelete.php1276
-rw-r--r--includes/specials/SpecialUnlockdb.php107
-rw-r--r--includes/specials/SpecialUnusedcategories.php46
-rw-r--r--includes/specials/SpecialUnusedimages.php60
-rw-r--r--includes/specials/SpecialUnusedtemplates.php54
-rw-r--r--includes/specials/SpecialUnwatchedpages.php68
-rw-r--r--includes/specials/SpecialUpload.php1755
-rw-r--r--includes/specials/SpecialUploadMogile.php135
-rw-r--r--includes/specials/SpecialUserlogin.php929
-rw-r--r--includes/specials/SpecialUserlogout.php23
-rw-r--r--includes/specials/SpecialUserrights.php589
-rw-r--r--includes/specials/SpecialVersion.php391
-rw-r--r--includes/specials/SpecialWantedcategories.php90
-rw-r--r--includes/specials/SpecialWantedpages.php131
-rw-r--r--includes/specials/SpecialWatchlist.php383
-rw-r--r--includes/specials/SpecialWhatlinkshere.php408
-rw-r--r--includes/specials/SpecialWithoutinterwiki.php88
76 files changed, 19939 insertions, 0 deletions
diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php
new file mode 100644
index 00000000..c2a8de4e
--- /dev/null
+++ b/includes/specials/SpecialAllmessages.php
@@ -0,0 +1,217 @@
+<?php
+/**
+ * Use this special page to get a list of the MediaWiki system messages.
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor.
+ */
+function wfSpecialAllmessages() {
+ global $wgOut, $wgRequest, $wgMessageCache, $wgTitle;
+ global $wgUseDatabaseMessages;
+
+ # The page isn't much use if the MediaWiki namespace is not being used
+ if( !$wgUseDatabaseMessages ) {
+ $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' );
+ return;
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ wfProfileIn( __METHOD__ . '-setup' );
+ $ot = $wgRequest->getText( 'ot' );
+
+ $navText = wfMsg( 'allmessagestext' );
+
+ # Make sure all extension messages are available
+
+ $wgMessageCache->loadAllMessages();
+
+ $sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) );
+ ksort( $sortedArray );
+ $messages = array();
+
+ foreach ( $sortedArray as $key => $value ) {
+ $messages[$key]['enmsg'] = $value;
+ $messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false ); // wfMsgNoDbNoTrans doesn't exist
+ $messages[$key]['msg'] = wfMsgNoTrans( $key );
+ }
+
+ wfProfileOut( __METHOD__ . '-setup' );
+
+ wfProfileIn( __METHOD__ . '-output' );
+ $wgOut->addScriptFile( 'allmessages.js' );
+ if ( $ot == 'php' ) {
+ $navText .= wfAllMessagesMakePhp( $messages );
+ $wgOut->addHTML( '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( '<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";
+ }
+ $txt .= "</messages>";
+ return $txt;
+}
+
+/**
+ * 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 = '';
+ }
+ $txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n";
+ }
+ $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.
+ */
+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 );
+ $page = $dbr->tableName( 'page' );
+ $sql = "SELECT page_namespace,page_title FROM $page WHERE page_namespace IN (" . NS_MEDIAWIKI . ", " . NS_MEDIAWIKI_TALK . ")";
+ $res = $dbr->query( $sql );
+ while( $s = $dbr->fetchObject( $res ) ) {
+ $pageExists[$s->page_namespace][$s->page_title] = true;
+ }
+ $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();
+ }
+
+ $titleObj =& Title::makeTitle( NS_MEDIAWIKI, $title );
+ $talkPage =& Title::makeTitle( NS_MEDIAWIKI_TALK, $title );
+
+ $changed = ( $m['statmsg'] != $m['msg'] );
+ $message = htmlspecialchars( $m['statmsg'] );
+ $mw = htmlspecialchars( $m['msg'] );
+
+ if( isset( $pageExists[NS_MEDIAWIKI][$title] ) ) {
+ $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . htmlspecialchars( $key ) . '</span>' );
+ } else {
+ $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . htmlspecialchars( $key ) . '</span>' );
+ }
+ if( isset( $pageExists[NS_MEDIAWIKI_TALK][$title] ) ) {
+ $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) );
+ } else {
+ $talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) );
+ }
+
+ $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>";
+ } else {
+ $txt .= "
+ <tr class=\"def\" id=\"sp-allmessages-r1-$i\">
+ <td>
+ $anchor$pageLink<br />$talkLink
+ </td><td>
+$mw
+ </td>
+ </tr>";
+ }
+ $i++;
+ }
+ $txt .= '</table>';
+ wfProfileOut( __METHOD__ . '-output' );
+
+ wfProfileOut( __METHOD__ );
+ return $txt;
+}
diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php
new file mode 100644
index 00000000..7223e317
--- /dev/null
+++ b/includes/specials/SpecialAllpages.php
@@ -0,0 +1,404 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point : initialise variables and call subfunctions.
+ * @param $par String: becomes "FOO" when called like Special:Allpages/FOO (default NULL)
+ * @param $specialPage See the SpecialPage object.
+ */
+function wfSpecialAllpages( $par=NULL, $specialPage ) {
+ global $wgRequest, $wgOut, $wgContLang;
+
+ # GET values
+ $from = $wgRequest->getVal( 'from' );
+ $namespace = $wgRequest->getInt( 'namespace' );
+
+ $namespaces = $wgContLang->getNamespaces();
+
+ $indexPage = new SpecialAllpages();
+
+ $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ?
+ wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) :
+ wfMsg( 'allarticles' )
+ );
+
+ if ( isset($par) ) {
+ $indexPage->showChunk( $namespace, $par, $specialPage->including() );
+ } elseif ( isset($from) ) {
+ $indexPage->showChunk( $namespace, $from, $specialPage->including() );
+ } else {
+ $indexPage->showToplevel ( $namespace, $specialPage->including() );
+ }
+}
+
+/**
+ * Implements Special:Allpages
+ * @ingroup SpecialPage
+ */
+class SpecialAllpages {
+ /**
+ * Maximum number of pages to show on single subpage.
+ */
+ protected $maxPerPage = 960;
+
+ /**
+ * Name of this special page. Used to make title objects that reference back
+ * to this page.
+ */
+ protected $name = 'Allpages';
+
+ /**
+ * Determines, which message describes the input field 'nsfrom'.
+ */
+ protected $nsfromMsg = 'allpagesfrom';
+
+/**
+ * HTML for the top form
+ * @param integer $namespace A namespace constant (default NS_MAIN).
+ * @param string $from Article name we are starting listing at.
+ */
+function namespaceForm ( $namespace = NS_MAIN, $from = '' ) {
+ global $wgScript;
+ $t = SpecialPage::getTitleFor( $this->name );
+
+ $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( $this->nsfromMsg ), 'nsfrom' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'from', 20, $from, array( 'id' => 'nsfrom' ) ) .
+ "</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, $including = false ) {
+ global $wgOut, $wgContLang;
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ # TODO: Either make this *much* faster or cache the title index points
+ # in the querycache table.
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $out = "";
+ $where = array( 'page_namespace' => $namespace );
+
+ global $wgMemc;
+ $key = wfMemcKey( 'allpages', 'ns', $namespace );
+ $lines = $wgMemc->get( $key );
+
+ if( !is_array( $lines ) ) {
+ $options = array( 'LIMIT' => 1 );
+ if ( ! $dbr->implicitOrderby() ) {
+ $options['ORDER BY'] = 'page_title';
+ }
+ $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options );
+ $lastTitle = $firstTitle;
+
+ # This array is going to hold the page_titles in order.
+ $lines = array( $firstTitle );
+
+ # If we are going to show n rows, we need n+1 queries to find the relevant titles.
+ $done = false;
+ for( $i = 0; !$done; ++$i ) {
+ // Fetch the last title of this chunk and the first of the next
+ $chunk = is_null( $lastTitle )
+ ? ''
+ : 'page_title >= ' . $dbr->addQuotes( $lastTitle );
+ $res = $dbr->select(
+ 'page', /* FROM */
+ 'page_title', /* WHAT */
+ $where + array($chunk),
+ __METHOD__,
+ array ('LIMIT' => 2, 'OFFSET' => $this->maxPerPage - 1, 'ORDER BY' => 'page_title') );
+
+ if ( $s = $dbr->fetchObject( $res ) ) {
+ array_push( $lines, $s->page_title );
+ } else {
+ // Final chunk, but ended prematurely. Go back and find the end.
+ $endTitle = $dbr->selectField( 'page', 'MAX(page_title)',
+ array(
+ 'page_namespace' => $namespace,
+ $chunk
+ ), __METHOD__ );
+ array_push( $lines, $endTitle );
+ $done = true;
+ }
+ if( $s = $dbr->fetchObject( $res ) ) {
+ array_push( $lines, $s->page_title );
+ $lastTitle = $s->page_title;
+ } else {
+ // This was a final chunk and ended exactly at the limit.
+ // Rare but convenient!
+ $done = true;
+ }
+ $dbr->freeResult( $res );
+ }
+ $wgMemc->add( $key, $lines, 3600 );
+ }
+
+ // If there are only two or less sections, don't even display them.
+ // Instead, display the first section directly.
+ if( count( $lines ) <= 2 ) {
+ $this->showChunk( $namespace, '', $including );
+ return;
+ }
+
+ # At this point, $lines should contain an even number of elements.
+ $out .= "<table class='allpageslist' style='background: inherit;'>";
+ while ( count ( $lines ) > 0 ) {
+ $inpoint = array_shift ( $lines );
+ $outpoint = array_shift ( $lines );
+ $out .= $this->showline ( $inpoint, $outpoint, $namespace, false );
+ }
+ $out .= '</table>';
+ $nsForm = $this->namespaceForm( $namespace, '', false );
+
+ # Is there more?
+ if ( $including ) {
+ $out2 = '';
+ } else {
+ $morelinks = '';
+ if ( $morelinks != '' ) {
+ $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;">';
+ $out2 .= $morelinks . '</td></tr></table><hr />';
+ } else {
+ $out2 = $nsForm . '<hr />';
+ }
+ }
+
+ $wgOut->addHtml( $out2 . $out );
+}
+
+/**
+ * @todo Document
+ * @param string $from
+ * @param integer $namespace (Default NS_MAIN)
+ */
+function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) {
+ global $wgContLang;
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+ $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) );
+ $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) );
+ $queryparams = ($namespace ? "namespace=$namespace" : '');
+ $special = SpecialPage::getTitleFor( $this->name, $inpoint );
+ $link = $special->escapeLocalUrl( $queryparams );
+
+ $out = wfMsgHtml(
+ 'alphaindexline',
+ "<a href=\"$link\">$inpointf</a></td><td><a href=\"$link\">",
+ "</a></td><td><a href=\"$link\">$outpointf</a>"
+ );
+ return '<tr><td align="' . $align . '">'.$out.'</td></tr>';
+}
+
+/**
+ * @param integer $namespace (Default NS_MAIN)
+ * @param string $from list all pages from this name (default FALSE)
+ */
+function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
+ global $wgOut, $wgUser, $wgContLang;
+
+ $sk = $wgUser->getSkin();
+
+ $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+ $namespaces = $wgContLang->getNamespaces();
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ $n = 0;
+
+ if ( !$fromList ) {
+ $out = wfMsgWikiHtml( 'allpagesbadtitle' );
+ } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
+ // Show errormessage and reset to NS_MAIN
+ $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
+ $namespace = NS_MAIN;
+ } else {
+ list( $namespace, $fromKey, $from ) = $fromList;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'page',
+ array( 'page_namespace', 'page_title', 'page_is_redirect' ),
+ array(
+ 'page_namespace' => $namespace,
+ 'page_title >= ' . $dbr->addQuotes( $fromKey )
+ ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'page_title',
+ 'LIMIT' => $this->maxPerPage + 1,
+ 'USE INDEX' => 'name_title',
+ )
+ );
+
+ if( $res->numRows() > 0 ) {
+ $out = '<table style="background: inherit;" border="0" width="100%">';
+
+ while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ $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 ) .
+ ($s->page_is_redirect ? '</div>' : '' );
+ } else {
+ $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
+ }
+ if( $n % 3 == 0 ) {
+ $out .= '<tr>';
+ }
+ $out .= "<td width=\"33%\">$link</td>";
+ $n++;
+ if( $n % 3 == 0 ) {
+ $out .= '</tr>';
+ }
+ }
+ if( ($n % 3) != 0 ) {
+ $out .= '</tr>';
+ }
+ $out .= '</table>';
+ } else {
+ $out = '';
+ }
+ }
+
+ if ( $including ) {
+ $out2 = '';
+ } else {
+ if( $from == '' ) {
+ // First chunk; no previous link.
+ $prevTitle = null;
+ } else {
+ # Get the last title from previous chunk
+ $dbr = wfGetDB( DB_SLAVE );
+ $res_prev = $dbr->select(
+ 'page',
+ 'page_title',
+ array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ),
+ __METHOD__,
+ array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) )
+ );
+
+ # Get first title of previous complete chunk
+ if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) {
+ $pt = $dbr->fetchObject( $res_prev );
+ $prevTitle = Title::makeTitle( $namespace, $pt->page_title );
+ } else {
+ # The previous chunk is not complete, need to link to the very first title
+ # available in the database
+ $options = array( 'LIMIT' => 1 );
+ if ( ! $dbr->implicitOrderby() ) {
+ $options['ORDER BY'] = 'page_title';
+ }
+ $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), __METHOD__, $options );
+ # Show the previous link if it s not the current requested chunk
+ if( $from != $reallyFirstPage_title ) {
+ $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title );
+ } else {
+ $prevTitle = null;
+ }
+ }
+ }
+
+ $nsForm = $this->namespaceForm( $namespace, $from );
+ $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->makeKnownLink( $wgContLang->specialPage( "Allpages" ),
+ wfMsgHtml ( 'allpages' ) );
+
+ $self = SpecialPage::getTitleFor( 'Allpages' );
+
+ # 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 );
+ $out2 .= ' | ' . $prevLink;
+ }
+
+ if( $n == $this->maxPerPage && $s = $dbr->fetchObject($res) ) {
+ # $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 );
+ $out2 .= ' | ' . $nextLink;
+ }
+ $out2 .= "</td></tr></table><hr />";
+ }
+
+ $wgOut->addHtml( $out2 . $out );
+ if( isset($prevLink) or isset($nextLink) ) {
+ $wgOut->addHtml( '<hr /><p style="font-size: smaller; float: ' . $align . '">' );
+ if( isset( $prevLink ) ) {
+ $wgOut->addHTML( $prevLink );
+ }
+ if( isset( $prevLink ) && isset( $nextLink ) ) {
+ $wgOut->addHTML( ' | ' );
+ }
+ if( isset( $nextLink ) ) {
+ $wgOut->addHTML( $nextLink );
+ }
+ $wgOut->addHTML( '</p>' );
+
+ }
+
+}
+
+/**
+ * @param int $ns the namespace of the article
+ * @param string $text the name of the article
+ * @return array( int namespace, string dbkey, string pagename ) or NULL on error
+ * @static (sort of)
+ * @access private
+ */
+function getNamespaceKeyAndText ($ns, $text) {
+ if ( $text == '' )
+ return array( $ns, '', '' ); # shortcut for common case
+
+ $t = Title::makeTitleSafe($ns, $text);
+ if ( $t && $t->isLocal() ) {
+ return array( $t->getNamespace(), $t->getDBkey(), $t->getText() );
+ } else if ( $t ) {
+ return NULL;
+ }
+
+ # try again, in case the problem was an empty pagename
+ $text = preg_replace('/(#|$)/', 'X$1', $text);
+ $t = Title::makeTitleSafe($ns, $text);
+ if ( $t && $t->isLocal() ) {
+ return array( $t->getNamespace(), '', '' );
+ } else {
+ return NULL;
+ }
+}
+}
diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php
new file mode 100644
index 00000000..188ad914
--- /dev/null
+++ b/includes/specials/SpecialAncientpages.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Implements Special:Ancientpages
+ * @ingroup SpecialPage
+ */
+class AncientPagesPage extends QueryPage {
+
+ function getName() {
+ return "Ancientpages";
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ global $wgDBtype;
+ $db = wfGetDB( DB_SLAVE );
+ $page = $db->tableName( 'page' );
+ $revision = $db->tableName( 'revision' );
+ $epoch = $wgDBtype == 'mysql' ? 'UNIX_TIMESTAMP(rev_timestamp)' :
+ 'EXTRACT(epoch FROM rev_timestamp)';
+ return
+ "SELECT 'Ancientpages' as type,
+ page_namespace as namespace,
+ page_title as title,
+ $epoch as value
+ FROM $page, $revision
+ WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0
+ AND page_latest=rev_id";
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $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);
+ }
+}
+
+function wfSpecialAncientpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $app = new AncientPagesPage();
+
+ $app->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialBlankpage.php b/includes/specials/SpecialBlankpage.php
new file mode 100644
index 00000000..fdabe49d
--- /dev/null
+++ b/includes/specials/SpecialBlankpage.php
@@ -0,0 +1,6 @@
+<?php
+
+function wfSpecialBlankpage() {
+ global $wgOut;
+ $wgOut->addHTML(wfMsg('intentionallyblankpage'));
+}
diff --git a/includes/specials/SpecialBlockip.php b/includes/specials/SpecialBlockip.php
new file mode 100644
index 00000000..52829d92
--- /dev/null
+++ b/includes/specials/SpecialBlockip.php
@@ -0,0 +1,494 @@
+<?php
+/**
+ * Constructor for Special:Blockip page
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialBlockip( $par ) {
+ global $wgUser, $wgOut, $wgRequest;
+
+ # Can't block when the database is locked
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ # Permission check
+ if( !$wgUser->isAllowed( 'block' ) ) {
+ $wgOut->permissionRequired( 'block' );
+ return;
+ }
+
+ $ipb = new IPBlockForm( $par );
+
+ $action = $wgRequest->getVal( 'action' );
+ if ( 'success' == $action ) {
+ $ipb->showSuccess();
+ } else if ( $wgRequest->wasPosted() && 'submit' == $action &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $ipb->doSubmit();
+ } else {
+ $ipb->showForm( '' );
+ }
+}
+
+/**
+ * Form object for the Special:Blockip page.
+ *
+ * @ingroup SpecialPage
+ */
+class IPBlockForm {
+ var $BlockAddress, $BlockExpiry, $BlockReason;
+# var $BlockEmail;
+
+ function IPBlockForm( $par ) {
+ global $wgRequest, $wgUser;
+
+ $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->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' );
+
+ # Unchecked checkboxes are not included in the form data at all, so having one
+ # that is true by default is a bit tricky
+ $byDefault = !$wgRequest->wasPosted();
+ $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;
+ }
+
+ function showForm( $err ) {
+ global $wgOut, $wgUser, $wgSysopUserBans;
+
+ $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+ $wgOut->addWikiMsg( 'blockiptext' );
+
+ if($wgSysopUserBans) {
+ $mIpaddress = Xml::label( wfMsg( 'ipadressorusername' ), 'mw-bi-target' );
+ } else {
+ $mIpaddress = Xml::label( wfMsg( 'ipaddress' ), 'mw-bi-target' );
+ }
+ $mIpbexpiry = Xml::label( wfMsg( 'ipbexpiry' ), 'wpBlockExpiry' );
+ $mIpbother = Xml::label( wfMsg( 'ipbother' ), 'mw-bi-other' );
+ $mIpbreasonother = Xml::label( wfMsg( 'ipbreason' ), 'wpBlockReasonList' );
+ $mIpbreason = Xml::label( wfMsg( 'ipbotherreason' ), 'mw-bi-reason' );
+
+ $titleObj = SpecialPage::getTitleFor( 'Blockip' );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
+ $wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $err ) );
+ }
+
+ $scBlockExpiryOptions = wfMsgForContent( 'ipboptions' );
+
+ $showblockoptions = $scBlockExpiryOptions != '-';
+ 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);
+ $blockExpiryFormOptions .= Xml::option( $show, $value, $this->BlockExpiry === $value ? true : false ) . "\n";
+ }
+
+ $reasonDropDown = Xml::listDropDown( 'wpBlockReasonList',
+ wfMsgForContent( 'ipbreason-dropdown' ),
+ wfMsgForContent( 'ipbreasonotherlist' ), '', 'wpBlockDropDown', 4 );
+
+ 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( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'blockip-legend' ) ) .
+ 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()' ) ). "
+ </td>
+ </tr>
+ <tr>"
+ );
+ if ( $showblockoptions ) {
+ $wgOut->addHTML("
+ <td class='mw-label'>
+ {$mIpbexpiry}
+ </td>
+ <td class='mw-input'>" .
+ Xml::tags( 'select',
+ array(
+ 'id' => 'wpBlockExpiry',
+ 'name' => 'wpBlockExpiry',
+ 'onchange' => 'considerChangingExpiryFocus()',
+ 'tabindex' => '2' ),
+ $blockExpiryFormOptions ) .
+ "</td>"
+ );
+ }
+ $wgOut->addHTML("
+ </tr>
+ <tr id='wpBlockOther'>
+ <td class='mw-label'>
+ {$mIpbother}
+ </td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpBlockOther', 45, $this->BlockOther,
+ array( 'tabindex' => '3', 'id' => 'mw-bi-other' ) ) . "
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>
+ {$mIpbreasonother}
+ </td>
+ <td class='mw-input'>
+ {$reasonDropDown}
+ </td>
+ </tr>
+ <tr id=\"wpBlockReason\">
+ <td class='mw-label'>
+ {$mIpbreason}
+ </td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpBlockReason', 45, $this->BlockReason,
+ array( 'tabindex' => '5', 'id' => 'mw-bi-reason', 'maxlength'=> '200' ) ) . "
+ </td>
+ </tr>
+ <tr id='wpAnonOnlyRow'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbanononly' ),
+ 'wpAnonOnly', 'wpAnonOnly', $this->BlockAnonOnly,
+ array( 'tabindex' => '6' ) ) . "
+ </td>
+ </tr>
+ <tr id='wpCreateAccountRow'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbcreateaccount' ),
+ 'wpCreateAccount', 'wpCreateAccount', $this->BlockCreateAccount,
+ array( 'tabindex' => '7' ) ) . "
+ </td>
+ </tr>
+ <tr id='wpEnableAutoblockRow'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbenableautoblock' ),
+ 'wpEnableAutoblock', 'wpEnableAutoblock', $this->BlockEnableAutoblock,
+ array( 'tabindex' => '8' ) ) . "
+ </td>
+ </tr>"
+ );
+
+ global $wgSysopEmailBans;
+ if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
+ $wgOut->addHTML("
+ <tr id='wpEnableEmailBan'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbemailban' ),
+ 'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
+ array( 'tabindex' => '9' )) . "
+ </td>
+ </tr>"
+ );
+ }
+
+ // Allow some users to hide name from block log, blocklist and listusers
+ if ( $wgUser->isAllowed( 'hideuser' ) ) {
+ $wgOut->addHTML("
+ <tr id='wpEnableHideUser'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbhidename' ),
+ 'wpHideName', 'wpHideName', $this->BlockHideName,
+ array( 'tabindex' => '10' ) ) . "
+ </td>
+ </tr>"
+ );
+ }
+
+ # Watchlist their user page?
+ $wgOut->addHTML("
+ <tr id='wpEnableWatchUser'>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'ipbwatchuser' ),
+ 'wpWatchUser', 'wpWatchUser', $this->BlockWatchUser,
+ array( 'tabindex' => '11' ) ) . "
+ </td>
+ </tr>"
+ );
+
+ $wgOut->addHTML("
+ <tr>
+ <td style='padding-top: 1em'>&nbsp;</td>
+ <td class='mw-submit' style='padding-top: 1em'>" .
+ Xml::submitButton( wfMsg( 'ipbsubmit' ),
+ array( 'name' => 'wpBlock', 'tabindex' => '12' ) ) . "
+ </td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) .
+ Xml::tags( 'script', array( 'type' => 'text/javascript' ), 'updateBlockOptions()' ) . "\n"
+ );
+
+ $wgOut->addHtml( $this->getConvenienceLinks() );
+
+ $user = User::newFromName( $this->BlockAddress );
+ if( is_object( $user ) ) {
+ $this->showLogFragment( $wgOut, $user->getUserPage() );
+ } elseif( preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $this->BlockAddress ) ) {
+ $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) );
+ } elseif( preg_match( '/^\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}/', $this->BlockAddress ) ) {
+ $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) );
+ }
+ }
+
+ /**
+ * 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;
+
+ $userId = 0;
+ # Expand valid IPv6 addresses, usernames are left as is
+ $this->BlockAddress = IP::sanitizeIP( $this->BlockAddress );
+ # isIPv4() and IPv6() are used for final validation
+ $rxIP4 = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';
+ $rxIP6 = '\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}';
+ $rxIP = "($rxIP4|$rxIP6)";
+
+ # Check for invalid specifications
+ if ( !preg_match( "/^$rxIP$/", $this->BlockAddress ) ) {
+ $matches = array();
+ 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');
+ }
+ $this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
+ } else {
+ # Range block illegal
+ return array('range_block_disabled');
+ }
+ } else if ( 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');
+ }
+ $this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
+ } else {
+ # Range block illegal
+ return array('range_block_disabled');
+ }
+ } else {
+ # Username block
+ 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 ) );
+ }
+ } else {
+ return array('badipaddress');
+ }
+ }
+ }
+
+ $reasonstr = $this->BlockReasonList;
+ if ( $reasonstr != 'other' && $this->BlockReason != '') {
+ // Entry from drop down menu + additional comment
+ $reasonstr .= ': ' . $this->BlockReason;
+ } elseif ( $reasonstr == 'other' ) {
+ $reasonstr = $this->BlockReason;
+ }
+
+ $expirestr = $this->BlockExpiry;
+ if( $expirestr == 'other' )
+ $expirestr = $this->BlockOther;
+
+ if ((strlen($expirestr) == 0) || (strlen($expirestr) > 50)) {
+ return array('ipb_expiry_invalid');
+ }
+
+ if ( false === ($expiry = Block::parseExpiryInput( $expirestr )) ) {
+ // Bad expiry.
+ return array('ipb_expiry_invalid');
+ }
+
+ if( $this->BlockHideName && $expiry != 'infinity' ) {
+ // Bad expiry.
+ return array('ipb_expiry_temp');
+ }
+
+ # Create block
+ # 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 );
+
+ if ( wfRunHooks('BlockIp', array(&$block, &$wgUser)) ) {
+
+ if ( !$block->insert() ) {
+ return array('ipb_already_blocked', htmlspecialchars($this->BlockAddress));
+ }
+
+ wfRunHooks('BlockIpComplete', array($block, $wgUser));
+
+ if ( $this->BlockWatchUser ) {
+ $wgUser->addWatch ( Title::makeTitle( NS_USER, $this->BlockAddress ) );
+ }
+
+ # Prepare log parameters
+ $logParams = array();
+ $logParams[] = $expirestr;
+ $logParams[] = $this->blockLogFlags();
+
+ # Make log entry, if the name is hidden, put it in the oversight log
+ $log_type = ($this->BlockHideName) ? 'suppress' : 'block';
+ $log = new LogPage( $log_type );
+ $log->addEntry( 'block', Title::makeTitle( NS_USER, $this->BlockAddress ),
+ $reasonstr, $logParams );
+
+ # Report to the user
+ return array();
+ }
+ else
+ return array('hookaborted');
+ }
+
+ /**
+ * UI entry point for blocking
+ * Wraps around doBlock()
+ */
+ function doSubmit()
+ {
+ global $wgOut;
+ $retval = $this->doBlock();
+ if(empty($retval)) {
+ $titleObj = SpecialPage::getTitleFor( 'Blockip' );
+ $wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' .
+ urlencode( $this->BlockAddress ) ) );
+ return;
+ }
+ $key = array_shift($retval);
+ $this->showForm(wfMsgReal($key, $retval));
+ }
+
+ function showSuccess() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+ $wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) );
+ $text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress );
+ $wgOut->addHtml( $text );
+ }
+
+ function showLogFragment( $out, $title ) {
+ $out->addHtml( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) );
+ LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText() );
+ }
+
+ /**
+ * Return a comma-delimited list of "flags" to be passed to the log
+ * reader for this block, to provide more information in the logs
+ *
+ * @return array
+ */
+ private function blockLogFlags() {
+ $flags = array();
+ if( $this->BlockAnonOnly && IP::isIPAddress( $this->BlockAddress ) )
+ // when blocking a user the option 'anononly' is not available/has no effect -> do not write this into log
+ $flags[] = 'anononly';
+ if( $this->BlockCreateAccount )
+ $flags[] = 'nocreate';
+ if( !$this->BlockEnableAutoblock )
+ $flags[] = 'noautoblock';
+ if ( $this->BlockEmail )
+ $flags[] = 'noemail';
+ return implode( ',', $flags );
+ }
+
+ /**
+ * Builds unblock and block list links
+ *
+ * @return string
+ */
+ private function getConvenienceLinks() {
+ global $wgUser;
+ $skin = $wgUser->getSkin();
+ $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) );
+ $links[] = $this->getUnblockLink( $skin );
+ $links[] = $this->getBlockListLink( $skin );
+ return '<p class="mw-ipb-conveniencelinks">' . implode( ' | ', $links ) . '</p>';
+ }
+
+ /**
+ * Build a convenient link to unblock the given username or IP
+ * address, if available; otherwise link to a blank unblock
+ * form
+ *
+ * @param $skin Skin to use
+ * @return string
+ */
+ private function getUnblockLink( $skin ) {
+ $list = SpecialPage::getTitleFor( 'Ipblocklist' );
+ if( $this->BlockAddress ) {
+ $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
+ return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock-addr', $addr ),
+ 'action=unblock&ip=' . urlencode( $this->BlockAddress ) );
+ } else {
+ return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock' ), 'action=unblock' );
+ }
+ }
+
+ /**
+ * Build a convenience link to the block list
+ *
+ * @param $skin Skin to use
+ * @return string
+ */
+ private function getBlockListLink( $skin ) {
+ $list = SpecialPage::getTitleFor( 'Ipblocklist' );
+ if( $this->BlockAddress ) {
+ $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
+ return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist-addr', $addr ),
+ 'ip=' . urlencode( $this->BlockAddress ) );
+ } else {
+ return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist' ) );
+ }
+ }
+}
diff --git a/includes/specials/SpecialBlockme.php b/includes/specials/SpecialBlockme.php
new file mode 100644
index 00000000..f222e3c6
--- /dev/null
+++ b/includes/specials/SpecialBlockme.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialBlockme() {
+ global $wgRequest, $wgBlockOpenProxies, $wgOut, $wgProxyKey;
+
+ $ip = wfGetIP();
+
+ if( !$wgBlockOpenProxies || $wgRequest->getText( 'ip' ) != md5( $ip . $wgProxyKey ) ) {
+ $wgOut->addWikiMsg( 'proxyblocker-disabled' );
+ return;
+ }
+
+ $blockerName = wfMsg( "proxyblocker" );
+ $reason = wfMsg( "proxyblockreason" );
+
+ $u = User::newFromName( $blockerName );
+ $id = $u->idForName();
+ if ( !$id ) {
+ $u = User::newFromName( $blockerName );
+ $u->addToDatabase();
+ $u->setPassword( bin2hex( mt_rand(0, 0x7fffffff ) ) );
+ $u->saveSettings();
+ $id = $u->getID();
+ }
+
+ $block = new Block( $ip, 0, $id, $reason, wfTimestampNow() );
+ $block->insert();
+
+ $wgOut->addWikiMsg( "proxyblocksuccess" );
+}
diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php
new file mode 100644
index 00000000..0690c5c0
--- /dev/null
+++ b/includes/specials/SpecialBooksources.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * Special page outputs information on sourcing a book with a particular ISBN
+ * The parser creates links to this page when dealing with ISBNs in wikitext
+ *
+ * @author Rob Church <robchur@gmail.com>
+ * @todo Validate ISBNs using the standard check-digit method
+ * @ingroup SpecialPages
+ */
+class SpecialBookSources extends SpecialPage {
+
+ /**
+ * ISBN passed to the page, if any
+ */
+ private $isbn = '';
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ parent::__construct( 'Booksources' );
+ }
+
+ /**
+ * Show the special page
+ *
+ * @param $isbn ISBN passed as a subpage parameter
+ */
+ public function execute( $isbn ) {
+ global $wgOut, $wgRequest;
+ $this->setHeaders();
+ $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) );
+ $wgOut->addWikiMsg( 'booksources-summary' );
+ $wgOut->addHtml( $this->makeForm() );
+ if( strlen( $this->isbn ) > 0 )
+ $this->showList();
+ }
+
+ /**
+ * Trim ISBN and remove characters which aren't required
+ *
+ * @param $isbn Unclean ISBN
+ * @return string
+ */
+ private function cleanIsbn( $isbn ) {
+ return trim( preg_replace( '![^0-9X]!', '', $isbn ) );
+ }
+
+ /**
+ * Generate a form to allow users to enter an ISBN
+ *
+ * @return string
+ */
+ private function makeForm() {
+ global $wgScript;
+ $title = self::getTitleFor( 'Booksources' );
+ $form = '<fieldset><legend>' . wfMsgHtml( 'booksources-search-legend' ) . '</legend>';
+ $form .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+ $form .= Xml::hidden( 'title', $title->getPrefixedText() );
+ $form .= '<p>' . Xml::inputLabel( wfMsg( 'booksources-isbn' ), 'isbn', 'isbn', 20, $this->isbn );
+ $form .= '&nbsp;' . Xml::submitButton( wfMsg( 'booksources-go' ) ) . '</p>';
+ $form .= Xml::closeElement( 'form' );
+ $form .= '</fieldset>';
+ return $form;
+ }
+
+ /**
+ * Determine where to get the list of book sources from,
+ * format and output them
+ *
+ * @return string
+ */
+ private function showList() {
+ global $wgOut, $wgContLang;
+
+ # Hook to allow extensions to insert additional HTML,
+ # e.g. for API-interacting plugins and so on
+ wfRunHooks( 'BookInformation', array( $this->isbn, &$wgOut ) );
+
+ # Check for a local page such as Project:Book_sources and use that if available
+ $title = Title::makeTitleSafe( NS_PROJECT, wfMsgForContent( 'booksources' ) ); # Show list in content language
+ if( is_object( $title ) && $title->exists() ) {
+ $rev = Revision::newFromTitle( $title );
+ $wgOut->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
+ return true;
+ }
+
+ # Fall back to the defaults given in the language file
+ $wgOut->addWikiMsg( 'booksources-text' );
+ $wgOut->addHtml( '<ul>' );
+ $items = $wgContLang->getBookstoreList();
+ foreach( $items as $label => $url )
+ $wgOut->addHtml( $this->makeListItem( $label, $url ) );
+ $wgOut->addHtml( '</ul>' );
+ return true;
+ }
+
+ /**
+ * Format a book source list item
+ *
+ * @param $label Book source label
+ * @param $url Book source URL
+ * @return string
+ */
+ private function makeListItem( $label, $url ) {
+ $url = str_replace( '$1', $this->isbn, $url );
+ return '<li><a href="' . htmlspecialchars( $url ) . '">' . htmlspecialchars( $label ) . '</a></li>';
+ }
+}
diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php
new file mode 100644
index 00000000..0a16e6de
--- /dev/null
+++ b/includes/specials/SpecialBrokenRedirects.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page listing redirects to non existent page. Those should be
+ * fixed to point to an existing page.
+ * @ingroup SpecialPage
+ */
+class BrokenRedirectsPage extends PageQueryPage {
+ var $targets = array();
+
+ function getName() {
+ return 'BrokenRedirects';
+ }
+
+ function isExpensive( ) { return true; }
+ function isSyndicated() { return false; }
+
+ function getPageHeader( ) {
+ return wfMsgExt( 'brokenredirectstext', array( 'parse' ) );
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' );
+
+ $sql = "SELECT 'BrokenRedirects' AS type,
+ p1.page_namespace AS namespace,
+ p1.page_title AS title,
+ rd_namespace,
+ rd_title
+ FROM $redirect AS rd
+ 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
+ AND p2.page_namespace IS NULL";
+ return $sql;
+ }
+
+ function getOrder() {
+ return '';
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgUser, $wgContLang;
+
+ $fromObj = Title::makeTitle( $result->namespace, $result->title );
+ if ( isset( $result->rd_title ) ) {
+ $toObj = Title::makeTitle( $result->rd_namespace, $result->rd_title );
+ } else {
+ $blinks = $fromObj->getBrokenLinksFrom(); # TODO: check for redirect, not for links
+ if ( $blinks ) {
+ $toObj = $blinks[0];
+ } else {
+ $toObj = false;
+ }
+ }
+
+ // $toObj may very easily be false if the $result list is cached
+ if ( !is_object( $toObj ) ) {
+ return '<s>' . $skin->makeLinkObj( $fromObj ) . '</s>';
+ }
+
+ $from = $skin->makeKnownLinkObj( $fromObj ,'', 'redirect=no' );
+ $edit = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-edit' ), 'action=edit' );
+ $to = $skin->makeBrokenLinkObj( $toObj );
+ $arr = $wgContLang->getArrow();
+
+ $out = "{$from} {$edit}";
+
+ if( $wgUser->isAllowed( 'delete' ) ) {
+ $delete = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-delete' ), 'action=delete' );
+ $out .= " {$delete}";
+ }
+
+ $out .= " {$arr} {$to}";
+ return $out;
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialBrokenRedirects() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $sbr = new BrokenRedirectsPage();
+
+ return $sbr->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php
new file mode 100644
index 00000000..951c2228
--- /dev/null
+++ b/includes/specials/SpecialCategories.php
@@ -0,0 +1,112 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialCategories( $par=null ) {
+ global $wgOut, $wgRequest;
+
+ if( $par == '' ) {
+ $from = $wgRequest->getText( 'from' );
+ } else {
+ $from = $par;
+ }
+ $cap = new CategoryPager( $from );
+ $wgOut->addHTML(
+ wfMsgExt( 'categoriespagetext', array( 'parse' ) ) .
+ $cap->getStartForm( $from ) .
+ $cap->getNavigationBar() .
+ '<ul>' . $cap->getBody() . '</ul>' .
+ $cap->getNavigationBar()
+ );
+}
+
+/**
+ * TODO: Allow sorting by count. We need to have a unique index to do this
+ * properly.
+ *
+ * @ingroup SpecialPage Pager
+ */
+class CategoryPager extends AlphabeticPager {
+ function __construct( $from ) {
+ parent::__construct();
+ $from = str_replace( ' ', '_', $from );
+ if( $from !== '' ) {
+ global $wgCapitalLinks, $wgContLang;
+ if( $wgCapitalLinks ) {
+ $from = $wgContLang->ucfirst( $from );
+ }
+ $this->mOffset = $from;
+ }
+ }
+
+ function getQueryInfo() {
+ global $wgRequest;
+ return array(
+ 'tables' => array( 'category' ),
+ 'fields' => array( 'cat_title','cat_pages' ),
+ 'conds' => array( 'cat_pages > 0' ),
+ 'options' => array( 'USE INDEX' => 'cat_title' ),
+ );
+ }
+
+ function getIndexField() {
+# return array( 'abc' => 'cat_title', 'count' => 'cat_pages' );
+ return 'cat_title';
+ }
+
+ function getDefaultQuery() {
+ parent::getDefaultQuery();
+ unset( $this->mDefaultQuery['from'] );
+ }
+# protected function getOrderTypeMessages() {
+# return array( 'abc' => 'special-categories-sort-abc',
+# 'count' => 'special-categories-sort-count' );
+# }
+
+ protected function getDefaultDirections() {
+# return array( 'abc' => false, 'count' => true );
+ return false;
+ }
+
+ /* 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();
+
+ while ( $row = $this->mResult->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_CATEGORY, $row->cat_title ) );
+ }
+ $batch->execute();
+ $this->mResult->rewind();
+ return parent::getBody();
+ }
+
+ function formatRow($result) {
+ global $wgLang;
+ $title = Title::makeTitle( NS_CATEGORY, $result->cat_title );
+ $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) );
+ $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->cat_pages ) );
+ return Xml::tags('li', null, "$titleText ($count)" ) . "\n";
+ }
+
+ public function getStartForm( $from ) {
+ global $wgScript;
+ $t = SpecialPage::getTitleFor( 'Categories' );
+
+ return
+ Xml::tags( 'form', array( 'method' => 'get', 'action' => $wgScript ),
+ Xml::hidden( 'title', $t->getPrefixedText() ) .
+ Xml::fieldset( wfMsg( 'categories' ),
+ Xml::inputLabel( wfMsg( 'categoriesfrom' ),
+ 'from', 'from', 20, $from ) .
+ ' ' .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) ) );
+ }
+}
diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php
new file mode 100644
index 00000000..9075fb95
--- /dev/null
+++ b/includes/specials/SpecialConfirmemail.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * Special page allows users to request email confirmation message, and handles
+ * processing of the confirmation code when the link in the email is followed
+ *
+ * @ingroup SpecialPage
+ * @author Brion Vibber
+ * @author Rob Church <robchur@gmail.com>
+ */
+class EmailConfirmation extends UnlistedSpecialPage {
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ parent::__construct( 'Confirmemail' );
+ }
+
+ /**
+ * Main execution point
+ *
+ * @param $code Confirmation code passed to the page
+ */
+ function execute( $code ) {
+ global $wgUser, $wgOut;
+ $this->setHeaders();
+ if( empty( $code ) ) {
+ if( $wgUser->isLoggedIn() ) {
+ if( User::isValidEmailAddr( $wgUser->getEmail() ) ) {
+ $this->showRequestForm();
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_noemail' );
+ }
+ } else {
+ $title = SpecialPage::getTitleFor( 'Userlogin' );
+ $self = SpecialPage::getTitleFor( 'Confirmemail' );
+ $skin = $wgUser->getSkin();
+ $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $self->getPrefixedUrl() );
+ $wgOut->addHtml( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) );
+ }
+ } else {
+ $this->attemptConfirm( $code );
+ }
+ }
+
+ /**
+ * Show a nice form for the user to request a confirmation mail
+ */
+ function showRequestForm() {
+ global $wgOut, $wgUser, $wgLang, $wgRequest;
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) {
+ $ok = $wgUser->sendConfirmationMail();
+ if ( WikiError::isError( $ok ) ) {
+ $wgOut->addWikiMsg( 'confirmemail_sendfailed', $ok->toString() );
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_sent' );
+ }
+ } else {
+ if( $wgUser->isEmailConfirmed() ) {
+ $time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true );
+ $wgOut->addWikiMsg( 'emailauthenticated', $time );
+ }
+ if( $wgUser->isEmailConfirmationPending() ) {
+ $wgOut->addWikiMsg( 'confirmemail_pending' );
+ }
+ $wgOut->addWikiMsg( 'confirmemail_text' );
+ $self = SpecialPage::getTitleFor( 'Confirmemail' );
+ $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
+ $form .= wfHidden( 'token', $wgUser->editToken() );
+ $form .= wfSubmitButton( wfMsgHtml( 'confirmemail_send' ) );
+ $form .= wfCloseElement( 'form' );
+ $wgOut->addHtml( $form );
+ }
+ }
+
+ /**
+ * Attempt to confirm the user's email address and show success or failure
+ * as needed; if successful, take the user to log in
+ *
+ * @param $code Confirmation code
+ */
+ function attemptConfirm( $code ) {
+ global $wgUser, $wgOut;
+ $user = User::newFromConfirmationCode( $code );
+ if( is_object( $user ) ) {
+ $user->confirmEmail();
+ $user->saveSettings();
+ $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
+ $wgOut->addWikiMsg( $message );
+ if( !$wgUser->isLoggedIn() ) {
+ $title = SpecialPage::getTitleFor( 'Userlogin' );
+ $wgOut->returnToMain( true, $title );
+ }
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_invalid' );
+ }
+ }
+
+}
+
+/**
+ * Special page allows users to cancel an email confirmation using the e-mail
+ * confirmation code
+ *
+ * @ingroup SpecialPage
+ */
+class EmailInvalidation extends UnlistedSpecialPage {
+
+ public function __construct() {
+ parent::__construct( 'Invalidateemail' );
+ }
+
+ function execute( $code ) {
+ $this->setHeaders();
+ $this->attemptInvalidate( $code );
+ }
+
+ /**
+ * Attempt to invalidate the user's email address and show success or failure
+ * as needed; if successful, link to main page
+ *
+ * @param $code Confirmation code
+ */
+ function attemptInvalidate( $code ) {
+ global $wgUser, $wgOut;
+ $user = User::newFromConfirmationCode( $code );
+ if( is_object( $user ) ) {
+ $user->invalidateEmail();
+ $user->saveSettings();
+ $wgOut->addWikiMsg( 'confirmemail_invalidated' );
+ if( !$wgUser->isLoggedIn() ) {
+ $wgOut->returnToMain();
+ }
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_invalid' );
+ }
+ }
+}
diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php
new file mode 100644
index 00000000..4a131f15
--- /dev/null
+++ b/includes/specials/SpecialContributions.php
@@ -0,0 +1,470 @@
+<?php
+/**
+ * Special:Contributions, show user contributions in a paged list
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Pager for Special:Contributions
+ * @ingroup SpecialPage Pager
+ */
+class ContribsPager extends ReverseChronologicalPager {
+ public $mDefaultDirection = true;
+ var $messages, $target;
+ var $namespace = '', $year = '', $month = '', $mDb;
+
+ function __construct( $target, $namespace = false, $year = false, $month = false ) {
+ parent::__construct();
+ foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist newpageletter minoreditletter' ) as $msg ) {
+ $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
+ }
+ $this->target = $target;
+ $this->namespace = $namespace;
+
+ $year = intval($year);
+ $month = intval($month);
+
+ $this->year = $year > 0 ? $year : false;
+ $this->month = ($month > 0 && $month < 13) ? $month : false;
+ $this->getDateCond();
+
+ $this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
+ }
+
+ function getDefaultQuery() {
+ $query = parent::getDefaultQuery();
+ $query['target'] = $this->target;
+ $query['month'] = $this->month;
+ $query['year'] = $this->year;
+ return $query;
+ }
+
+ function getQueryInfo() {
+ list( $index, $userCond ) = $this->getUserCond();
+ $conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() );
+ $queryInfo = array(
+ 'tables' => array( 'page', 'revision' ),
+ 'fields' => array(
+ 'page_namespace', 'page_title', 'page_is_new', 'page_latest', 'rev_id', 'rev_page',
+ 'rev_text_id', 'rev_timestamp', 'rev_comment', 'rev_minor_edit', 'rev_user',
+ 'rev_user_text', 'rev_parent_id', 'rev_deleted'
+ ),
+ 'conds' => $conds,
+ 'options' => array( 'USE INDEX' => array('revision' => $index) )
+ );
+ wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) );
+ return $queryInfo;
+ }
+
+ function getUserCond() {
+ $condition = array();
+
+ if ( $this->target == 'newbies' ) {
+ $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
+ $condition[] = 'rev_user >' . (int)($max - $max / 100);
+ $index = 'user_timestamp';
+ } else {
+ $condition['rev_user_text'] = $this->target;
+ $index = 'usertext_timestamp';
+ }
+ return array( $index, $condition );
+ }
+
+ function getNamespaceCond() {
+ if ( $this->namespace !== '' ) {
+ return array( 'page_namespace' => (int)$this->namespace );
+ } else {
+ return array();
+ }
+ }
+
+ function getDateCond() {
+ // Given an optional year and month, we need to generate a timestamp
+ // to use as "WHERE rev_timestamp <= result"
+ // Examples: year = 2006 equals < 20070101 (+000000)
+ // year=2005, month=1 equals < 20050201
+ // year=2005, month=12 equals < 20060101
+
+ if (!$this->year && !$this->month)
+ return;
+
+ if ( $this->year ) {
+ $year = $this->year;
+ }
+ else {
+ // If no year given, assume the current one
+ $year = gmdate( 'Y' );
+ // If this month hasn't happened yet this year, go back to last year's month
+ if( $this->month > gmdate( 'n' ) ) {
+ $year--;
+ }
+ }
+
+ if ( $this->month ) {
+ $month = $this->month + 1;
+ // For December, we want January 1 of the next year
+ if ($month > 12) {
+ $month = 1;
+ $year++;
+ }
+ }
+ else {
+ // No month implies we want up to the end of the year in question
+ $month = 1;
+ $year++;
+ }
+
+ if ($year > 2032)
+ $year = 2032;
+ $ymd = (int)sprintf( "%04d%02d01", $year, $month );
+
+ // Y2K38 bug
+ if ($ymd > 20320101)
+ $ymd = 20320101;
+
+ $this->mOffset = $this->mDb->timestamp( "${ymd}000000" );
+ }
+
+ function getIndexField() {
+ return 'rev_timestamp';
+ }
+
+ function getStartBody() {
+ return "<ul>\n";
+ }
+
+ function getEndBody() {
+ return "</ul>\n";
+ }
+
+ /**
+ * Generates each row in the contributions list.
+ *
+ * Contributions which are marked "top" are currently on top of the history.
+ * For these contributions, a [rollback] link is shown for users with roll-
+ * back privileges. The rollback link restores the most recent version that
+ * was not written by the target user.
+ *
+ * @todo This would probably look a lot nicer in a table.
+ */
+ function formatRow( $row ) {
+ wfProfileIn( __METHOD__ );
+
+ global $wgLang, $wgUser, $wgContLang;
+
+ $sk = $this->getSkin();
+ $rev = new Revision( $row );
+
+ $page = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $link = $sk->makeKnownLinkObj( $page );
+ $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' ) . ')';
+ } else {
+ $difftext .= $this->messages['newarticle'];
+ }
+
+ if( !$page->getUserPermissionsErrors( 'rollback', $wgUser )
+ && !$page->getUserPermissionsErrors( 'edit', $wgUser ) ) {
+ $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 ) . ')';
+ } else {
+ $difftext = '(' . $this->messages['diff'] . ')';
+ }
+ $histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')';
+
+ $comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true );
+ $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
+
+ if( $this->target == 'newbies' ) {
+ $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text );
+ $userlink .= ' (' . $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>';
+ } else {
+ $nflag = '';
+ }
+
+ if( $row->rev_minor_edit ) {
+ $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> ';
+ } else {
+ $mflag = '';
+ }
+
+ $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink}{$comment} {$topmarktext}";
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $ret .= ' ' . wfMsgHtml( 'deletedrev' );
+ }
+ // Let extensions add data
+ wfRunHooks( 'ContributionsLineEnding', array( &$this, &$ret, $row ) );
+
+ $ret = "<li>$ret</li>\n";
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ /**
+ * Get the Database object in use
+ *
+ * @return Database
+ */
+ public function getDatabase() {
+ return $this->mDb;
+ }
+
+}
+
+/**
+ * Special page "user contributions".
+ * Shows a list of the contributions of a user.
+ *
+ * @return none
+ * @param $par String: (optional) user name of the user for which to show the contributions
+ */
+function wfSpecialContributions( $par = null ) {
+ global $wgUser, $wgOut, $wgLang, $wgRequest;
+
+ $options = array();
+
+ if ( isset( $par ) && $par == 'newbies' ) {
+ $target = 'newbies';
+ $options['contribs'] = 'newbie';
+ } elseif ( isset( $par ) ) {
+ $target = $par;
+ } else {
+ $target = $wgRequest->getVal( 'target' );
+ }
+
+ // check for radiobox
+ if ( $wgRequest->getVal( 'contribs' ) == 'newbie' ) {
+ $target = 'newbies';
+ $options['contribs'] = 'newbie';
+ }
+
+ if ( !strlen( $target ) ) {
+ $wgOut->addHTML( contributionsForm( '' ) );
+ return;
+ }
+
+ $options['limit'] = $wgRequest->getInt( 'limit', 50 );
+ $options['target'] = $target;
+
+ $nt = Title::makeTitleSafe( NS_USER, $target );
+ if ( !$nt ) {
+ $wgOut->addHTML( contributionsForm( '' ) );
+ return;
+ }
+ $id = User::idFromName( $nt->getText() );
+
+ if ( $target != 'newbies' ) {
+ $target = $nt->getText();
+ $wgOut->setSubtitle( contributionsSub( $nt, $id ) );
+ } else {
+ $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') );
+ }
+
+ if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
+ $options['namespace'] = intval( $ns );
+ } else {
+ $options['namespace'] = '';
+ }
+ if ( $wgUser->isAllowed( 'markbotedit' ) && $wgRequest->getBool( 'bot' ) ) {
+ $options['bot'] = '1';
+ }
+
+ $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
+ # Offset overrides year/month selection
+ if ( ( $month = $wgRequest->getIntOrNull( 'month' ) ) !== null && $month !== -1 ) {
+ $options['month'] = intval( $month );
+ } else {
+ $options['month'] = '';
+ }
+ if ( ( $year = $wgRequest->getIntOrNull( 'year' ) ) !== null ) {
+ $options['year'] = intval( $year );
+ } else if( $options['month'] ) {
+ $thisMonth = intval( gmdate( 'n' ) );
+ $thisYear = intval( gmdate( 'Y' ) );
+ if( intval( $options['month'] ) > $thisMonth ) {
+ $thisYear--;
+ }
+ $options['year'] = $thisYear;
+ } else {
+ $options['year'] = '';
+ }
+
+ wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
+
+ if( $skip ) {
+ $options['year'] = '';
+ $options['month'] = '';
+ }
+
+ $wgOut->addHTML( contributionsForm( $options ) );
+
+ $pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] );
+ if ( !$pager->getNumRows() ) {
+ $wgOut->addWikiMsg( 'nocontribs' );
+ return;
+ }
+
+ # 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>' );
+
+ # 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>' );
+ }
+ }
+}
+
+/**
+ * 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
+ */
+function contributionsSub( $nt, $id ) {
+ global $wgSysopUserBans, $wgLang, $wgUser;
+
+ $sk = $wgUser->getSkin();
+
+ if ( 0 == $id ) {
+ $user = $nt->getText();
+ } else {
+ $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
+ }
+ $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' ) );
+ # Block log link
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() );
+ }
+ # Other logs link
+ $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
+
+ wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
+
+ $links = implode( ' | ', $tools );
+ }
+
+ // 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,
+ // otherwise use 'contribsub2'.
+ if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) {
+ return wfMsgHtml( 'contribsub2', $user, $links );
+ } else {
+ return wfMsgHtml( 'contribsub', "$user ($links)" );
+ }
+}
+
+/**
+ * Generates the namespace selector form with hidden attributes.
+ * @param $options Array: the options to be included.
+ */
+function contributionsForm( $options ) {
+ global $wgScript, $wgTitle, $wgRequest;
+
+ $options['title'] = $wgTitle->getPrefixedText();
+ if ( !isset( $options['target'] ) ) {
+ $options['target'] = '';
+ } else {
+ $options['target'] = str_replace( '_' , ' ' , $options['target'] );
+ }
+
+ if ( !isset( $options['namespace'] ) ) {
+ $options['namespace'] = '';
+ }
+
+ if ( !isset( $options['contribs'] ) ) {
+ $options['contribs'] = 'user';
+ }
+
+ if ( !isset( $options['year'] ) ) {
+ $options['year'] = '';
+ }
+
+ if ( !isset( $options['month'] ) ) {
+ $options['month'] = '';
+ }
+
+ if ( $options['contribs'] == 'newbie' ) {
+ $options['target'] = '';
+ }
+
+ $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+
+ foreach ( $options as $name => $value ) {
+ if ( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) {
+ continue;
+ }
+ $f .= "\t" . Xml::hidden( $name, $value ) . "\n";
+ }
+
+ $f .= '<fieldset>' .
+ Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
+ Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), 'contribs' , 'newbie' , 'newbie', $options['contribs'] == 'newbie' ? true : false ) . '<br />' .
+ Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), 'contribs' , 'user', 'user', $options['contribs'] == 'user' ? true : false ) . ' ' .
+ Xml::input( 'target', 20, $options['target']) . ' '.
+ '<span style="white-space: nowrap">' .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
+ Xml::namespaceSelector( $options['namespace'], '' ) .
+ '</span>' .
+ Xml::openElement( 'p' ) .
+ '<span style="white-space: nowrap">' .
+ Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
+ Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) .
+ '</span>' .
+ ' '.
+ '<span style="white-space: nowrap">' .
+ Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
+ Xml::monthSelector( $options['month'], -1 ) . ' '.
+ '</span>' .
+ Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
+ Xml::closeElement( 'p' );
+
+ $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' );
+ if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) )
+ $f .= "<p>{$explain}</p>";
+
+ $f .= '</fieldset>' .
+ Xml::closeElement( 'form' );
+ return $f;
+}
diff --git a/includes/specials/SpecialDeadendpages.php b/includes/specials/SpecialDeadendpages.php
new file mode 100644
index 00000000..a8416c97
--- /dev/null
+++ b/includes/specials/SpecialDeadendpages.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class DeadendPagesPage extends PageQueryPage {
+
+ function getName( ) {
+ return "Deadendpages";
+ }
+
+ function getPageHeader() {
+ return wfMsgExt( 'deadendpagestext', array( 'parse' ) );
+ }
+
+ /**
+ * LEFT JOIN is expensive
+ *
+ * @return true
+ */
+ function isExpensive( ) {
+ return 1;
+ }
+
+ function isSyndicated() { return false; }
+
+ /**
+ * @return false
+ */
+ function sortDescending() {
+ return false;
+ }
+
+ /**
+ * @return string an sqlquery
+ */
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' );
+ return "SELECT 'Deadendpages' as type, page_namespace AS namespace, page_title as title, page_title AS value " .
+ "FROM $page LEFT JOIN $pagelinks ON page_id = pl_from " .
+ "WHERE pl_from IS NULL " .
+ "AND page_namespace = 0 " .
+ "AND page_is_redirect = 0";
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialDeadendpages() {
+
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $depp = new DeadendPagesPage();
+
+ return $depp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialDisambiguations.php b/includes/specials/SpecialDisambiguations.php
new file mode 100644
index 00000000..34045660
--- /dev/null
+++ b/includes/specials/SpecialDisambiguations.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class DisambiguationsPage extends PageQueryPage {
+
+ function getName() {
+ return 'Disambiguations';
+ }
+
+ function isExpensive( ) { return true; }
+ function isSyndicated() { return false; }
+
+
+ function getPageHeader( ) {
+ return wfMsgExt( 'disambiguations-text', array( 'parse' ) );
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $dMsgText = wfMsgForContent('disambiguationspage');
+
+ $linkBatch = new LinkBatch;
+
+ # If the text can be treated as a title, use it verbatim.
+ # Otherwise, pull the titles from the links table
+ $dp = Title::newFromText($dMsgText);
+ if( $dp ) {
+ if($dp->getNamespace() != NS_TEMPLATE) {
+ # FIXME we assume the disambiguation message is a template but
+ # the page can potentially be from another namespace :/
+ wfDebug("Mediawiki:disambiguationspage message does not refer to a template!\n");
+ }
+ $linkBatch->addObj( $dp );
+ } else {
+ # Get all the templates linked from the Mediawiki:Disambiguationspage
+ $disPageObj = Title::makeTitleSafe( NS_MEDIAWIKI, 'disambiguationspage' );
+ $res = $dbr->select(
+ array('pagelinks', 'page'),
+ 'pl_title',
+ array('page_id = pl_from', 'pl_namespace' => NS_TEMPLATE,
+ 'page_namespace' => $disPageObj->getNamespace(), 'page_title' => $disPageObj->getDBkey()),
+ __METHOD__ );
+
+ while ( $row = $dbr->fetchObject( $res ) ) {
+ $linkBatch->addObj( Title::makeTitle( NS_TEMPLATE, $row->pl_title ));
+ }
+
+ $dbr->freeResult( $res );
+ }
+
+ $set = $linkBatch->constructSet( 'lb.tl', $dbr );
+ if( $set === false ) {
+ # We must always return a valid sql query, but this way DB will always quicly return an empty result
+ $set = 'FALSE';
+ wfDebug("Mediawiki:disambiguationspage message does not link to any templates!\n");
+ }
+
+ list( $page, $pagelinks, $templatelinks) = $dbr->tableNamesN( 'page', 'pagelinks', 'templatelinks' );
+
+ $sql = "SELECT 'Disambiguations' AS \"type\", pb.page_namespace AS namespace,"
+ ." pb.page_title AS title, la.pl_from AS value"
+ ." FROM {$templatelinks} AS lb, {$page} AS pb, {$pagelinks} AS la, {$page} AS pa"
+ ." WHERE $set" # disambiguation template(s)
+ .' AND pa.page_id = la.pl_from'
+ .' AND pa.page_namespace = ' . NS_MAIN # Limit to just articles in the main namespace
+ .' AND pb.page_id = lb.tl_from'
+ .' AND pb.page_namespace = la.pl_namespace'
+ .' AND pb.page_title = la.pl_title'
+ .' ORDER BY lb.tl_namespace, lb.tl_title';
+
+ return $sql;
+ }
+
+ function getOrder() {
+ return '';
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+ $title = Title::newFromId( $result->value );
+ $dp = Title::makeTitle( $result->namespace, $result->title );
+
+ $from = $skin->makeKnownLinkObj( $title, '' );
+ $edit = $skin->makeKnownLinkObj( $title, "(".wfMsgHtml("qbedit").")" , 'redirect=no&action=edit' );
+ $arr = $wgContLang->getArrow();
+ $to = $skin->makeKnownLinkObj( $dp, '' );
+
+ return "$from $edit $arr $to";
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialDisambiguations() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $sd = new DisambiguationsPage();
+
+ return $sd->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php
new file mode 100644
index 00000000..b1bad0c3
--- /dev/null
+++ b/includes/specials/SpecialDoubleRedirects.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page listing redirects to redirecting page.
+ * The software will automatically not follow double redirects, to prevent loops.
+ * @ingroup SpecialPage
+ */
+class DoubleRedirectsPage extends PageQueryPage {
+
+ function getName() {
+ return 'DoubleRedirects';
+ }
+
+ function isExpensive( ) { return true; }
+ function isSyndicated() { return false; }
+
+ function getPageHeader( ) {
+ return wfMsgExt( 'doubleredirectstext', array( 'parse' ) );
+ }
+
+ function getSQLText( &$dbr, $namespace = null, $title = null ) {
+
+ list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' );
+
+ $limitToTitle = !( $namespace === null && $title === null );
+ $sql = $limitToTitle ? "SELECT" : "SELECT 'DoubleRedirects' as type," ;
+ $sql .=
+ " pa.page_namespace as namespace, pa.page_title as title," .
+ " pb.page_namespace as nsb, pb.page_title as tb," .
+ " pc.page_namespace as nsc, pc.page_title as tc" .
+ " FROM $redirect AS ra, $redirect AS rb, $page AS pa, $page AS pb, $page AS pc" .
+ " WHERE ra.rd_from=pa.page_id" .
+ " AND ra.rd_namespace=pb.page_namespace" .
+ " AND ra.rd_title=pb.page_title" .
+ " AND rb.rd_from=pb.page_id" .
+ " AND rb.rd_namespace=pc.page_namespace" .
+ " AND rb.rd_title=pc.page_title";
+
+ if( $limitToTitle ) {
+ $encTitle = $dbr->addQuotes( $title );
+ $sql .= " AND pa.page_namespace=$namespace" .
+ " AND pa.page_title=$encTitle";
+ }
+
+ return $sql;
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ return $this->getSQLText( $dbr );
+ }
+
+ function getOrder() {
+ return '';
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ $fname = 'DoubleRedirectsPage::formatResult';
+ $titleA = Title::makeTitle( $result->namespace, $result->title );
+
+ if ( $result && !isset( $result->nsb ) ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $sql = $this->getSQLText( $dbr, $result->namespace, $result->title );
+ $res = $dbr->query( $sql, $fname );
+ if ( $res ) {
+ $result = $dbr->fetchObject( $res );
+ $dbr->freeResult( $res );
+ }
+ }
+ if ( !$result ) {
+ return '<s>' . $skin->makeLinkObj( $titleA, '', '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 );
+ $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
+
+ return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialDoubleRedirects() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $sdr = new DoubleRedirectsPage();
+
+ return $sdr->doQuery( $offset, $limit );
+
+}
diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php
new file mode 100644
index 00000000..3874c6a1
--- /dev/null
+++ b/includes/specials/SpecialEmailuser.php
@@ -0,0 +1,286 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ */
+function wfSpecialEmailuser( $par ) {
+ global $wgRequest, $wgUser, $wgOut;
+
+ $action = $wgRequest->getVal( 'action' );
+ $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
+ $targetUser = EmailUserForm::validateEmailTarget( $target );
+
+ if ( !( $targetUser instanceof User ) ) {
+ $wgOut->showErrorPage( $targetUser[0], $targetUser[1] );
+ return;
+ }
+
+ $form = new EmailUserForm( $targetUser,
+ $wgRequest->getText( 'wpText' ),
+ $wgRequest->getText( 'wpSubject' ),
+ $wgRequest->getBool( 'wpCCMe' ) );
+ if ( $action == 'success' ) {
+ $form->showSuccess();
+ return;
+ }
+
+ $error = EmailUserForm::getPermissionsError( $wgUser, $wgRequest->getVal( 'wpEditToken' ) );
+ if ( $error ) {
+ switch ( $error[0] ) {
+ case 'blockedemailuser':
+ $wgOut->blockedPage();
+ return;
+ case 'actionthrottledtext':
+ $wgOut->rateLimited();
+ return;
+ case 'sessionfailure':
+ $form->showForm();
+ return;
+ default:
+ $wgOut->showErrorPage( $error[0], $error[1] );
+ return;
+ }
+ }
+
+
+ if ( "submit" == $action && $wgRequest->wasPosted() ) {
+ $result = $form->doSubmit();
+
+ if ( !is_null( $result ) ) {
+ $wgOut->addHTML( wfMsg( "usermailererror" ) .
+ ' ' . htmlspecialchars( $result->getMessage() ) );
+ } else {
+ $titleObj = SpecialPage::getTitleFor( "Emailuser" );
+ $encTarget = wfUrlencode( $form->getTarget()->getName() );
+ $wgOut->redirect( $titleObj->getFullURL( "target={$encTarget}&action=success" ) );
+ }
+ } else {
+ $form->showForm();
+ }
+}
+
+/**
+ * Implements the Special:Emailuser web interface, and invokes userMailer for sending the email message.
+ * @ingroup SpecialPage
+ */
+class EmailUserForm {
+
+ var $target;
+ var $text, $subject;
+ var $cc_me; // Whether user requested to be sent a separate copy of their email.
+
+ /**
+ * @param User $target
+ */
+ function EmailUserForm( $target, $text, $subject, $cc_me ) {
+ $this->target = $target;
+ $this->text = $text;
+ $this->subject = $subject;
+ $this->cc_me = $cc_me;
+ }
+
+ function showForm() {
+ global $wgOut, $wgUser;
+ $skin = $wgUser->getSkin();
+
+ $wgOut->setPagetitle( wfMsg( "emailpage" ) );
+ $wgOut->addWikiMsg( "emailpagetext" );
+
+ if ( $this->subject === "" ) {
+ $this->subject = wfMsgExt( 'defemailsubject', array( 'content', 'parsemag' ) );
+ }
+
+ $emf = wfMsg( "emailfrom" );
+ $senderLink = $skin->makeLinkObj(
+ $wgUser->getUserPage(), htmlspecialchars( $wgUser->getName() ) );
+ $emt = wfMsg( "emailto" );
+ $recipientLink = $skin->makeLinkObj(
+ $this->target->getUserPage(), htmlspecialchars( $this->target->getName() ) );
+ $emr = wfMsg( "emailsubject" );
+ $emm = wfMsg( "emailmessage" );
+ $ems = wfMsg( "emailsend" );
+ $emc = wfMsg( "emailccme" );
+ $encSubject = htmlspecialchars( $this->subject );
+
+ $titleObj = SpecialPage::getTitleFor( "Emailuser" );
+ $action = $titleObj->escapeLocalURL( "target=" .
+ urlencode( $this->target->getName() ) . "&action=submit" );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ $wgOut->addHTML( "
+<form id=\"emailuser\" method=\"post\" action=\"{$action}\">
+<table border='0' id='mailheader'><tr>
+<td align='right'>{$emf}:</td>
+<td align='left'><strong>{$senderLink}</strong></td>
+</tr><tr>
+<td align='right'>{$emt}:</td>
+<td align='left'><strong>{$recipientLink}</strong></td>
+</tr><tr>
+<td align='right'>{$emr}:</td>
+<td align='left'>
+<input type='text' size='60' maxlength='200' name=\"wpSubject\" value=\"{$encSubject}\" />
+</td>
+</tr>
+</table>
+<span id='wpTextLabel'><label for=\"wpText\">{$emm}:</label><br /></span>
+<textarea id=\"wpText\" name=\"wpText\" rows='20' cols='80' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) .
+"</textarea>
+" . wfCheckLabel( $emc, 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) . "<br />
+<input type='submit' name=\"wpSend\" value=\"{$ems}\" />
+<input type='hidden' name='wpEditToken' value=\"$token\" />
+</form>\n" );
+
+ }
+
+ /*
+ * Really send a mail. Permissions should have been checked using
+ * EmailUserForm::getPermissionsError. It is probably also a good idea to
+ * check the edit token and ping limiter in advance.
+ */
+ function doSubmit() {
+ global $wgUser, $wgUserEmailUseReplyTo, $wgSiteName;
+
+ $to = new MailAddress( $this->target );
+ $from = new MailAddress( $wgUser );
+ $subject = $this->subject;
+
+ // Add a standard footer and trim up trailing newlines
+ $this->text = rtrim($this->text) . "\n\n---\n" . wfMsgExt( 'emailuserfooter',
+ array( 'content', 'parsemag' ), array( $from->name, $to->name ) );
+
+ if( wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$this->text ) ) ) {
+
+ if( $wgUserEmailUseReplyTo ) {
+ // Put the generic wiki autogenerated address in the From:
+ // header and reserve the user for Reply-To.
+ //
+ // This is a bit ugly, but will serve to differentiate
+ // wiki-borne mails from direct mails and protects against
+ // SPF and bounce problems with some mailers (see below).
+ global $wgPasswordSender;
+ $mailFrom = new MailAddress( $wgPasswordSender );
+ $replyTo = $from;
+ } else {
+ // Put the sending user's e-mail address in the From: header.
+ //
+ // This is clean-looking and convenient, but has issues.
+ // One is that it doesn't as clearly differentiate the wiki mail
+ // from "directly" sent mails.
+ //
+ // Another is that some mailers (like sSMTP) will use the From
+ // address as the envelope sender as well. For open sites this
+ // can cause mails to be flunked for SPF violations (since the
+ // wiki server isn't an authorized sender for various users'
+ // domains) as well as creating a privacy issue as bounces
+ // containing the recipient's e-mail address may get sent to
+ // the sending user.
+ $mailFrom = $from;
+ $replyTo = null;
+ }
+
+ $mailResult = UserMailer::send( $to, $mailFrom, $subject, $this->text, $replyTo );
+
+ if( WikiError::isError( $mailResult ) ) {
+ return $mailResult;
+
+ } else {
+
+ // if the user requested a copy of this mail, do this now,
+ // unless they are emailing themselves, in which case one copy of the message is sufficient.
+ if ($this->cc_me && $to != $from) {
+ $cc_subject = wfMsg('emailccsubject', $this->target->getName(), $subject);
+ if( wfRunHooks( 'EmailUser', array( &$from, &$from, &$cc_subject, &$this->text ) ) ) {
+ $ccResult = UserMailer::send( $from, $from, $cc_subject, $this->text );
+ if( WikiError::isError( $ccResult ) ) {
+ // At this stage, the user's CC mail has failed, but their
+ // original mail has succeeded. It's unlikely, but still, what to do?
+ // We can either show them an error, or we can say everything was fine,
+ // or we can say we sort of failed AND sort of succeeded. Of these options,
+ // simply saying there was an error is probably best.
+ return $ccResult;
+ }
+ }
+ }
+
+ wfRunHooks( 'EmailUserComplete', array( $to, $from, $subject, $this->text ) );
+ return;
+ }
+ }
+ }
+
+ function showSuccess( &$user = null ) {
+ global $wgOut;
+
+ if ( is_null($user) )
+ $user = $this->target;
+
+ $wgOut->setPagetitle( wfMsg( "emailsent" ) );
+ $wgOut->addHTML( wfMsg( "emailsenttext" ) );
+
+ $wgOut->returnToMain( false, $user->getUserPage() );
+ }
+
+ function getTarget() {
+ return $this->target;
+ }
+
+ static function validateEmailTarget ( $target ) {
+ global $wgEnableEmail, $wgEnableUserEmail;
+
+ if( !( $wgEnableEmail && $wgEnableUserEmail ) )
+ return array( "nosuchspecialpage", "nospecialpagetext" );
+
+ if ( "" == $target ) {
+ wfDebug( "Target is empty.\n" );
+ return array( "notargettitle", "notargettext" );
+ }
+
+ $nt = Title::newFromURL( $target );
+ if ( is_null( $nt ) ) {
+ wfDebug( "Target is invalid title.\n" );
+ return array( "notargettitle", "notargettext" );
+ }
+
+ $nu = User::newFromName( $nt->getText() );
+ if( is_null( $nu ) || !$nu->canReceiveEmail() ) {
+ wfDebug( "Target is invalid user or can't receive.\n" );
+ return array( "noemailtitle", "noemailtext" );
+ }
+
+ return $nu;
+ }
+ static function getPermissionsError ( $user, $editToken ) {
+ if( !$user->canSendEmail() ) {
+ wfDebug( "User can't send.\n" );
+ return array( "mailnologin", "mailnologintext" );
+ }
+
+ if( $user->isBlockedFromEmailuser() ) {
+ wfDebug( "User is blocked from sending e-mail.\n" );
+ return array( "blockedemailuser", "" );
+ }
+
+ if( $user->pingLimiter( 'emailuser' ) ) {
+ wfDebug( "Ping limiter triggered.\n" );
+ return array( 'actionthrottledtext', '' );
+ }
+
+ if( !$user->matchEditToken( $editToken ) ) {
+ wfDebug( "Matching edit token failed.\n" );
+ return array( 'sessionfailure', '' );
+ }
+
+ return;
+ }
+
+ static function newFromURL( $target, $text, $subject, $cc_me )
+ {
+ $nt = Title::newFromURL( $target );
+ $nu = User::newFromName( $nt->getText() );
+ return new EmailUserForm( $nu, $text, $subject, $cc_me );
+ }
+}
diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php
new file mode 100644
index 00000000..38bfc83e
--- /dev/null
+++ b/includes/specials/SpecialExport.php
@@ -0,0 +1,284 @@
+<?php
+# Copyright (C) 2003 Brion Vibber <brion@pobox.com>
+# 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
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfExportGetPagesFromCategory( $title ) {
+ global $wgContLang;
+
+ $name = $title->getDBkey();
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
+ $sql = "SELECT page_namespace, page_title FROM $page " .
+ "JOIN $categorylinks ON cl_from = page_id " .
+ "WHERE cl_to = " . $dbr->addQuotes( $name );
+
+ $pages = array();
+ $res = $dbr->query( $sql, 'wfExportGetPagesFromCategory' );
+ while ( $row = $dbr->fetchObject( $res ) ) {
+ $n = $row->page_title;
+ if ($row->page_namespace) {
+ $ns = $wgContLang->getNsText( $row->page_namespace );
+ $n = $ns . ':' . $n;
+ }
+
+ $pages[] = $n;
+ }
+ $dbr->freeResult($res);
+
+ return $pages;
+}
+
+/**
+ * Expand a list of pages to include templates used in those pages.
+ * @param $inputPages array, list of titles to look up
+ * @param $pageSet array, associative array indexed by titles for output
+ * @return array associative array index by titles
+ */
+function wfExportGetTemplates( $inputPages, $pageSet ) {
+ return wfExportGetLinks( $inputPages, $pageSet,
+ 'templatelinks',
+ array( 'tl_namespace AS namespace', 'tl_title AS title' ),
+ array( 'page_id=tl_from' ) );
+}
+
+/**
+ * Expand a list of pages to include images used in those pages.
+ * @param $inputPages array, list of titles to look up
+ * @param $pageSet array, associative array indexed by titles for output
+ * @return array associative array index by titles
+ */
+function wfExportGetImages( $inputPages, $pageSet ) {
+ return wfExportGetLinks( $inputPages, $pageSet,
+ 'imagelinks',
+ array( NS_IMAGE . ' AS namespace', 'il_to AS title' ),
+ array( 'page_id=il_from' ) );
+}
+
+/**
+ * Expand a list of pages to include items used in those pages.
+ * @private
+ */
+function wfExportGetLinks( $inputPages, $pageSet, $table, $fields, $join ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ foreach( $inputPages as $page ) {
+ $title = Title::newFromText( $page );
+ if( $title ) {
+ $pageSet[$title->getPrefixedText()] = true;
+ /// @fixme May or may not be more efficient to batch these
+ /// by namespace when given multiple input pages.
+ $result = $dbr->select(
+ array( 'page', $table ),
+ $fields,
+ array_merge( $join,
+ array(
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDbKey() ) ),
+ __METHOD__ );
+ foreach( $result as $row ) {
+ $template = Title::makeTitle( $row->namespace, $row->title );
+ $pageSet[$template->getPrefixedText()] = true;
+ }
+ }
+ }
+ return $pageSet;
+}
+
+/**
+ * Callback function to remove empty strings from the pages array.
+ */
+function wfFilterPage( $page ) {
+ return $page !== '' && $page !== null;
+}
+
+/**
+ *
+ */
+function wfSpecialExport( $page = '' ) {
+ global $wgOut, $wgRequest, $wgSitename, $wgExportAllowListContributors;
+ global $wgExportAllowHistory, $wgExportMaxHistory;
+
+ $curonly = true;
+ $doexport = false;
+
+ if ( $wgRequest->getCheck( 'addcat' ) ) {
+ $page = $wgRequest->getText( 'pages' );
+ $catname = $wgRequest->getText( 'catname' );
+
+ if ( $catname !== '' && $catname !== NULL && $catname !== false ) {
+ $t = Title::makeTitleSafe( NS_CATEGORY, $catname );
+ if ( $t ) {
+ /**
+ * @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.
+ */
+ $catpages = wfExportGetPagesFromCategory( $t );
+ if ( $catpages ) $page .= "\n" . implode( "\n", $catpages );
+ }
+ }
+ }
+ else if( $wgRequest->wasPosted() && $page == '' ) {
+ $page = $wgRequest->getText( 'pages' );
+ $curonly = $wgRequest->getCheck( 'curonly' );
+ $rawOffset = $wgRequest->getVal( 'offset' );
+ if( $rawOffset ) {
+ $offset = wfTimestamp( TS_MW, $rawOffset );
+ } else {
+ $offset = null;
+ }
+ $limit = $wgRequest->getInt( 'limit' );
+ $dir = $wgRequest->getVal( 'dir' );
+ $history = array(
+ 'dir' => 'asc',
+ 'offset' => false,
+ 'limit' => $wgExportMaxHistory,
+ );
+ $historyCheck = $wgRequest->getCheck( 'history' );
+ if ( $curonly ) {
+ $history = WikiExporter::CURRENT;
+ } elseif ( !$historyCheck ) {
+ if ( $limit > 0 && $limit < $wgExportMaxHistory ) {
+ $history['limit'] = $limit;
+ }
+ if ( !is_null( $offset ) ) {
+ $history['offset'] = $offset;
+ }
+ if ( strtolower( $dir ) == 'desc' ) {
+ $history['dir'] = 'desc';
+ }
+ }
+
+ if( $page != '' ) $doexport = true;
+ } else {
+ // Default to current-only for GET requests
+ $page = $wgRequest->getText( 'pages', $page );
+ $historyCheck = $wgRequest->getCheck( 'history' );
+ if( $historyCheck ) {
+ $history = WikiExporter::FULL;
+ } else {
+ $history = WikiExporter::CURRENT;
+ }
+
+ if( $page != '' ) $doexport = true;
+ }
+
+ if( !$wgExportAllowHistory ) {
+ // Override
+ $history = WikiExporter::CURRENT;
+ }
+
+ $list_authors = $wgRequest->getCheck( 'listauthors' );
+ if ( !$curonly || !$wgExportAllowListContributors ) $list_authors = false ;
+
+ if ( $doexport ) {
+ $wgOut->disable();
+
+ // Cancel output buffering and gzipping if set
+ // This should provide safer streaming for pages with history
+ wfResetOutputBuffers();
+ header( "Content-type: application/xml; charset=utf-8" );
+ if( $wgRequest->getCheck( 'wpDownload' ) ) {
+ // Provide a sane filename suggestion
+ $filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' );
+ $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
+ }
+
+ /* Split up the input and look up linked pages */
+ $inputPages = array_filter( explode( "\n", $page ), 'wfFilterPage' );
+ $pageSet = array_flip( $inputPages );
+
+ if( $wgRequest->getCheck( 'templates' ) ) {
+ $pageSet = wfExportGetTemplates( $inputPages, $pageSet );
+ }
+
+ /*
+ // Enable this when we can do something useful exporting/importing image information. :)
+ if( $wgRequest->getCheck( 'images' ) ) {
+ $pageSet = wfExportGetImages( $inputPages, $pageSet );
+ }
+ */
+
+ $pages = array_keys( $pageSet );
+
+ /* Ok, let's get to it... */
+
+ $db = wfGetDB( DB_SLAVE );
+ $exporter = new WikiExporter( $db, $history );
+ $exporter->list_authors = $list_authors ;
+ $exporter->openStream();
+
+ foreach( $pages as $page ) {
+ /*
+ if( $wgExportMaxHistory && !$curonly ) {
+ $title = Title::newFromText( $page );
+ if( $title ) {
+ $count = Revision::countByTitle( $db, $title );
+ if( $count > $wgExportMaxHistory ) {
+ wfDebug( __FUNCTION__ .
+ ": Skipped $page, $count revisions too big\n" );
+ continue;
+ }
+ }
+ }*/
+
+ #Bug 8824: Only export pages the user can read
+ $title = Title::newFromText( $page );
+ if( is_null( $title ) ) continue; #TODO: perhaps output an <error> tag or something.
+ if( !$title->userCanRead() ) continue; #TODO: perhaps output an <error> tag or something.
+
+ $exporter->pageByTitle( $title );
+ }
+
+ $exporter->closeStream();
+ return;
+ }
+
+ $self = SpecialPage::getTitleFor( 'Export' );
+ $wgOut->addHtml( wfMsgExt( 'exporttext', 'parse' ) );
+
+ $form = Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $self->getLocalUrl( 'action=submit' ) ) );
+
+ $form .= Xml::inputLabel( wfMsg( 'export-addcattext' ) , 'catname', 'catname', 40 ) . '&nbsp;';
+ $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />';
+
+ $form .= Xml::openElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) );
+ $form .= htmlspecialchars( $page );
+ $form .= Xml::closeElement( 'textarea' );
+ $form .= '<br />';
+
+ if( $wgExportAllowHistory ) {
+ $form .= Xml::checkLabel( wfMsg( 'exportcuronly' ), 'curonly', 'curonly', true ) . '<br />';
+ } else {
+ $wgOut->addHtml( wfMsgExt( 'exportnohistory', 'parse' ) );
+ }
+ $form .= Xml::checkLabel( wfMsg( 'export-templates' ), 'templates', 'wpExportTemplates', false ) . '<br />';
+ // Enable this when we can do something useful exporting/importing image information. :)
+ //$form .= Xml::checkLabel( wfMsg( 'export-images' ), 'images', 'wpExportImages', false ) . '<br />';
+ $form .= Xml::checkLabel( wfMsg( 'export-download' ), 'wpDownload', 'wpDownload', true ) . '<br />';
+
+ $form .= Xml::submitButton( wfMsg( 'export-submit' ) );
+ $form .= Xml::closeElement( 'form' );
+ $wgOut->addHtml( $form );
+}
diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php
new file mode 100644
index 00000000..afd5ad48
--- /dev/null
+++ b/includes/specials/SpecialFewestrevisions.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page for listing the articles with the fewest revisions.
+ *
+ * @ingroup SpecialPage
+ * @author Martin Drashkov
+ */
+class FewestrevisionsPage extends QueryPage {
+
+ function getName() {
+ return 'Fewestrevisions';
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSql() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $revision, $page ) = $dbr->tableNamesN( 'revision', 'page' );
+
+ return "SELECT 'Fewestrevisions' as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_is_redirect as redirect,
+ COUNT(*) as value
+ FROM $revision
+ JOIN $page ON page_id = rev_page
+ WHERE page_namespace = " . NS_MAIN . "
+ GROUP BY page_namespace, page_title, page_is_redirect
+ HAVING COUNT(*) > 1";
+ // ^^^ This was probably here to weed out redirects.
+ // Since we mark them as such now, it might be
+ // useful to remove this. People _do_ create pages
+ // and never revise them, they aren't necessarily
+ // redirects.
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitleSafe( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+ $plink = $skin->makeKnownLinkObj( $nt, $text );
+
+ $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ $redirect = $result->redirect ? ' - ' . wfMsg( 'isredirect' ) : '';
+ $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' ) . $redirect;
+
+
+ return wfSpecialList( $plink, $nlink );
+ }
+}
+
+function wfSpecialFewestrevisions() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $frp = new FewestrevisionsPage();
+ $frp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php
new file mode 100644
index 00000000..5236ca25
--- /dev/null
+++ b/includes/specials/SpecialFileDuplicateSearch.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * A special page to search for files by hash value as defined in the
+ * img_sha1 field in the image table
+ *
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Raimond Spekking, based on Special:MIMESearch by Ævar Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Searches the database for files of the requested hash, comparing this with the
+ * 'img_sha1' field in the image table.
+ * @ingroup SpecialPage
+ */
+class FileDuplicateSearchPage extends QueryPage {
+ var $hash, $filename;
+
+ function FileDuplicateSearchPage( $hash, $filename ) {
+ $this->hash = $hash;
+ $this->filename = $filename;
+ }
+
+ function getName() { return 'FileDuplicateSearch'; }
+ function isExpensive() { return false; }
+ function isSyndicated() { return false; }
+
+ function linkParameters() {
+ return array( 'filename' => $this->filename );
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $image = $dbr->tableName( 'image' );
+ $hash = $dbr->addQuotes( $this->hash );
+
+ return "SELECT 'FileDuplicateSearch' AS type,
+ img_name AS title,
+ img_sha1 AS value,
+ img_user_text,
+ img_timestamp
+ FROM $image
+ WHERE img_sha1 = $hash
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang, $wgLang;
+
+ $nt = Title::makeTitle( NS_IMAGE, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+ $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
+
+ $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
+ $time = $wgLang->timeanddate( $result->img_timestamp );
+
+ return "$plink . . $user . . $time";
+ }
+}
+
+/**
+ * Output the HTML search form, and constructs the FileDuplicateSearch object.
+ */
+function wfSpecialFileDuplicateSearch( $par = null ) {
+ global $wgRequest, $wgTitle, $wgOut, $wgLang, $wgContLang;
+
+ $hash = '';
+ $filename = isset( $par ) ? $par : $wgRequest->getText( 'filename' );
+
+ $title = Title::newFromText( $filename );
+ if( $title && $title->getText() != '' ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $image = $dbr->tableName( 'image' );
+ $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDbKey() ) );
+ $sql = "SELECT img_sha1 from $image where img_name = $encFilename";
+ $res = $dbr->query( $sql );
+ $row = $dbr->fetchRow( $res );
+ if( $row !== false ) {
+ $hash = $row[0];
+ }
+ $dbr->freeResult( $res );
+ }
+
+ # Create the input form
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'id' => 'fileduplicatesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'fileduplicatesearch-legend' ) ) .
+ Xml::inputLabel( wfMsg( 'fileduplicatesearch-filename' ), 'filename', 'filename', 50, $filename ) . ' ' .
+ Xml::submitButton( wfMsg( 'fileduplicatesearch-submit' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+
+ if( $hash != '' ) {
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ # Show a thumbnail of the file
+ $img = wfFindFile( $title );
+ if ( $img ) {
+ $thumb = $img->getThumbnail( 120, 120 );
+ if( $thumb ) {
+ $wgOut->addHTML( '<div style="float:' . $align . '" id="mw-fileduplicatesearch-icon">' .
+ $thumb->toHtml( array( 'desc-link' => false ) ) . '<br />' .
+ wfMsgExt( 'fileduplicatesearch-info', array( 'parse' ),
+ $wgLang->formatNum( $img->getWidth() ),
+ $wgLang->formatNum( $img->getHeight() ),
+ $wgLang->formatSize( $img->getSize() ),
+ $img->getMimeType()
+ ) .
+ '</div>' );
+ }
+ }
+
+ # Do the query
+ $wpp = new FileDuplicateSearchPage( $hash, $filename );
+ list( $limit, $offset ) = wfCheckLimits();
+ $count = $wpp->doQuery( $offset, $limit );
+
+ # Show a short summary
+ if( $count == 1 ) {
+ $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-1">' .
+ wfMsgHtml( 'fileduplicatesearch-result-1', $filename ) .
+ '</p>'
+ );
+ } elseif ( $count > 1 ) {
+ $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-n">' .
+ wfMsgExt( 'fileduplicatesearch-result-n', array( 'parseinline' ), $filename, $wgLang->formatNum( $count - 1 ) ) .
+ '</p>'
+ );
+ }
+ }
+}
diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php
new file mode 100644
index 00000000..a2ba3e57
--- /dev/null
+++ b/includes/specials/SpecialFilepath.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialFilepath( $par ) {
+ global $wgRequest, $wgOut;
+
+ $file = isset( $par ) ? $par : $wgRequest->getText( 'file' );
+
+ $title = Title::newFromText( $file, NS_IMAGE );
+
+ if ( ! $title instanceof Title || $title->getNamespace() != NS_IMAGE ) {
+ $cform = new FilepathForm( $title );
+ $cform->execute();
+ } else {
+ $file = wfFindFile( $title );
+ if ( $file && $file->exists() ) {
+ $wgOut->redirect( $file->getURL() );
+ } else {
+ $wgOut->setStatusCode( 404 );
+ $cform = new FilepathForm( $title );
+ $cform->execute();
+ }
+ }
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+class FilepathForm {
+ var $mTitle;
+
+ function FilepathForm( &$title ) {
+ $this->mTitle =& $title;
+ }
+
+ function execute() {
+ global $wgOut, $wgTitle, $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::inputLabel( wfMsg( 'filepath-page' ), 'file', 'file', 25, is_object( $this->mTitle ) ? $this->mTitle->getText() : '' ) . ' ' .
+ Xml::submitButton( wfMsg( 'filepath-submit' ) ) . "\n" .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+ }
+}
diff --git a/includes/specials/SpecialImagelist.php b/includes/specials/SpecialImagelist.php
new file mode 100644
index 00000000..3d449b54
--- /dev/null
+++ b/includes/specials/SpecialImagelist.php
@@ -0,0 +1,161 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialImagelist() {
+ global $wgOut;
+
+ $pager = new ImageListPager;
+
+ $limit = $pager->getForm();
+ $body = $pager->getBody();
+ $nav = $pager->getNavigationBar();
+ $wgOut->addHTML( "$limit<br />\n$body<br />\n$nav" );
+}
+
+/**
+ * @ingroup SpecialPage Pager
+ */
+class ImageListPager extends TablePager {
+ var $mFieldNames = null;
+ var $mQueryConds = array();
+
+ function __construct() {
+ global $wgRequest, $wgMiserMode;
+ if ( $wgRequest->getText( 'sort', 'img_date' ) == 'img_date' ) {
+ $this->mDefaultDirection = true;
+ } else {
+ $this->mDefaultDirection = false;
+ }
+ $search = $wgRequest->getText( 'ilsearch' );
+ if ( $search != '' && !$wgMiserMode ) {
+ $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}%'" );
+ }
+ }
+
+ parent::__construct();
+ }
+
+ function getFieldNames() {
+ if ( !$this->mFieldNames ) {
+ $this->mFieldNames = array(
+ 'img_timestamp' => wfMsg( 'imagelist_date' ),
+ 'img_name' => wfMsg( 'imagelist_name' ),
+ 'img_user_text' => wfMsg( 'imagelist_user' ),
+ 'img_size' => wfMsg( 'imagelist_size' ),
+ 'img_description' => wfMsg( 'imagelist_description' ),
+ );
+ }
+ return $this->mFieldNames;
+ }
+
+ function isFieldSortable( $field ) {
+ static $sortable = array( 'img_timestamp', 'img_name', 'img_size' );
+ return in_array( $field, $sortable );
+ }
+
+ function getQueryInfo() {
+ $fields = $this->getFieldNames();
+ $fields = array_keys( $fields );
+ $fields[] = 'img_user';
+ return array(
+ 'tables' => 'image',
+ 'fields' => $fields,
+ 'conds' => $this->mQueryConds
+ );
+ }
+
+ function getDefaultSort() {
+ return 'img_timestamp';
+ }
+
+ function getStartBody() {
+ # Do a link batch query for user pages
+ if ( $this->mResult->numRows() ) {
+ $lb = new LinkBatch;
+ $this->mResult->seek( 0 );
+ while ( $row = $this->mResult->fetchObject() ) {
+ if ( $row->img_user ) {
+ $lb->add( NS_USER, str_replace( ' ', '_', $row->img_user_text ) );
+ }
+ }
+ $lb->execute();
+ }
+
+ return parent::getStartBody();
+ }
+
+ function formatValue( $field, $value ) {
+ global $wgLang;
+ switch ( $field ) {
+ case 'img_timestamp':
+ return $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_IMAGE, $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 ) );
+ } else {
+ $link = htmlspecialchars( $value );
+ }
+ return $link;
+ case 'img_size':
+ return $this->getSkin()->formatSize( $value );
+ case 'img_description':
+ return $this->getSkin()->commentBlock( $value );
+ }
+ }
+
+ function getForm() {
+ global $wgRequest, $wgMiserMode;
+ $search = $wgRequest->getText( 'ilsearch' );
+
+ $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-imagelist-form' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'imagelist' ) ) .
+ Xml::tags( 'label', null, wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) );
+
+ if ( !$wgMiserMode ) {
+ $s .= "<br />\n" .
+ Xml::inputLabel( wfMsg( 'imagelist_search_for' ), 'ilsearch', 'mw-ilsearch', 20, $search );
+ }
+ $s .= ' ' .
+ Xml::submitButton( wfMsg( 'table_pager_limit_submit' ) ) ."\n" .
+ $this->getHiddenFields( array( 'limit', 'ilsearch' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n";
+ return $s;
+ }
+
+ function getTableClass() {
+ return 'imagelist ' . parent::getTableClass();
+ }
+
+ function getNavClass() {
+ return 'imagelist_nav ' . parent::getNavClass();
+ }
+
+ function getSortHeaderClass() {
+ return 'imagelist_sort ' . parent::getSortHeaderClass();
+ }
+}
diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php
new file mode 100644
index 00000000..4c37f1f9
--- /dev/null
+++ b/includes/specials/SpecialImport.php
@@ -0,0 +1,1154 @@
+<?php
+/**
+ * MediaWiki page data importer
+ * Copyright (C) 2003,2005 Brion Vibber <brion@pobox.com>
+ * 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
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialImport( $page = '' ) {
+ global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources;
+ global $wgImportTargetNamespace;
+
+ $interwiki = false;
+ $namespace = $wgImportTargetNamespace;
+ $frompage = '';
+ $history = true;
+
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') {
+ $isUpload = false;
+ $namespace = $wgRequest->getIntOrNull( 'namespace' );
+
+ switch( $wgRequest->getVal( "source" ) ) {
+ case "upload":
+ $isUpload = true;
+ if( $wgUser->isAllowed( 'importupload' ) ) {
+ $source = ImportStreamSource::newFromUpload( "xmlimport" );
+ } else {
+ return $wgOut->permissionRequired( 'importupload' );
+ }
+ break;
+ case "interwiki":
+ $interwiki = $wgRequest->getVal( 'interwiki' );
+ $history = $wgRequest->getCheck( 'interwikiHistory' );
+ $frompage = $wgRequest->getText( "frompage" );
+ $source = ImportStreamSource::newFromInterwiki(
+ $interwiki,
+ $frompage,
+ $history );
+ break;
+ default:
+ $source = new WikiErrorMsg( "importunknownsource" );
+ }
+
+ if( WikiError::isError( $source ) ) {
+ $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $source->getMessage() ) );
+ } else {
+ $wgOut->addWikiMsg( "importstart" );
+
+ $importer = new WikiImporter( $source );
+ if( !is_null( $namespace ) ) {
+ $importer->setTargetNamespace( $namespace );
+ }
+ $reporter = new ImportReporter( $importer, $isUpload, $interwiki );
+
+ $reporter->open();
+ $result = $importer->doImport();
+ $resultCount = $reporter->close();
+
+ if( WikiError::isError( $result ) ) {
+ # No source or XML parse error
+ $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $result->getMessage() ) );
+ } elseif( WikiError::isError( $resultCount ) ) {
+ # Zero revisions
+ $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $resultCount->getMessage() ) );
+ } else {
+ # Success!
+ $wgOut->addWikiMsg( 'importsuccess' );
+ }
+ $wgOut->addWikiText( '<hr />' );
+ }
+ }
+
+ $action = $wgTitle->getLocalUrl( 'action=submit' );
+
+ if( $wgUser->isAllowed( 'importupload' ) ) {
+ $wgOut->addWikiMsg( "importtext" );
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ).
+ Xml::element( 'legend', null, wfMsg( 'import-upload' ) ) .
+ Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action ) ) .
+ Xml::hidden( 'action', 'submit' ) .
+ Xml::hidden( 'source', 'upload' ) .
+ Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' .
+ Xml::submitButton( wfMsg( 'uploadbtn' ) ) .
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' )
+ );
+ } else {
+ if( empty( $wgImportSources ) ) {
+ $wgOut->addWikiMsg( 'importnosources' );
+ }
+ }
+
+ if( !empty( $wgImportSources ) ) {
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'importinterwiki' ) ) .
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action ) ) .
+ wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) .
+ Xml::hidden( 'action', 'submit' ) .
+ Xml::hidden( 'source', 'interwiki' ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) .
+ "<tr>
+ <td>" .
+ Xml::openElement( 'select', array( 'name' => 'interwiki' ) )
+ );
+ foreach( $wgImportSources as $prefix ) {
+ $selected = ( $interwiki === $prefix ) ? ' selected="selected"' : '';
+ $wgOut->addHTML( Xml::option( $prefix, $prefix, $selected ) );
+ }
+ $wgOut->addHTML(
+ Xml::closeElement( 'select' ) .
+ "</td>
+ <td>" .
+ Xml::input( 'frompage', 50, $frompage ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td>" .
+ Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $history ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td>" .
+ Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) .
+ Xml::namespaceSelector( $namespace, '' ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td>" .
+ Xml::submitButton( wfMsg( 'import-interwiki-submit' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ).
+ Xml::closeElement( 'form' ) .
+ Xml::closeElement( 'fieldset' )
+ );
+ }
+}
+
+/**
+ * Reporting callback
+ * @ingroup SpecialPage
+ */
+class ImportReporter {
+ function __construct( $importer, $upload, $interwiki ) {
+ $importer->setPageOutCallback( array( $this, 'reportPage' ) );
+ $this->mPageCount = 0;
+ $this->mIsUpload = $upload;
+ $this->mInterwiki = $interwiki;
+ }
+
+ function open() {
+ global $wgOut;
+ $wgOut->addHtml( "<ul>\n" );
+ }
+
+ function reportPage( $title, $origTitle, $revisionCount, $successCount ) {
+ global $wgOut, $wgUser, $wgLang, $wgContLang;
+
+ $skin = $wgUser->getSkin();
+
+ $this->mPageCount++;
+
+ $localCount = $wgLang->formatNum( $successCount );
+ $contentCount = $wgContLang->formatNum( $successCount );
+
+ if( $successCount > 0 ) {
+ $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) . " " .
+ wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) .
+ "</li>\n"
+ );
+
+ $log = new LogPage( 'import' );
+ if( $this->mIsUpload ) {
+ $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ),
+ $contentCount );
+ $log->addEntry( 'upload', $title, $detail );
+ } else {
+ $interwiki = '[[:' . $this->mInterwiki . ':' .
+ $origTitle->getPrefixedText() . ']]';
+ $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ),
+ $contentCount, $interwiki );
+ $log->addEntry( 'interwiki', $title, $detail );
+ }
+
+ $comment = $detail; // quick
+ $dbw = wfGetDB( DB_MASTER );
+ $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleId(), $comment, true );
+ $nullRevision->insertOn( $dbw );
+ $article = new Article( $title );
+ # Update page record
+ $article->updateRevisionOn( $dbw, $nullRevision );
+ wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
+ } else {
+ $wgOut->addHtml( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' );
+ }
+ }
+
+ function close() {
+ global $wgOut;
+ if( $this->mPageCount == 0 ) {
+ $wgOut->addHtml( "</ul>\n" );
+ return new WikiErrorMsg( "importnopages" );
+ }
+ $wgOut->addHtml( "</ul>\n" );
+
+ return $this->mPageCount;
+ }
+}
+
+/**
+ *
+ * @ingroup SpecialPage
+ */
+class WikiRevision {
+ var $title = null;
+ var $id = 0;
+ var $timestamp = "20010115000000";
+ var $user = 0;
+ var $user_text = "";
+ var $text = "";
+ var $comment = "";
+ var $minor = false;
+
+ function setTitle( $title ) {
+ if( is_object( $title ) ) {
+ $this->title = $title;
+ } elseif( is_null( $title ) ) {
+ throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." );
+ } else {
+ throw new MWException( "WikiRevision given non-object title in import." );
+ }
+ }
+
+ function setID( $id ) {
+ $this->id = $id;
+ }
+
+ function setTimestamp( $ts ) {
+ # 2003-08-05T18:30:02Z
+ $this->timestamp = wfTimestamp( TS_MW, $ts );
+ }
+
+ function setUsername( $user ) {
+ $this->user_text = $user;
+ }
+
+ function setUserIP( $ip ) {
+ $this->user_text = $ip;
+ }
+
+ function setText( $text ) {
+ $this->text = $text;
+ }
+
+ function setComment( $text ) {
+ $this->comment = $text;
+ }
+
+ function setMinor( $minor ) {
+ $this->minor = (bool)$minor;
+ }
+
+ function setSrc( $src ) {
+ $this->src = $src;
+ }
+
+ function setFilename( $filename ) {
+ $this->filename = $filename;
+ }
+
+ function setSize( $size ) {
+ $this->size = intval( $size );
+ }
+
+ function getTitle() {
+ return $this->title;
+ }
+
+ function getID() {
+ return $this->id;
+ }
+
+ function getTimestamp() {
+ return $this->timestamp;
+ }
+
+ function getUser() {
+ return $this->user_text;
+ }
+
+ function getText() {
+ return $this->text;
+ }
+
+ function getComment() {
+ return $this->comment;
+ }
+
+ function getMinor() {
+ return $this->minor;
+ }
+
+ function getSrc() {
+ return $this->src;
+ }
+
+ function getFilename() {
+ return $this->filename;
+ }
+
+ function getSize() {
+ return $this->size;
+ }
+
+ function importOldRevision() {
+ $dbw = wfGetDB( DB_MASTER );
+
+ # Sneak a single revision into place
+ $user = User::newFromName( $this->getUser() );
+ if( $user ) {
+ $userId = intval( $user->getId() );
+ $userText = $user->getName();
+ } else {
+ $userId = 0;
+ $userText = $this->getUser();
+ }
+
+ // avoid memory leak...?
+ $linkCache = LinkCache::singleton();
+ $linkCache->clear();
+
+ $article = new Article( $this->title );
+ $pageId = $article->getId();
+ if( $pageId == 0 ) {
+ # must create the page...
+ $pageId = $article->insertOn( $dbw );
+ $created = true;
+ } else {
+ $created = false;
+
+ $prior = Revision::loadFromTimestamp( $dbw, $this->title, $this->timestamp );
+ if( !is_null( $prior ) ) {
+ // FIXME: this could fail slightly for multiple matches :P
+ wfDebug( __METHOD__ . ": skipping existing revision for [[" .
+ $this->title->getPrefixedText() . "]], timestamp " .
+ $this->timestamp . "\n" );
+ return false;
+ }
+ }
+
+ # FIXME: Use original rev_id optionally
+ # FIXME: blah blah blah
+
+ #if( $numrows > 0 ) {
+ # return wfMsg( "importhistoryconflict" );
+ #}
+
+ # Insert the row
+ $revision = new Revision( array(
+ 'page' => $pageId,
+ 'text' => $this->getText(),
+ 'comment' => $this->getComment(),
+ 'user' => $userId,
+ 'user_text' => $userText,
+ 'timestamp' => $this->timestamp,
+ 'minor_edit' => $this->minor,
+ ) );
+ $revId = $revision->insertOn( $dbw );
+ $changed = $article->updateIfNewerOn( $dbw, $revision );
+
+ if( $created ) {
+ wfDebug( __METHOD__ . ": running onArticleCreate\n" );
+ Article::onArticleCreate( $this->title );
+
+ wfDebug( __METHOD__ . ": running create updates\n" );
+ $article->createUpdates( $revision );
+
+ } elseif( $changed ) {
+ wfDebug( __METHOD__ . ": running onArticleEdit\n" );
+ Article::onArticleEdit( $this->title );
+
+ wfDebug( __METHOD__ . ": running edit updates\n" );
+ $article->editUpdates(
+ $this->getText(),
+ $this->getComment(),
+ $this->minor,
+ $this->timestamp,
+ $revId );
+ }
+
+ return true;
+ }
+
+ function importUpload() {
+ wfDebug( __METHOD__ . ": STUB\n" );
+
+ /**
+ // from file revert...
+ $source = $this->file->getArchiveVirtualUrl( $this->oldimage );
+ $comment = $wgRequest->getText( 'wpComment' );
+ // TODO: Preserve file properties from database instead of reloading from file
+ $status = $this->file->upload( $source, $comment, $comment );
+ if( $status->isGood() ) {
+ */
+
+ /**
+ // from file upload...
+ $this->mLocalFile = wfLocalFile( $nt );
+ $this->mDestName = $this->mLocalFile->getName();
+ //....
+ $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
+ File::DELETE_SOURCE, $this->mFileProps );
+ if ( !$status->isGood() ) {
+ $resultDetails = array( 'internal' => $status->getWikiText() );
+ */
+
+ // @fixme upload() uses $wgUser, which is wrong here
+ // it may also create a page without our desire, also wrong potentially.
+ // and, it will record a *current* upload, but we might want an archive version here
+
+ $file = wfLocalFile( $this->getTitle() );
+ if( !$file ) {
+ var_dump( $file );
+ wfDebug( "IMPORT: Bad file. :(\n" );
+ return false;
+ }
+
+ $source = $this->downloadSource();
+ if( !$source ) {
+ wfDebug( "IMPORT: Could not fetch remote file. :(\n" );
+ return false;
+ }
+
+ $status = $file->upload( $source,
+ $this->getComment(),
+ $this->getComment(), // Initial page, if none present...
+ File::DELETE_SOURCE,
+ false, // props...
+ $this->getTimestamp() );
+
+ if( $status->isGood() ) {
+ // yay?
+ wfDebug( "IMPORT: is ok?\n" );
+ return true;
+ }
+
+ wfDebug( "IMPORT: is bad? " . $status->getXml() . "\n" );
+ return false;
+
+ }
+
+ function downloadSource() {
+ global $wgEnableUploads;
+ if( !$wgEnableUploads ) {
+ return false;
+ }
+
+ $tempo = tempnam( wfTempDir(), 'download' );
+ $f = fopen( $tempo, 'wb' );
+ if( !$f ) {
+ wfDebug( "IMPORT: couldn't write to temp file $tempo\n" );
+ return false;
+ }
+
+ // @fixme!
+ $src = $this->getSrc();
+ $data = Http::get( $src );
+ if( !$data ) {
+ wfDebug( "IMPORT: couldn't fetch source $src\n" );
+ fclose( $f );
+ unlink( $tempo );
+ return false;
+ }
+
+ fwrite( $f, $data );
+ fclose( $f );
+
+ return $tempo;
+ }
+
+}
+
+/**
+ * implements Special:Import
+ * @ingroup SpecialPage
+ */
+class WikiImporter {
+ var $mDebug = false;
+ var $mSource = null;
+ var $mPageCallback = null;
+ var $mPageOutCallback = null;
+ var $mRevisionCallback = null;
+ var $mUploadCallback = null;
+ var $mTargetNamespace = null;
+ var $lastfield;
+ var $tagStack = array();
+
+ function __construct( $source ) {
+ $this->setRevisionCallback( array( $this, "importRevision" ) );
+ $this->setUploadCallback( array( $this, "importUpload" ) );
+ $this->mSource = $source;
+ }
+
+ function throwXmlError( $err ) {
+ $this->debug( "FAILURE: $err" );
+ wfDebug( "WikiImporter XML error: $err\n" );
+ }
+
+ # --------------
+
+ function doImport() {
+ if( empty( $this->mSource ) ) {
+ return new WikiErrorMsg( "importnotext" );
+ }
+
+ $parser = xml_parser_create( "UTF-8" );
+
+ # case folding violates XML standard, turn it off
+ xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+ xml_set_object( $parser, $this );
+ xml_set_element_handler( $parser, "in_start", "" );
+
+ $offset = 0; // for context extraction on error reporting
+ do {
+ $chunk = $this->mSource->readChunk();
+ if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) {
+ wfDebug( "WikiImporter::doImport encountered XML parsing error\n" );
+ return new WikiXmlError( $parser, wfMsgHtml( 'import-parse-failure' ), $chunk, $offset );
+ }
+ $offset += strlen( $chunk );
+ } while( $chunk !== false && !$this->mSource->atEnd() );
+ xml_parser_free( $parser );
+
+ return true;
+ }
+
+ function debug( $data ) {
+ if( $this->mDebug ) {
+ wfDebug( "IMPORT: $data\n" );
+ }
+ }
+
+ function notice( $data ) {
+ global $wgCommandLineMode;
+ if( $wgCommandLineMode ) {
+ print "$data\n";
+ } else {
+ global $wgOut;
+ $wgOut->addHTML( "<li>" . htmlspecialchars( $data ) . "</li>\n" );
+ }
+ }
+
+ /**
+ * Set debug mode...
+ */
+ function setDebug( $debug ) {
+ $this->mDebug = $debug;
+ }
+
+ /**
+ * Sets the action to perform as each new page in the stream is reached.
+ * @param $callback callback
+ * @return callback
+ */
+ function setPageCallback( $callback ) {
+ $previous = $this->mPageCallback;
+ $this->mPageCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Sets the action to perform as each page in the stream is completed.
+ * Callback accepts the page title (as a Title object), a second object
+ * with the original title form (in case it's been overridden into a
+ * local namespace), and a count of revisions.
+ *
+ * @param $callback callback
+ * @return callback
+ */
+ function setPageOutCallback( $callback ) {
+ $previous = $this->mPageOutCallback;
+ $this->mPageOutCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Sets the action to perform as each page revision is reached.
+ * @param $callback callback
+ * @return callback
+ */
+ function setRevisionCallback( $callback ) {
+ $previous = $this->mRevisionCallback;
+ $this->mRevisionCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Sets the action to perform as each file upload version is reached.
+ * @param $callback callback
+ * @return callback
+ */
+ function setUploadCallback( $callback ) {
+ $previous = $this->mUploadCallback;
+ $this->mUploadCallback = $callback;
+ return $previous;
+ }
+
+ /**
+ * Set a target namespace to override the defaults
+ */
+ function setTargetNamespace( $namespace ) {
+ if( is_null( $namespace ) ) {
+ // Don't override namespaces
+ $this->mTargetNamespace = null;
+ } elseif( $namespace >= 0 ) {
+ // FIXME: Check for validity
+ $this->mTargetNamespace = intval( $namespace );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Default per-revision callback, performs the import.
+ * @param $revision WikiRevision
+ * @private
+ */
+ function importRevision( $revision ) {
+ $dbw = wfGetDB( DB_MASTER );
+ return $dbw->deadlockLoop( array( $revision, 'importOldRevision' ) );
+ }
+
+ /**
+ * Dummy for now...
+ */
+ function importUpload( $revision ) {
+ //$dbw = wfGetDB( DB_MASTER );
+ //return $dbw->deadlockLoop( array( $revision, 'importUpload' ) );
+ return false;
+ }
+
+ /**
+ * Alternate per-revision callback, for debugging.
+ * @param $revision WikiRevision
+ * @private
+ */
+ function debugRevisionHandler( &$revision ) {
+ $this->debug( "Got revision:" );
+ if( is_object( $revision->title ) ) {
+ $this->debug( "-- Title: " . $revision->title->getPrefixedText() );
+ } else {
+ $this->debug( "-- Title: <invalid>" );
+ }
+ $this->debug( "-- User: " . $revision->user_text );
+ $this->debug( "-- Timestamp: " . $revision->timestamp );
+ $this->debug( "-- Comment: " . $revision->comment );
+ $this->debug( "-- Text: " . $revision->text );
+ }
+
+ /**
+ * Notify the callback function when a new <page> is reached.
+ * @param $title Title
+ * @private
+ */
+ function pageCallback( $title ) {
+ if( is_callable( $this->mPageCallback ) ) {
+ call_user_func( $this->mPageCallback, $title );
+ }
+ }
+
+ /**
+ * Notify the callback function when a </page> is closed.
+ * @param $title Title
+ * @param $origTitle Title
+ * @param $revisionCount int
+ * @param $successCount Int: number of revisions for which callback returned true
+ * @private
+ */
+ function pageOutCallback( $title, $origTitle, $revisionCount, $successCount ) {
+ if( is_callable( $this->mPageOutCallback ) ) {
+ call_user_func( $this->mPageOutCallback, $title, $origTitle,
+ $revisionCount, $successCount );
+ }
+ }
+
+
+ # XML parser callbacks from here out -- beware!
+ function donothing( $parser, $x, $y="" ) {
+ #$this->debug( "donothing" );
+ }
+
+ function in_start( $parser, $name, $attribs ) {
+ $this->debug( "in_start $name" );
+ if( $name != "mediawiki" ) {
+ return $this->throwXMLerror( "Expected <mediawiki>, got <$name>" );
+ }
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+ }
+
+ function in_mediawiki( $parser, $name, $attribs ) {
+ $this->debug( "in_mediawiki $name" );
+ if( $name == 'siteinfo' ) {
+ xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" );
+ } elseif( $name == 'page' ) {
+ $this->push( $name );
+ $this->workRevisionCount = 0;
+ $this->workSuccessCount = 0;
+ $this->uploadCount = 0;
+ $this->uploadSuccessCount = 0;
+ xml_set_element_handler( $parser, "in_page", "out_page" );
+ } else {
+ return $this->throwXMLerror( "Expected <page>, got <$name>" );
+ }
+ }
+ function out_mediawiki( $parser, $name ) {
+ $this->debug( "out_mediawiki $name" );
+ if( $name != "mediawiki" ) {
+ return $this->throwXMLerror( "Expected </mediawiki>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "donothing", "donothing" );
+ }
+
+
+ function in_siteinfo( $parser, $name, $attribs ) {
+ // no-ops for now
+ $this->debug( "in_siteinfo $name" );
+ switch( $name ) {
+ case "sitename":
+ case "base":
+ case "generator":
+ case "case":
+ case "namespaces":
+ case "namespace":
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in <siteinfo>." );
+ }
+ }
+
+ function out_siteinfo( $parser, $name ) {
+ if( $name == "siteinfo" ) {
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+ }
+ }
+
+
+ function in_page( $parser, $name, $attribs ) {
+ $this->debug( "in_page $name" );
+ switch( $name ) {
+ case "id":
+ case "title":
+ case "restrictions":
+ $this->appendfield = $name;
+ $this->appenddata = "";
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ case "revision":
+ $this->push( "revision" );
+ if( is_object( $this->pageTitle ) ) {
+ $this->workRevision = new WikiRevision;
+ $this->workRevision->setTitle( $this->pageTitle );
+ $this->workRevisionCount++;
+ } else {
+ // Skipping items due to invalid page title
+ $this->workRevision = null;
+ }
+ xml_set_element_handler( $parser, "in_revision", "out_revision" );
+ break;
+ case "upload":
+ $this->push( "upload" );
+ if( is_object( $this->pageTitle ) ) {
+ $this->workRevision = new WikiRevision;
+ $this->workRevision->setTitle( $this->pageTitle );
+ $this->uploadCount++;
+ } else {
+ // Skipping items due to invalid page title
+ $this->workRevision = null;
+ }
+ xml_set_element_handler( $parser, "in_upload", "out_upload" );
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in a <page>." );
+ }
+ }
+
+ function out_page( $parser, $name ) {
+ $this->debug( "out_page $name" );
+ $this->pop();
+ if( $name != "page" ) {
+ return $this->throwXMLerror( "Expected </page>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+
+ $this->pageOutCallback( $this->pageTitle, $this->origTitle,
+ $this->workRevisionCount, $this->workSuccessCount );
+
+ $this->workTitle = null;
+ $this->workRevision = null;
+ $this->workRevisionCount = 0;
+ $this->workSuccessCount = 0;
+ $this->pageTitle = null;
+ $this->origTitle = null;
+ }
+
+ function in_nothing( $parser, $name, $attribs ) {
+ $this->debug( "in_nothing $name" );
+ return $this->throwXMLerror( "No child elements allowed here; got <$name>" );
+ }
+ function char_append( $parser, $data ) {
+ $this->debug( "char_append '$data'" );
+ $this->appenddata .= $data;
+ }
+ function out_append( $parser, $name ) {
+ $this->debug( "out_append $name" );
+ if( $name != $this->appendfield ) {
+ return $this->throwXMLerror( "Expected </{$this->appendfield}>, got </$name>" );
+ }
+
+ switch( $this->appendfield ) {
+ case "title":
+ $this->workTitle = $this->appenddata;
+ $this->origTitle = Title::newFromText( $this->workTitle );
+ if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) {
+ $this->pageTitle = Title::makeTitle( $this->mTargetNamespace,
+ $this->origTitle->getDBkey() );
+ } else {
+ $this->pageTitle = Title::newFromText( $this->workTitle );
+ }
+ if( is_null( $this->pageTitle ) ) {
+ // Invalid page title? Ignore the page
+ $this->notice( "Skipping invalid page title '$this->workTitle'" );
+ } else {
+ $this->pageCallback( $this->workTitle );
+ }
+ break;
+ case "id":
+ if ( $this->parentTag() == 'revision' ) {
+ if( $this->workRevision )
+ $this->workRevision->setID( $this->appenddata );
+ }
+ break;
+ case "text":
+ if( $this->workRevision )
+ $this->workRevision->setText( $this->appenddata );
+ break;
+ case "username":
+ if( $this->workRevision )
+ $this->workRevision->setUsername( $this->appenddata );
+ break;
+ case "ip":
+ if( $this->workRevision )
+ $this->workRevision->setUserIP( $this->appenddata );
+ break;
+ case "timestamp":
+ if( $this->workRevision )
+ $this->workRevision->setTimestamp( $this->appenddata );
+ break;
+ case "comment":
+ if( $this->workRevision )
+ $this->workRevision->setComment( $this->appenddata );
+ break;
+ case "minor":
+ if( $this->workRevision )
+ $this->workRevision->setMinor( true );
+ break;
+ case "filename":
+ if( $this->workRevision )
+ $this->workRevision->setFilename( $this->appenddata );
+ break;
+ case "src":
+ if( $this->workRevision )
+ $this->workRevision->setSrc( $this->appenddata );
+ break;
+ case "size":
+ if( $this->workRevision )
+ $this->workRevision->setSize( intval( $this->appenddata ) );
+ break;
+ default:
+ $this->debug( "Bad append: {$this->appendfield}" );
+ }
+ $this->appendfield = "";
+ $this->appenddata = "";
+
+ $parent = $this->parentTag();
+ xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
+ xml_set_character_data_handler( $parser, "donothing" );
+ }
+
+ function in_revision( $parser, $name, $attribs ) {
+ $this->debug( "in_revision $name" );
+ switch( $name ) {
+ case "id":
+ case "timestamp":
+ case "comment":
+ case "minor":
+ case "text":
+ $this->appendfield = $name;
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ case "contributor":
+ $this->push( "contributor" );
+ xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." );
+ }
+ }
+
+ function out_revision( $parser, $name ) {
+ $this->debug( "out_revision $name" );
+ $this->pop();
+ if( $name != "revision" ) {
+ return $this->throwXMLerror( "Expected </revision>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_page", "out_page" );
+
+ if( $this->workRevision ) {
+ $ok = call_user_func_array( $this->mRevisionCallback,
+ array( $this->workRevision, $this ) );
+ if( $ok ) {
+ $this->workSuccessCount++;
+ }
+ }
+ }
+
+ function in_upload( $parser, $name, $attribs ) {
+ $this->debug( "in_upload $name" );
+ switch( $name ) {
+ case "timestamp":
+ case "comment":
+ case "text":
+ case "filename":
+ case "src":
+ case "size":
+ $this->appendfield = $name;
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ case "contributor":
+ $this->push( "contributor" );
+ xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
+ break;
+ default:
+ return $this->throwXMLerror( "Element <$name> not allowed in an <upload>." );
+ }
+ }
+
+ function out_upload( $parser, $name ) {
+ $this->debug( "out_revision $name" );
+ $this->pop();
+ if( $name != "upload" ) {
+ return $this->throwXMLerror( "Expected </upload>, got </$name>" );
+ }
+ xml_set_element_handler( $parser, "in_page", "out_page" );
+
+ if( $this->workRevision ) {
+ $ok = call_user_func_array( $this->mUploadCallback,
+ array( $this->workRevision, $this ) );
+ if( $ok ) {
+ $this->workUploadSuccessCount++;
+ }
+ }
+ }
+
+ function in_contributor( $parser, $name, $attribs ) {
+ $this->debug( "in_contributor $name" );
+ switch( $name ) {
+ case "username":
+ case "ip":
+ case "id":
+ $this->appendfield = $name;
+ xml_set_element_handler( $parser, "in_nothing", "out_append" );
+ xml_set_character_data_handler( $parser, "char_append" );
+ break;
+ default:
+ $this->throwXMLerror( "Invalid tag <$name> in <contributor>" );
+ }
+ }
+
+ function out_contributor( $parser, $name ) {
+ $this->debug( "out_contributor $name" );
+ $this->pop();
+ if( $name != "contributor" ) {
+ return $this->throwXMLerror( "Expected </contributor>, got </$name>" );
+ }
+ $parent = $this->parentTag();
+ xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
+ }
+
+ private function push( $name ) {
+ array_push( $this->tagStack, $name );
+ $this->debug( "PUSH $name" );
+ }
+
+ private function pop() {
+ $name = array_pop( $this->tagStack );
+ $this->debug( "POP $name" );
+ return $name;
+ }
+
+ private function parentTag() {
+ $name = $this->tagStack[count( $this->tagStack ) - 1];
+ $this->debug( "PARENT $name" );
+ return $name;
+ }
+
+}
+
+/**
+ * @todo document (e.g. one-sentence class description).
+ * @ingroup SpecialPage
+ */
+class ImportStringSource {
+ function __construct( $string ) {
+ $this->mString = $string;
+ $this->mRead = false;
+ }
+
+ function atEnd() {
+ return $this->mRead;
+ }
+
+ function readChunk() {
+ if( $this->atEnd() ) {
+ return false;
+ } else {
+ $this->mRead = true;
+ return $this->mString;
+ }
+ }
+}
+
+/**
+ * @todo document (e.g. one-sentence class description).
+ * @ingroup SpecialPage
+ */
+class ImportStreamSource {
+ function __construct( $handle ) {
+ $this->mHandle = $handle;
+ }
+
+ function atEnd() {
+ return feof( $this->mHandle );
+ }
+
+ function readChunk() {
+ return fread( $this->mHandle, 32768 );
+ }
+
+ static function newFromFile( $filename ) {
+ $file = @fopen( $filename, 'rt' );
+ if( !$file ) {
+ return new WikiErrorMsg( "importcantopen" );
+ }
+ return new ImportStreamSource( $file );
+ }
+
+ static function newFromUpload( $fieldname = "xmlimport" ) {
+ $upload =& $_FILES[$fieldname];
+
+ if( !isset( $upload ) || !$upload['name'] ) {
+ return new WikiErrorMsg( 'importnofile' );
+ }
+ if( !empty( $upload['error'] ) ) {
+ switch($upload['error']){
+ case 1: # The uploaded file exceeds the upload_max_filesize directive in php.ini.
+ return new WikiErrorMsg( 'importuploaderrorsize' );
+ case 2: # The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
+ return new WikiErrorMsg( 'importuploaderrorsize' );
+ case 3: # The uploaded file was only partially uploaded
+ return new WikiErrorMsg( 'importuploaderrorpartial' );
+ case 6: #Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.
+ return new WikiErrorMsg( 'importuploaderrortemp' );
+ # case else: # Currently impossible
+ }
+
+ }
+ $fname = $upload['tmp_name'];
+ if( is_uploaded_file( $fname ) ) {
+ return ImportStreamSource::newFromFile( $fname );
+ } else {
+ return new WikiErrorMsg( 'importnofile' );
+ }
+ }
+
+ static function newFromURL( $url, $method = 'GET' ) {
+ wfDebug( __METHOD__ . ": opening $url\n" );
+ # Use the standard HTTP fetch function; it times out
+ # quicker and sorts out user-agent problems which might
+ # otherwise prevent importing from large sites, such
+ # as the Wikimedia cluster, etc.
+ $data = Http::request( $method, $url );
+ if( $data !== false ) {
+ $file = tmpfile();
+ fwrite( $file, $data );
+ fflush( $file );
+ fseek( $file, 0 );
+ return new ImportStreamSource( $file );
+ } else {
+ return new WikiErrorMsg( 'importcantopen' );
+ }
+ }
+
+ public static function newFromInterwiki( $interwiki, $page, $history=false ) {
+ if( $page == '' ) {
+ return new WikiErrorMsg( 'import-noarticle' );
+ }
+ $link = Title::newFromText( "$interwiki:Special:Export/$page" );
+ if( is_null( $link ) || $link->getInterwiki() == '' ) {
+ return new WikiErrorMsg( 'importbadinterwiki' );
+ } else {
+ $params = $history ? 'history=1' : '';
+ $url = $link->getFullUrl( $params );
+ # For interwikis, use POST to avoid redirects.
+ return ImportStreamSource::newFromURL( $url, "POST" );
+ }
+ }
+}
diff --git a/includes/specials/SpecialIpblocklist.php b/includes/specials/SpecialIpblocklist.php
new file mode 100644
index 00000000..696c7efe
--- /dev/null
+++ b/includes/specials/SpecialIpblocklist.php
@@ -0,0 +1,427 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ */
+function wfSpecialIpblocklist() {
+ global $wgUser, $wgOut, $wgRequest;
+
+ $ip = $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) );
+ $id = $wgRequest->getVal( 'id' );
+ $reason = $wgRequest->getText( 'wpUnblockReason' );
+ $action = $wgRequest->getText( 'action' );
+ $successip = $wgRequest->getVal( 'successip' );
+
+ $ipu = new IPUnblockForm( $ip, $id, $reason );
+
+ if( $action == 'unblock' ) {
+ # Check permissions
+ if( !$wgUser->isAllowed( 'block' ) ) {
+ $wgOut->permissionRequired( 'block' );
+ return;
+ }
+ # Check for database lock
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ # Show unblock form
+ $ipu->showForm( '' );
+ } elseif( $action == 'submit' && $wgRequest->wasPosted()
+ && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ # Check permissions
+ if( !$wgUser->isAllowed( 'block' ) ) {
+ $wgOut->permissionRequired( 'block' );
+ return;
+ }
+ # Check for database lock
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ # Remove blocks and redirect user to success page
+ $ipu->doSubmit();
+ } elseif( $action == 'success' ) {
+ # Inform the user of a successful unblock
+ # (No need to check permissions or locks here,
+ # if something was done, then it's too late!)
+ if ( substr( $successip, 0, 1) == '#' ) {
+ // A block ID was unblocked
+ $ipu->showList( $wgOut->parse( wfMsg( 'unblocked-id', $successip ) ) );
+ } else {
+ // A username/IP was unblocked
+ $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) );
+ }
+ } else {
+ # Just show the block list
+ $ipu->showList( '' );
+ }
+
+}
+
+/**
+ * implements Special:ipblocklist GUI
+ * @ingroup SpecialPage
+ */
+class IPUnblockForm {
+ var $ip, $reason, $id;
+
+ function IPUnblockForm( $ip, $id, $reason ) {
+ $this->ip = strtr( $ip, '_', ' ' );
+ $this->id = $id;
+ $this->reason = $reason;
+ }
+
+ /**
+ * Generates the unblock form
+ * @param $err string: error message
+ * @return $out string: HTML form
+ */
+ function showForm( $err ) {
+ global $wgOut, $wgUser, $wgSysopUserBans;
+
+ $wgOut->setPagetitle( wfMsg( 'unblockip' ) );
+ $wgOut->addWikiMsg( 'unblockiptext' );
+
+ $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
+ $action = $titleObj->getLocalURL( "action=submit" );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsg( "formerror" ) );
+ $wgOut->addWikiText( Xml::tags( 'span', array( 'class' => 'error' ), $err ) . "\n" );
+ }
+
+ $addressPart = false;
+ if ( $this->id ) {
+ $block = Block::newFromID( $this->id );
+ if ( $block ) {
+ $encName = htmlspecialchars( $block->getRedactedName() );
+ $encId = $this->id;
+ $addressPart = $encName . Xml::hidden( 'id', $encId );
+ $ipa = wfMsgHtml( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' );
+ }
+ }
+ if ( !$addressPart ) {
+ $addressPart = Xml::input( 'wpUnblockAddress', 40, $this->ip, array( 'type' => 'text', 'tabindex' => '1' ) );
+ $ipa = Xml::label( wfMsg( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' ), 'wpUnblockAddress' );
+ }
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'unblockip' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'ipb-unblock' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-unblock-table' ) ).
+ "<tr>
+ <td class='mw-label'>
+ {$ipa}
+ </td>
+ <td class='mw-input'>
+ {$addressPart}
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'ipbreason' ), 'wpUnblockReason' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpUnblockReason', 40, $this->reason, array( 'type' => 'text', 'tabindex' => '2' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'ipusubmit' ), array( 'name' => 'wpBlock', 'tabindex' => '3' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+
+ }
+
+ const UNBLOCK_SUCCESS = 0; // Success
+ const UNBLOCK_NO_SUCH_ID = 1; // No such block ID
+ const UNBLOCK_USER_NOT_BLOCKED = 2; // IP wasn't blocked
+ const UNBLOCK_BLOCKED_AS_RANGE = 3; // IP is part of a range block
+ const UNBLOCK_UNKNOWNERR = 4; // Unknown error
+
+ /**
+ * Backend code for unblocking. doSubmit() wraps around this.
+ * $range is only used when UNBLOCK_BLOCKED_AS_RANGE is returned, in which
+ * case it contains the range $ip is part of.
+ * @return array array(message key, parameters) on failure, empty array on success
+ */
+
+ static function doUnblock(&$id, &$ip, &$reason, &$range = null)
+ {
+ if ( $id ) {
+ $block = Block::newFromID( $id );
+ if ( !$block ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+ $ip = $block->getRedactedName();
+ } else {
+ $block = new Block();
+ $ip = trim( $ip );
+ if ( substr( $ip, 0, 1 ) == "#" ) {
+ $id = substr( $ip, 1 );
+ $block = Block::newFromID( $id );
+ if( !$block ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+ $ip = $block->getRedactedName();
+ } else {
+ $block = Block::newFromDB( $ip );
+ if ( !$block ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+ 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;
+ return array('ipb_blocked_as_range', $ip, $range);
+ }
+ }
+ }
+ // Yes, this is really necessary
+ $id = $block->mId;
+
+ # Delete block
+ if ( !$block->delete() ) {
+ return array('ipb_cant_unblock', htmlspecialchars($id));
+ }
+
+ # Make log entry
+ $log = new LogPage( 'block' );
+ $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $ip ), $reason );
+ return array();
+ }
+
+ function doSubmit() {
+ global $wgOut;
+ $retval = self::doUnblock($this->id, $this->ip, $this->reason, $range);
+ if(!empty($retval))
+ {
+ $key = array_shift($retval);
+ $this->showForm(wfMsgReal($key, $retval));
+ return;
+ }
+ # Report to the user
+ $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
+ $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) );
+ $wgOut->redirect( $success );
+ }
+
+ function showList( $msg ) {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsg( "ipblocklist" ) );
+ if ( "" != $msg ) {
+ $wgOut->setSubtitle( $msg );
+ }
+
+ // Purge expired entries on one in every 10 queries
+ if ( !mt_rand( 0, 10 ) ) {
+ Block::purgeExpired();
+ }
+
+ $conds = array();
+ $matches = array();
+ // Is user allowed to see all the blocks?
+ if ( !$wgUser->isAllowed( 'suppress' ) )
+ $conds['ipb_deleted'] = 0;
+ if ( $this->ip == '' ) {
+ // No extra conditions
+ } elseif ( substr( $this->ip, 0, 1 ) == '#' ) {
+ $conds['ipb_id'] = substr( $this->ip, 1 );
+ } elseif ( IP::toUnsigned( $this->ip ) !== false ) {
+ $conds['ipb_address'] = $this->ip;
+ $conds['ipb_auto'] = 0;
+ } elseif( preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/', $this->ip, $matches ) ) {
+ $conds['ipb_address'] = Block::normaliseRange( $this->ip );
+ $conds['ipb_auto'] = 0;
+ } else {
+ $user = User::newFromName( $this->ip );
+ if ( $user && ( $id = $user->getId() ) != 0 ) {
+ $conds['ipb_user'] = $id;
+ } else {
+ // Uh...?
+ $conds['ipb_address'] = $this->ip;
+ $conds['ipb_auto'] = 0;
+ }
+ }
+
+ $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->addWikiMsg( 'ipblocklist-empty' );
+ }
+ }
+
+ function searchForm() {
+ global $wgTitle, $wgScript, $wgRequest;
+ return
+ Xml::tags( 'form', array( 'action' => $wgScript ),
+ Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) .
+ Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) .
+ '&nbsp;' .
+ Xml::submitButton( wfMsg( 'ipblocklist-submit' ) ) .
+ Xml::closeElement( 'fieldset' )
+ );
+ }
+
+ /**
+ * Callback function to output a block
+ */
+ function formatRow( $block ) {
+ global $wgUser, $wgLang;
+
+ wfProfileIn( __METHOD__ );
+
+ static $sk=null, $msg=null;
+
+ if( is_null( $sk ) )
+ $sk = $wgUser->getSkin();
+ if( is_null( $msg ) ) {
+ $msg = array();
+ $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink',
+ 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' );
+ foreach( $keys as $key ) {
+ $msg[$key] = wfMsgHtml( $key );
+ }
+ $msg['blocklistline'] = wfMsg( 'blocklistline' );
+ }
+
+ # Prepare links to the blocker's user and talk pages
+ $blocker_id = $block->getBy();
+ $blocker_name = $block->getByName();
+ $blocker = $sk->userLink( $blocker_id, $blocker_name );
+ $blocker .= $sk->userToolLinks( $blocker_id, $blocker_name );
+
+ # Prepare links to the block target's user and contribs. pages (as applicable, don't do it for autoblocks)
+ if( $block->mAuto ) {
+ $target = $block->getRedactedName(); # Hide the IP addresses of auto-blocks; privacy
+ } else {
+ $target = $sk->userLink( $block->mUser, $block->mAddress )
+ . $sk->userToolLinks( $block->mUser, $block->mAddress, false, Linker::TOOL_LINKS_NOBLOCK );
+ }
+
+ $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true );
+
+ $properties = array();
+ $properties[] = Block::formatExpiry( $block->mExpiry );
+ if ( $block->mAnonOnly ) {
+ $properties[] = $msg['anononlyblock'];
+ }
+ if ( $block->mCreateAccount ) {
+ $properties[] = $msg['createaccountblock'];
+ }
+ if (!$block->mEnableAutoblock && $block->mUser ) {
+ $properties[] = $msg['noautoblockblock'];
+ }
+
+ if ( $block->mBlockEmail && $block->mUser ) {
+ $properties[] = $msg['emailblock'];
+ }
+
+ $properties = implode( ', ', $properties );
+
+ $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) );
+
+ $unblocklink = '';
+ if ( $wgUser->isAllowed('block') ) {
+ $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
+ $unblocklink = ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')';
+ }
+
+ $comment = $sk->commentBlock( $block->mReason );
+
+ $s = "{$line} $comment";
+ if ( $block->mHideName )
+ $s = '<span class="history-deleted">' . $s . '</span>';
+
+ wfProfileOut( __METHOD__ );
+ return "<li>$s $unblocklink</li>\n";
+ }
+}
+
+/**
+ * @todo document
+ * @ingroup Pager
+ */
+class IPBlocklistPager extends ReverseChronologicalPager {
+ public $mForm, $mConds;
+
+ function __construct( $form, $conds = array() ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $this->mResult->seek( 0 );
+ $lb = new LinkBatch;
+
+ /*
+ while ( $row = $this->mResult->fetchObject() ) {
+ $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
+ $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
+ $lb->addObj( Title::makeTitleSafe( NS_USER, $row->ipb_address ) );
+ $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ipb_address ) );
+ }*/
+ # 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.
+ while ( $row = $this->mResult->fetchObject() ) {
+ $name = str_replace( ' ', '_', $row->ipb_by_text );
+ $lb->add( NS_USER, $name );
+ $lb->add( NS_USER_TALK, $name );
+ $name = str_replace( ' ', '_', $row->ipb_address );
+ $lb->add( NS_USER, $name );
+ $lb->add( NS_USER_TALK, $name );
+ }
+ $lb->execute();
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ $block = new Block;
+ $block->initFromRow( $row );
+ return $this->mForm->formatRow( $block );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds[] = 'ipb_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+ return array(
+ 'tables' => 'ipblocks',
+ 'fields' => '*',
+ 'conds' => $conds,
+ );
+ }
+
+ function getIndexField() {
+ return 'ipb_timestamp';
+ }
+}
diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php
new file mode 100644
index 00000000..131c0606
--- /dev/null
+++ b/includes/specials/SpecialListgrouprights.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * This special page lists all defined user groups and the associated rights.
+ * See also @ref $wgGroupPermissions.
+ *
+ * @ingroup SpecialPage
+ * @author Petr Kadlec <mormegil@centrum.cz>
+ */
+class SpecialListGroupRights extends SpecialPage {
+
+ var $skin;
+
+ /**
+ * Constructor
+ */
+ function __construct() {
+ global $wgUser;
+ parent::__construct( 'Listgrouprights' );
+ $this->skin = $wgUser->getSkin();
+ }
+
+ /**
+ * Show the special page
+ */
+ public function execute( $par ) {
+ global $wgOut, $wgGroupPermissions, $wgImplicitGroups, $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
+
+ $this->setHeaders();
+ $this->outputHeader();
+
+ $wgOut->addHTML(
+ Xml::openElement( 'table', array( 'class' => 'mw-listgrouprights-table' ) ) .
+ '<tr>' .
+ Xml::element( 'th', null, wfMsg( 'listgrouprights-group' ) ) .
+ Xml::element( 'th', null, wfMsg( 'listgrouprights-rights' ) ) .
+ '</tr>'
+ );
+
+ foreach( $wgGroupPermissions as $group => $permissions ) {
+ $groupname = ( $group == '*' ) ? 'all' : htmlspecialchars( $group ); // Replace * with a more descriptive groupname
+
+ $msg = wfMsg( 'group-' . $groupname );
+ if ( wfEmptyMsg( 'group-' . $groupname, $msg ) || $msg == '' ) {
+ $groupnameLocalized = $groupname;
+ } else {
+ $groupnameLocalized = $msg;
+ }
+
+ $msg = wfMsgForContent( 'grouppage-' . $groupname );
+ if ( wfEmptyMsg( 'grouppage-' . $groupname, $msg ) || $msg == '' ) {
+ $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname;
+ } else {
+ $grouppageLocalized = $msg;
+ }
+
+ if( $group == '*' ) {
+ // Do not make a link for the generic * group
+ $grouppage = $groupnameLocalized;
+ } else {
+ $grouppage = $this->skin->makeLink( $grouppageLocalized, $groupnameLocalized );
+ }
+
+ if ( !in_array( $group, $wgImplicitGroups ) ) {
+ $grouplink = '<br />' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), 'group=' . $group );
+ } else {
+ // No link to Special:listusers for implicit groups as they are unlistable
+ $grouplink = '';
+ }
+
+ $wgOut->addHTML(
+ '<tr>
+ <td>' .
+ $grouppage . $grouplink .
+ '</td>
+ <td>' .
+ self::formatPermissions( $permissions ) .
+ '</td>
+ </tr>'
+ );
+ }
+ $wgOut->addHTML(
+ Xml::closeElement( 'table' ) . "\n"
+ );
+ }
+
+ /**
+ * Create a user-readable list of permissions from the given array.
+ *
+ * @param $permissions Array of permission => bool (from $wgGroupPermissions items)
+ * @return string List of all granted permissions, separated by comma separator
+ */
+ private static function formatPermissions( $permissions ) {
+ $r = array();
+ foreach( $permissions as $permission => $granted ) {
+ if ( $granted ) {
+ $description = wfMsgHTML( 'listgrouprights-right-display',
+ User::getRightDescription($permission),
+ $permission
+ );
+ $r[] = $description;
+ }
+ }
+ sort( $r );
+ if( empty( $r ) ) {
+ return '';
+ } else {
+ return '<ul><li>' . implode( "</li>\n<li>", $r ) . '</li></ul>';
+ }
+ }
+}
diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php
new file mode 100644
index 00000000..808aab14
--- /dev/null
+++ b/includes/specials/SpecialListredirects.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright © 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Special:Listredirects - Lists all the redirects on the wiki.
+ * @ingroup SpecialPage
+ */
+class ListredirectsPage extends QueryPage {
+
+ function getName() { return( 'Listredirects' ); }
+ function isExpensive() { return( true ); }
+ function isSyndicated() { return( false ); }
+ function sortDescending() { return( false ); }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $sql = "SELECT 'Listredirects' AS type, page_title AS title, page_namespace AS namespace, 0 AS value FROM $page WHERE page_is_redirect = 1";
+ return( $sql );
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ # Make a link to the redirect itself
+ $rd_title = Title::makeTitle( $result->namespace, $result->title );
+ $rd_link = $skin->makeLinkObj( $rd_title, '', 'redirect=no' );
+
+ # Find out where the redirect leads
+ $revision = Revision::newFromTitle( $rd_title );
+ if( $revision ) {
+ # Make a link to the destination page
+ $target = Title::newFromRedirect( $revision->getText() );
+ if( $target ) {
+ $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
+ $targetLink = $skin->makeLinkObj( $target );
+ return "$rd_link $arr $targetLink";
+ } else {
+ return "<s>$rd_link</s>";
+ }
+ } else {
+ return "<s>$rd_link</s>";
+ }
+ }
+}
+
+function wfSpecialListredirects() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $lrp = new ListredirectsPage();
+ $lrp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php
new file mode 100644
index 00000000..7dba44e2
--- /dev/null
+++ b/includes/specials/SpecialListusers.php
@@ -0,0 +1,235 @@
+<?php
+
+# Copyright (C) 2004 Brion Vibber, lcrocker, Tim Starling,
+# Domas Mituzas, Ashar Voultoiz, Jens Frank, Zhengzhu.
+#
+# © 2006 Rob Church <robchur@gmail.com>
+#
+# 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
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * This class is used to get a list of user. The ones with specials
+ * rights (sysop, bureaucrat, developer) will have them displayed
+ * next to their names.
+ *
+ * @ingroup SpecialPage
+ */
+class UsersPager extends AlphabeticPager {
+
+ function __construct($group=null) {
+ global $wgRequest;
+ $this->requestedGroup = $group != "" ? $group : $wgRequest->getVal( 'group' );
+ $un = $wgRequest->getText( 'username' );
+ $this->requestedUser = '';
+ if ( $un != '' ) {
+ $username = Title::makeTitleSafe( NS_USER, $un );
+ if( ! is_null( $username ) ) {
+ $this->requestedUser = $username->getText();
+ }
+ }
+ parent::__construct();
+ }
+
+
+ function getIndexField() {
+ return 'user_name';
+ }
+
+ function getQueryInfo() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $conds=array();
+ // don't show hidden names
+ $conds[]='ipb_deleted IS NULL OR ipb_deleted = 0';
+ if ($this->requestedGroup != "") {
+ $conds['ug_group'] = $this->requestedGroup;
+ $useIndex = '';
+ } else {
+ $useIndex = $dbr->useIndexClause('user_name');
+ }
+ if ($this->requestedUser != "") {
+ $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser );
+ }
+
+ list ($user,$user_groups,$ipblocks) = $dbr->tableNamesN('user','user_groups','ipblocks');
+
+ $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 ",
+ 'fields' => array('user_name',
+ 'MAX(user_id) AS user_id',
+ 'COUNT(ug_group) AS numgroups',
+ 'MAX(ug_group) AS singlegroup'),
+ 'options' => array('GROUP BY' => 'user_name'),
+ 'conds' => $conds
+ );
+
+ wfRunHooks( 'SpecialListusersQueryInfo', array( $this, &$query ) );
+ return $query;
+ }
+
+ function formatRow( $row ) {
+ $userPage = Title::makeTitle( NS_USER, $row->user_name );
+ $name = $this->getSkin()->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) );
+
+ if( $row->numgroups > 1 || ( $this->requestedGroup && $row->numgroups == 1 ) ) {
+ $list = array();
+ foreach( self::getGroups( $row->user_id ) as $group )
+ $list[] = self::buildGroupLink( $group );
+ $groups = implode( ', ', $list );
+ } elseif( $row->numgroups == 1 ) {
+ $groups = self::buildGroupLink( $row->singlegroup );
+ } else {
+ $groups = '';
+ }
+
+ $item = wfSpecialList( $name, $groups );
+ wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) );
+ return "<li>{$item}</li>";
+ }
+
+ function getBody() {
+ if (!$this->mQueryDone) {
+ $this->doQuery();
+ }
+ $batch = new LinkBatch;
+
+ $this->mResult->rewind();
+
+ while ( $row = $this->mResult->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
+ }
+ $batch->execute();
+ $this->mResult->rewind();
+ return parent::getBody();
+ }
+
+ function getPageHeader( ) {
+ global $wgScript, $wgRequest;
+ $self = $this->getTitle();
+
+ # Form tag
+ $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+ '<fieldset>' .
+ Xml::element( 'legend', array(), wfMsg( 'listusers' ) );
+ $out .= Xml::hidden( 'title', $self->getPrefixedDbKey() );
+
+ # Username field
+ $out .= Xml::label( wfMsg( 'listusersfrom' ), 'offset' ) . ' ' .
+ Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' ';
+
+ # Group drop-down list
+ $out .= Xml::label( wfMsg( 'group' ), 'group' ) . ' ' .
+ Xml::openElement('select', array( 'name' => 'group', 'id' => 'group' ) ) .
+ Xml::option( wfMsg( 'group-all' ), '' );
+ foreach( $this->getAllGroups() as $group => $groupText )
+ $out .= Xml::option( $groupText, $group, $group == $this->requestedGroup );
+ $out .= Xml::closeElement( 'select' ) . ' ';
+
+ wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) );
+
+ # Submit button and form bottom
+ if( $this->mLimit )
+ $out .= Xml::hidden( 'limit', $this->mLimit );
+ $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+ wfRunHooks( 'SpecialListusersHeader', array( $this, &$out ) );
+ $out .= '</fieldset>' .
+ Xml::closeElement( 'form' );
+
+ return $out;
+ }
+
+ function getAllGroups() {
+ $result = array();
+ foreach( User::getAllGroups() as $group ) {
+ $result[$group] = User::getGroupName( $group );
+ }
+ return $result;
+ }
+
+ /**
+ * Preserve group and username offset parameters when paging
+ * @return array
+ */
+ function getDefaultQuery() {
+ $query = parent::getDefaultQuery();
+ if( $this->requestedGroup != '' )
+ $query['group'] = $this->requestedGroup;
+ if( $this->requestedUser != '' )
+ $query['username'] = $this->requestedUser;
+ wfRunHooks( 'SpecialListusersDefaultQuery', array( $this, &$query ) );
+ return $query;
+ }
+
+ /**
+ * Get a list of groups the specified user belongs to
+ *
+ * @param int $uid
+ * @return array
+ */
+ protected static function getGroups( $uid ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $groups = array();
+ $res = $dbr->select( 'user_groups', 'ug_group', array( 'ug_user' => $uid ), __METHOD__ );
+ if( $res && $dbr->numRows( $res ) > 0 ) {
+ while( $row = $dbr->fetchObject( $res ) )
+ $groups[] = $row->ug_group;
+ $dbr->freeResult( $res );
+ }
+ return $groups;
+ }
+
+ /**
+ * Format a link to a group description page
+ *
+ * @param string $group
+ * @return string
+ */
+ protected static function buildGroupLink( $group ) {
+ static $cache = array();
+ if( !isset( $cache[$group] ) )
+ $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
+ return $cache[$group];
+ }
+}
+
+/**
+ * constructor
+ * $par string (optional) A group to list users from
+ */
+function wfSpecialListusers( $par = null ) {
+ global $wgRequest, $wgOut;
+
+ $up = new UsersPager($par);
+
+ # getBody() first to check, if empty
+ $usersbody = $up->getBody();
+ $s = $up->getPageHeader();
+ if( $usersbody ) {
+ $s .= $up->getNavigationBar();
+ $s .= '<ul>' . $usersbody . '</ul>';
+ $s .= $up->getNavigationBar() ;
+ } else {
+ $s .= '<p>' . wfMsgHTML('listusers-noresult') . '</p>';
+ };
+
+ $wgOut->addHTML( $s );
+}
diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php
new file mode 100644
index 00000000..04019223
--- /dev/null
+++ b/includes/specials/SpecialLockdb.php
@@ -0,0 +1,131 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialLockdb() {
+ global $wgUser, $wgOut, $wgRequest;
+
+ if( !$wgUser->isAllowed( 'siteadmin' ) ) {
+ $wgOut->permissionRequired( 'siteadmin' );
+ return;
+ }
+
+ # If the lock file isn't writable, we can do sweet bugger all
+ global $wgReadOnlyFile;
+ if( !is_writable( dirname( $wgReadOnlyFile ) ) ) {
+ DBLockForm::notWritable();
+ return;
+ }
+
+ $action = $wgRequest->getVal( 'action' );
+ $f = new DBLockForm();
+
+ if ( 'success' == $action ) {
+ $f->showSuccess();
+ } else if ( 'submit' == $action && $wgRequest->wasPosted() &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $f->doSubmit();
+ } else {
+ $f->showForm( '' );
+ }
+}
+
+/**
+ * A form to make the database readonly (eg for maintenance purposes).
+ * @ingroup SpecialPage
+ */
+class DBLockForm {
+ var $reason = '';
+
+ function DBLockForm() {
+ global $wgRequest;
+ $this->reason = $wgRequest->getText( 'wpLockReason' );
+ }
+
+ function showForm( $err ) {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
+ $wgOut->addWikiMsg( 'lockdbtext' );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsg( 'formerror' ) );
+ $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
+ }
+ $lc = htmlspecialchars( wfMsg( 'lockconfirm' ) );
+ $lb = htmlspecialchars( wfMsg( 'lockbtn' ) );
+ $elr = htmlspecialchars( wfMsg( 'enterlockreason' ) );
+ $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
+ $action = $titleObj->escapeLocalURL( 'action=submit' );
+ $reason = htmlspecialchars( $this->reason );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ $wgOut->addHTML( <<<END
+<form id="lockdb" method="post" action="{$action}">
+{$elr}:
+<textarea name="wpLockReason" rows="10" cols="60" wrap="virtual">{$reason}</textarea>
+<table border="0">
+ <tr>
+ <td align="right">
+ <input type="checkbox" name="wpLockConfirm" />
+ </td>
+ <td align="left">{$lc}</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td align="left">
+ <input type="submit" name="wpLock" value="{$lb}" />
+ </td>
+ </tr>
+</table>
+<input type="hidden" name="wpEditToken" value="{$token}" />
+</form>
+END
+);
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgUser, $wgLang, $wgRequest;
+ global $wgReadOnlyFile;
+
+ if ( ! $wgRequest->getCheck( 'wpLockConfirm' ) ) {
+ $this->showForm( wfMsg( 'locknoconfirm' ) );
+ return;
+ }
+ $fp = @fopen( $wgReadOnlyFile, 'w' );
+
+ if ( false === $fp ) {
+ # This used to show a file not found error, but the likeliest reason for fopen()
+ # to fail at this point is insufficient permission to write to the file...good old
+ # is_writable() is plain wrong in some cases, it seems...
+ self::notWritable();
+ return;
+ }
+ fwrite( $fp, $this->reason );
+ fwrite( $fp, "\n<p>(by " . $wgUser->getName() . " at " .
+ $wgLang->timeanddate( wfTimestampNow() ) . ")\n" );
+ fclose( $fp );
+
+ $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
+ $wgOut->redirect( $titleObj->getFullURL( 'action=success' ) );
+ }
+
+ function showSuccess() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
+ $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) );
+ $wgOut->addWikiMsg( 'lockdbsuccesstext' );
+ }
+
+ public static function notWritable() {
+ global $wgOut;
+ $wgOut->showErrorPage( 'lockdb', 'lockfilenotwritable' );
+ }
+}
diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php
new file mode 100644
index 00000000..3154ed13
--- /dev/null
+++ b/includes/specials/SpecialLog.php
@@ -0,0 +1,65 @@
+<?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
+
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialLog( $par = '' ) {
+ global $wgRequest, $wgOut, $wgUser;
+ # Get parameters
+ $type = $wgRequest->getVal( 'type', $par );
+ $user = $wgRequest->getText( 'user' );
+ $title = $wgRequest->getText( 'page' );
+ $pattern = $wgRequest->getBool( 'pattern' );
+ $y = $wgRequest->getIntOrNull( 'year' );
+ $m = $wgRequest->getIntOrNull( 'month' );
+ # Don't let the user get stuck with a certain date
+ $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
+ if( $skip ) {
+ $y = '';
+ $m = '';
+ }
+ # 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 );
+ # Set title and add header
+ $loglist->showHeader( $pager->getType() );
+ # Show form options
+ $loglist->showOptions( $pager->getType(), $pager->getUser(), $pager->getPage(), $pager->getPattern(),
+ $pager->getYear(), $pager->getMonth() );
+ # Insert list
+ $logBody = $pager->getBody();
+ if( $logBody ) {
+ $wgOut->addHTML(
+ $pager->getNavigationBar() .
+ $loglist->beginLogEventsList() .
+ $logBody .
+ $loglist->endLogEventsList() .
+ $pager->getNavigationBar()
+ );
+ } else {
+ $wgOut->addWikiMsg( 'logempty' );
+ }
+}
diff --git a/includes/specials/SpecialLonelypages.php b/includes/specials/SpecialLonelypages.php
new file mode 100644
index 00000000..5aafac7d
--- /dev/null
+++ b/includes/specials/SpecialLonelypages.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page looking for articles with no article linking to them,
+ * thus being lonely.
+ * @ingroup SpecialPage
+ */
+class LonelyPagesPage extends PageQueryPage {
+
+ function getName() {
+ return "Lonelypages";
+ }
+ function getPageHeader() {
+ return wfMsgExt( 'lonelypagestext', array( 'parse' ) );
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' );
+
+ return
+ "SELECT 'Lonelypages' AS type,
+ page_namespace AS namespace,
+ page_title AS title,
+ page_title AS value
+ FROM $page
+ LEFT JOIN $pagelinks
+ ON page_namespace=pl_namespace AND page_title=pl_title
+ WHERE pl_namespace IS NULL
+ AND page_namespace=".NS_MAIN."
+ AND page_is_redirect=0";
+
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialLonelypages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new LonelyPagesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialLongpages.php b/includes/specials/SpecialLongpages.php
new file mode 100644
index 00000000..be16a029
--- /dev/null
+++ b/includes/specials/SpecialLongpages.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ * @ingroup SpecialPage
+ */
+class LongPagesPage extends ShortPagesPage {
+
+ function getName() {
+ return "Longpages";
+ }
+
+ function sortDescending() {
+ return true;
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialLongpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new LongPagesPage();
+
+ $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php
new file mode 100644
index 00000000..82ee4be6
--- /dev/null
+++ b/includes/specials/SpecialMIMEsearch.php
@@ -0,0 +1,138 @@
+<?php
+/**
+ * A special page to search for files by MIME type as defined in the
+ * img_major_mime and img_minor_mime fields in the image table
+ *
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Searches the database for files of the requested MIME type, comparing this with the
+ * 'img_major_mime' and 'img_minor_mime' fields in the image table.
+ * @ingroup SpecialPage
+ */
+class MIMEsearchPage extends QueryPage {
+ var $major, $minor;
+
+ function MIMEsearchPage( $major, $minor ) {
+ $this->major = $major;
+ $this->minor = $minor;
+ }
+
+ function getName() { return 'MIMEsearch'; }
+
+ /**
+ * Due to this page relying upon extra fields being passed in the SELECT it
+ * will fail if it's set as expensive and misermode is on
+ */
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function linkParameters() {
+ $arr = array( $this->major, $this->minor );
+ $mime = implode( '/', $arr );
+ return array( 'mime' => $mime );
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $image = $dbr->tableName( 'image' );
+ $major = $dbr->addQuotes( $this->major );
+ $minor = $dbr->addQuotes( $this->minor );
+
+ return
+ "SELECT 'MIMEsearch' AS type,
+ " . NS_IMAGE . " AS namespace,
+ img_name AS title,
+ img_major_mime AS value,
+
+ img_size,
+ img_width,
+ img_height,
+ img_user_text,
+ img_timestamp
+ FROM $image
+ WHERE img_major_mime = $major AND img_minor_mime = $minor
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang, $wgLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getText() );
+ $plink = $skin->makeLink( $nt->getPrefixedText(), $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 );
+
+ return "($download) $plink . . $dimensions . . $bytes . . $user . . $time";
+ }
+}
+
+/**
+ * Output the HTML search form, and constructs the MIMEsearchPage object.
+ */
+function wfSpecialMIMEsearch( $par = null ) {
+ global $wgRequest, $wgTitle, $wgOut;
+
+ $mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' );
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) .
+ Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' .
+ Xml::submitButton( wfMsg( 'ilsubmit' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' )
+ );
+
+ list( $major, $minor ) = wfSpecialMIMEsearchParse( $mime );
+ if ( $major == '' or $minor == '' or !wfSpecialMIMEsearchValidType( $major ) )
+ return;
+ $wpp = new MIMEsearchPage( $major, $minor );
+
+ list( $limit, $offset ) = wfCheckLimits();
+ $wpp->doQuery( $offset, $limit );
+}
+
+function wfSpecialMIMEsearchParse( $str ) {
+ // searched for an invalid MIME type.
+ if( strpos( $str, '/' ) === false) {
+ return array ('', '');
+ }
+
+ list( $major, $minor ) = explode( '/', $str, 2 );
+
+ return array(
+ ltrim( $major, ' ' ),
+ rtrim( $minor, ' ' )
+ );
+}
+
+function wfSpecialMIMEsearchValidType( $type ) {
+ // From maintenance/tables.sql => img_major_mime
+ $types = array(
+ 'unknown',
+ 'application',
+ 'audio',
+ 'image',
+ 'text',
+ 'video',
+ 'message',
+ 'model',
+ 'multipart'
+ );
+
+ return in_array( $type, $types );
+}
diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php
new file mode 100644
index 00000000..0460c207
--- /dev/null
+++ b/includes/specials/SpecialMergeHistory.php
@@ -0,0 +1,448 @@
+<?php
+/**
+ * Special page allowing users with the appropriate permissions to
+ * merge article histories, with some restrictions
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialMergehistory( $par ) {
+ global $wgRequest;
+
+ $form = new MergehistoryForm( $wgRequest, $par );
+ $form->execute();
+}
+
+/**
+ * The HTML form for Special:MergeHistory, which allows users with the appropriate
+ * permissions to view and restore deleted content.
+ * @ingroup SpecialPage
+ */
+class MergehistoryForm {
+ var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment;
+ var $mTargetObj, $mDestObj;
+
+ function MergehistoryForm( $request, $par = "" ) {
+ global $wgUser;
+
+ $this->mAction = $request->getVal( 'action' );
+ $this->mTarget = $request->getVal( 'target' );
+ $this->mDest = $request->getVal( 'dest' );
+ $this->mSubmitted = $request->getBool( 'submitted' );
+
+ $this->mTargetID = intval( $request->getVal( 'targetID' ) );
+ $this->mDestID = intval( $request->getVal( 'destID' ) );
+ $this->mTimestamp = $request->getVal( 'mergepoint' );
+ if( !preg_match("/[0-9]{14}/",$this->mTimestamp) ) {
+ $this->mTimestamp = '';
+ }
+ $this->mComment = $request->getText( 'wpComment' );
+
+ $this->mMerge = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+ // target page
+ if( $this->mSubmitted ) {
+ $this->mTargetObj = Title::newFromURL( $this->mTarget );
+ $this->mDestObj = Title::newFromURL( $this->mDest );
+ } else {
+ $this->mTargetObj = null;
+ $this->mDestObj = null;
+ }
+
+ $this->preCacheMessages();
+ }
+
+ /**
+ * As we use the same small set of messages in various methods and that
+ * they are called often, we call them once and save them in $this->message
+ */
+ function preCacheMessages() {
+ // Precache various messages
+ if( !isset( $this->message ) ) {
+ $this->message['last'] = wfMsgExt( 'last', array( 'escape') );
+ }
+ }
+
+ function execute() {
+ global $wgOut, $wgUser;
+
+ $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );
+
+ if( $this->mTargetID && $this->mDestID && $this->mAction=="submit" && $this->mMerge ) {
+ return $this->merge();
+ }
+
+ if ( !$this->mSubmitted ) {
+ $this->showMergeForm();
+ return;
+ }
+
+ $errors = array();
+ if ( !$this->mTargetObj instanceof Title ) {
+ $errors[] = wfMsgExt( 'mergehistory-invalid-source', array( 'parse' ) );
+ } elseif( !$this->mTargetObj->exists() ) {
+ $errors[] = wfMsgExt( 'mergehistory-no-source', array( 'parse' ),
+ wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
+ );
+ }
+
+ if ( !$this->mDestObj instanceof Title) {
+ $errors[] = wfMsgExt( 'mergehistory-invalid-destination', array( 'parse' ) );
+ } elseif( !$this->mDestObj->exists() ) {
+ $errors[] = wfMsgExt( 'mergehistory-no-destination', array( 'parse' ),
+ wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
+ );
+ }
+
+ // TODO: warn about target = dest?
+
+ if ( count( $errors ) ) {
+ $this->showMergeForm();
+ $wgOut->addHTML( implode( "\n", $errors ) );
+ } else {
+ $this->showHistory();
+ }
+
+ }
+
+ function showMergeForm() {
+ global $wgOut, $wgScript;
+
+ $wgOut->addWikiMsg( 'mergehistory-header' );
+
+ $wgOut->addHtml(
+ Xml::openElement( 'form', array(
+ 'method' => 'get',
+ 'action' => $wgScript ) ) .
+ '<fieldset>' .
+ Xml::element( 'legend', array(),
+ wfMsg( 'mergehistory-box' ) ) .
+ Xml::hidden( 'title',
+ SpecialPage::getTitleFor( 'Mergehistory' )->getPrefixedDbKey() ) .
+ Xml::hidden( 'submitted', '1' ) .
+ Xml::hidden( 'mergepoint', $this->mTimestamp ) .
+ Xml::openElement( 'table' ) .
+ "<tr>
+ <td>".Xml::label( wfMsg( 'mergehistory-from' ), 'target' )."</td>
+ <td>".Xml::input( 'target', 30, $this->mTarget, array('id'=>'target') )."</td>
+ </tr><tr>
+ <td>".Xml::label( wfMsg( 'mergehistory-into' ), 'dest' )."</td>
+ <td>".Xml::input( 'dest', 30, $this->mDest, array('id'=>'dest') )."</td>
+ </tr><tr><td>" .
+ Xml::submitButton( wfMsg( 'mergehistory-go' ) ) .
+ "</td></tr>" .
+ Xml::closeElement( 'table' ) .
+ '</fieldset>' .
+ '</form>' );
+ }
+
+ private function showHistory() {
+ global $wgLang, $wgContLang, $wgUser, $wgOut;
+
+ $this->sk = $wgUser->getSkin();
+
+ $wgOut->setPagetitle( wfMsg( "mergehistory" ) );
+
+ $this->showMergeForm();
+
+ # List all stored revisions
+ $revisions = new MergeHistoryPager( $this, array(), $this->mTargetObj, $this->mDestObj );
+ $haveRevisions = $revisions && $revisions->getNumRows() > 0;
+
+ $titleObj = SpecialPage::getTitleFor( "Mergehistory" );
+ $action = $titleObj->getLocalURL( "action=submit" );
+ # Start the form here
+ $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) );
+ $wgOut->addHtml( $top );
+
+ if( $haveRevisions ) {
+ # Format the user-visible controls (comment field, submission button)
+ # in a nice little table
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+ $table =
+ Xml::openElement( 'fieldset' ) .
+ Xml::openElement( 'table' ) .
+ "<tr>
+ <td colspan='2'>" .
+ wfMsgExt( 'mergehistory-merge', array('parseinline'),
+ $this->mTargetObj->getPrefixedText(), $this->mDestObj->getPrefixedText() ) .
+ "</td>
+ </tr>
+ <tr>
+ <td align='$align'>" .
+ Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
+ "</td>
+ <td>" .
+ Xml::input( 'wpComment', 50, $this->mComment ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>" .
+ Xml::submitButton( wfMsg( 'mergehistory-submit' ), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' );
+
+ $wgOut->addHtml( $table );
+ }
+
+ $wgOut->addHTML( "<h2 id=\"mw-mergehistory\">" . wfMsgHtml( "mergehistory-list" ) . "</h2>\n" );
+
+ if( $haveRevisions ) {
+ $wgOut->addHTML( $revisions->getNavigationBar() );
+ $wgOut->addHTML( "<ul>" );
+ $wgOut->addHTML( $revisions->getBody() );
+ $wgOut->addHTML( "</ul>" );
+ $wgOut->addHTML( $revisions->getNavigationBar() );
+ } else {
+ $wgOut->addWikiMsg( "mergehistory-empty" );
+ }
+
+ # Show relevant lines from the deletion log:
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'merge' ) ) . "</h2>\n" );
+ LogEventsList::showLogExtract( $wgOut, 'merge', $this->mTargetObj->getPrefixedText() );
+
+ # When we submit, go by page ID to avoid some nasty but unlikely collisions.
+ # Such would happen if a page was renamed after the form loaded, but before submit
+ $misc = Xml::hidden( 'targetID', $this->mTargetObj->getArticleID() );
+ $misc .= Xml::hidden( 'destID', $this->mDestObj->getArticleID() );
+ $misc .= Xml::hidden( 'target', $this->mTarget );
+ $misc .= Xml::hidden( 'dest', $this->mDest );
+ $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
+ $misc .= Xml::closeElement( 'form' );
+ $wgOut->addHtml( $misc );
+
+ return true;
+ }
+
+ function formatRevisionRow( $row ) {
+ global $wgUser, $wgLang;
+
+ $rev = new Revision( $row );
+
+ $stxt = '';
+ $last = $this->message['last'];
+
+ $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
+ $checkBox = wfRadio( "mergepoint", $ts, false );
+
+ $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(),
+ htmlspecialchars( $wgLang->timeanddate( $ts ) ), 'oldid=' . $rev->getId() );
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
+ }
+
+ # Last link
+ 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] );
+
+ $userLink = $this->sk->revUserTools( $rev );
+
+ if(!is_null($size = $row->rev_len)) {
+ $stxt = $this->sk->formatRevisionSize( $size );
+ }
+ $comment = $this->sk->revComment( $rev );
+
+ return "<li>$checkBox ($last) $pageLink . . $userLink $stxt $comment</li>";
+ }
+
+ /**
+ * Fetch revision text link if it's available to all users
+ * @return string
+ */
+ function getPageLink( $row, $titleObj, $ts, $target ) {
+ global $wgLang;
+
+ 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&timestamp=$ts" );
+ if( $this->isDeleted($row, Revision::DELETED_TEXT) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ function merge() {
+ global $wgOut, $wgUser;
+ # 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...
+ $targetTitle = Title::newFromID( $this->mTargetID );
+ $destTitle = Title::newFromID( $this->mDestID );
+ if( is_null($targetTitle) || is_null($destTitle) )
+ return false; // validate these
+ if( $targetTitle->getArticleId() == $destTitle->getArticleId() )
+ return false;
+ # Verify that this timestamp is valid
+ # Must be older than the destination page
+ $dbw = wfGetDB( DB_MASTER );
+ # Get timestamp into DB format
+ $this->mTimestamp = $this->mTimestamp ? $dbw->timestamp($this->mTimestamp) : '';
+ # Max timestamp should be min of destination page
+ $maxtimestamp = $dbw->selectField( 'revision', 'MIN(rev_timestamp)',
+ array('rev_page' => $this->mDestID ),
+ __METHOD__ );
+ # Destination page must exist with revisions
+ if( !$maxtimestamp ) {
+ $wgOut->addWikiMsg('mergehistory-fail');
+ return false;
+ }
+ # Get the latest timestamp of the source
+ $lasttimestamp = $dbw->selectField( array('page','revision'),
+ 'rev_timestamp',
+ array('page_id' => $this->mTargetID, 'page_latest = rev_id' ),
+ __METHOD__ );
+ # $this->mTimestamp must be older than $maxtimestamp
+ if( $this->mTimestamp >= $maxtimestamp ) {
+ $wgOut->addWikiMsg('mergehistory-fail');
+ return false;
+ }
+ # Update the revisions
+ if( $this->mTimestamp ) {
+ $timewhere = "rev_timestamp <= {$this->mTimestamp}";
+ $TimestampLimit = wfTimestamp(TS_MW,$this->mTimestamp);
+ } else {
+ $timewhere = "rev_timestamp <= {$maxtimestamp}";
+ $TimestampLimit = wfTimestamp(TS_MW,$lasttimestamp);
+ }
+ # Do the moving...
+ $dbw->update( 'revision',
+ array( 'rev_page' => $this->mDestID ),
+ array( 'rev_page' => $this->mTargetID,
+ $timewhere ),
+ __METHOD__ );
+
+ $count = $dbw->affectedRows();
+ # Make the source page a redirect if no revisions are left
+ $haveRevisions = $dbw->selectField( 'revision',
+ 'rev_timestamp',
+ array( 'rev_page' => $this->mTargetID ),
+ __METHOD__,
+ array( 'FOR UPDATE' ) );
+ if( !$haveRevisions ) {
+ if( $this->mComment ) {
+ $comment = wfMsgForContent( 'mergehistory-comment', $targetTitle->getPrefixedText(),
+ $destTitle->getPrefixedText(), $this->mComment );
+ } else {
+ $comment = wfMsgForContent( 'mergehistory-autocomment', $targetTitle->getPrefixedText(),
+ $destTitle->getPrefixedText() );
+ }
+ $mwRedir = MagicWord::get( 'redirect' );
+ $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $destTitle->getPrefixedText() . "]]\n";
+ $redirectArticle = new Article( $targetTitle );
+ $redirectRevision = new Revision( array(
+ 'page' => $this->mTargetID,
+ 'comment' => $comment,
+ 'text' => $redirectText ) );
+ $redirectRevision->insertOn( $dbw );
+ $redirectArticle->updateRevisionOn( $dbw, $redirectRevision );
+
+ # Now, we record the link from the redirect to the new title.
+ # It should have no other outgoing links...
+ $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ );
+ $dbw->insert( 'pagelinks',
+ array(
+ 'pl_from' => $this->mDestID,
+ 'pl_namespace' => $destTitle->getNamespace(),
+ 'pl_title' => $destTitle->getDBkey() ),
+ __METHOD__ );
+ } else {
+ $targetTitle->invalidateCache(); // update histories
+ }
+ $destTitle->invalidateCache(); // update histories
+ # Check if this did anything
+ if( !$count ) {
+ $wgOut->addWikiMsg('mergehistory-fail');
+ return false;
+ }
+ # Update our logs
+ $log = new LogPage( 'merge' );
+ $log->addEntry( 'merge', $targetTitle, $this->mComment,
+ array($destTitle->getPrefixedText(),$TimestampLimit) );
+
+ $wgOut->addHtml( wfMsgExt( 'mergehistory-success', array('parseinline'),
+ $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) );
+
+ wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
+
+ return true;
+ }
+}
+
+class MergeHistoryPager extends ReverseChronologicalPager {
+ public $mForm, $mConds;
+
+ function __construct( $form, $conds = array(), $source, $dest ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ $this->title = $source;
+ $this->articleID = $source->getArticleID();
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $maxtimestamp = $dbr->selectField( 'revision', 'MIN(rev_timestamp)',
+ array('rev_page' => $dest->getArticleID() ),
+ __METHOD__ );
+ $this->maxTimestamp = $maxtimestamp;
+
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $this->mResult->seek( 0 );
+ $batch = new LinkBatch();
+ # Give some pointers to make (last) links
+ $this->mForm->prevId = array();
+ while( $row = $this->mResult->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->rev_user_text ) );
+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->rev_user_text ) );
+
+ $rev_id = isset($rev_id) ? $rev_id : $row->rev_id;
+ if( $rev_id > $row->rev_id )
+ $this->mForm->prevId[$rev_id] = $row->rev_id;
+ else if( $rev_id < $row->rev_id )
+ $this->mForm->prevId[$row->rev_id] = $rev_id;
+
+ $rev_id = $row->rev_id;
+ }
+
+ $batch->execute();
+ $this->mResult->seek( 0 );
+
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ $block = new Block;
+ return $this->mForm->formatRevisionRow( $row );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds['rev_page'] = $this->articleID;
+ $conds[] = "rev_timestamp < {$this->maxTimestamp}";
+
+ return array(
+ 'tables' => array('revision'),
+ 'fields' => array( 'rev_minor_edit', 'rev_timestamp', 'rev_user', 'rev_user_text', 'rev_comment',
+ 'rev_id', 'rev_page', 'rev_text_id', 'rev_len', 'rev_deleted' ),
+ 'conds' => $conds
+ );
+ }
+
+ function getIndexField() {
+ return 'rev_timestamp';
+ }
+}
diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php
new file mode 100644
index 00000000..e6810999
--- /dev/null
+++ b/includes/specials/SpecialMostcategories.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * implements Special:Mostcategories
+ * @ingroup SpecialPage
+ */
+class MostcategoriesPage extends QueryPage {
+
+ function getName() { return 'Mostcategories'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $categorylinks, $page) = $dbr->tableNamesN( 'categorylinks', 'page' );
+ return
+ "
+ SELECT
+ 'Mostcategories' as type,
+ page_namespace as namespace,
+ page_title as title,
+ COUNT(*) as value
+ FROM $categorylinks
+ LEFT JOIN $page ON cl_from = page_id
+ WHERE page_namespace = " . NS_MAIN . "
+ GROUP BY page_namespace, page_title
+ HAVING COUNT(*) > 1
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang;
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if ( !$title instanceof Title ) { throw new MWException('Invalid title in database'); }
+ $count = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) );
+ $link = $skin->makeKnownLinkObj( $title, $title->getText() );
+ return wfSpecialList( $link, $count );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostcategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostcategoriesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostimages.php b/includes/specials/SpecialMostimages.php
new file mode 100644
index 00000000..6cfeb7ad
--- /dev/null
+++ b/includes/specials/SpecialMostimages.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * implements Special:Mostimages
+ * @ingroup SpecialPage
+ */
+class MostimagesPage extends ImageQueryPage {
+
+ function getName() { return 'Mostimages'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $imagelinks = $dbr->tableName( 'imagelinks' );
+ return
+ "
+ SELECT
+ 'Mostimages' as type,
+ " . NS_IMAGE . " as namespace,
+ il_to as title,
+ COUNT(*) as value
+ FROM $imagelinks
+ GROUP BY il_to
+ HAVING COUNT(*) > 1
+ ";
+ }
+
+ function getCellHtml( $row ) {
+ global $wgLang;
+ return wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $row->value ) ) . '<br />';
+ }
+
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialMostimages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostimagesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php
new file mode 100644
index 00000000..078489bd
--- /dev/null
+++ b/includes/specials/SpecialMostlinked.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page to show pages ordered by the number of pages linking to them.
+ * Implements Special:Mostlinked
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @copyright © 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class MostlinkedPage extends QueryPage {
+
+ function getName() { return 'Mostlinked'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ /**
+ * Note: Getting page_namespace only works if $this->isCached() is false
+ */
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ 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
+ 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";
+ }
+
+ /**
+ * Pre-fill the link cache
+ */
+ function preprocessResults( $db, $res ) {
+ if( $db->numRows( $res ) > 0 ) {
+ $linkBatch = new LinkBatch();
+ while( $row = $db->fetchObject( $res ) )
+ $linkBatch->add( $row->namespace, $row->title );
+ $db->dataSeek( $res, 0 );
+ $linkBatch->execute();
+ }
+ }
+
+ /**
+ * Make a link to "what links here" for the specified title
+ *
+ * @param $title Title being queried
+ * @param $skin Skin to use
+ * @return string
+ */
+ function makeWlhLink( &$title, $caption, &$skin ) {
+ $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() );
+ return $skin->makeKnownLinkObj( $wlh, $caption );
+ }
+
+ /**
+ * Make links to the page corresponding to the item, and the "what links here" page for it
+ *
+ * @param $skin Skin to be used
+ * @param $result Result row
+ * @return string
+ */
+ function formatResult( $skin, $result ) {
+ global $wgLang;
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ $link = $skin->makeLinkObj( $title );
+ $wlh = $this->makeWlhLink( $title,
+ wfMsgExt( 'nlinks', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) ), $skin );
+ return wfSpecialList( $link, $wlh );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostlinked() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostlinkedPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php
new file mode 100644
index 00000000..ab250675
--- /dev/null
+++ b/includes/specials/SpecialMostlinkedcategories.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A querypage to show categories ordered in descending order by the pages in them
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class MostlinkedCategoriesPage extends QueryPage {
+
+ function getName() { return 'Mostlinkedcategories'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $categorylinks = $dbr->tableName( 'categorylinks' );
+ $name = $dbr->addQuotes( $this->getName() );
+ return
+ "
+ SELECT
+ $name as type,
+ " . NS_CATEGORY . " as namespace,
+ cl_to as title,
+ COUNT(*) as value
+ FROM $categorylinks
+ GROUP BY cl_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 = $skin->makeLinkObj( $nt, htmlspecialchars( $text ) );
+
+ $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ return wfSpecialList($plink, $nlinks);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostlinkedCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostlinkedCategoriesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php
new file mode 100644
index 00000000..d597a4e0
--- /dev/null
+++ b/includes/specials/SpecialMostlinkedtemplates.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page lists templates with a large number of
+ * transclusion links, i.e. "most used" templates
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class SpecialMostlinkedtemplates extends QueryPage {
+
+ /**
+ * Name of the report
+ *
+ * @return string
+ */
+ public function getName() {
+ return 'Mostlinkedtemplates';
+ }
+
+ /**
+ * Is this report expensive, i.e should it be cached?
+ *
+ * @return bool
+ */
+ public function isExpensive() {
+ return true;
+ }
+
+ /**
+ * Is there a feed available?
+ *
+ * @return bool
+ */
+ public function isSyndicated() {
+ return false;
+ }
+
+ /**
+ * Sort the results in descending order?
+ *
+ * @return bool
+ */
+ public function sortDescending() {
+ return true;
+ }
+
+ /**
+ * Generate SQL for the report
+ *
+ * @return string
+ */
+ public function getSql() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $templatelinks = $dbr->tableName( 'templatelinks' );
+ $name = $dbr->addQuotes( $this->getName() );
+ return "SELECT {$name} AS type,
+ " . NS_TEMPLATE . " AS namespace,
+ tl_title AS title,
+ COUNT(*) AS value
+ FROM {$templatelinks}
+ WHERE tl_namespace = " . NS_TEMPLATE . "
+ GROUP BY tl_title";
+ }
+
+ /**
+ * Pre-cache page existence to speed up link generation
+ *
+ * @param Database $dbr Database connection
+ * @param int $res Result pointer
+ */
+ public function preprocessResults( $db, $res ) {
+ $batch = new LinkBatch();
+ while( $row = $db->fetchObject( $res ) ) {
+ $batch->add( $row->namespace, $row->title );
+ }
+ $batch->execute();
+ if( $db->numRows( $res ) > 0 )
+ $db->dataSeek( $res, 0 );
+ }
+
+ /**
+ * Format a result row
+ *
+ * @param Skin $skin Skin to use for UI elements
+ * @param object $result Result row
+ * @return string
+ */
+ public function formatResult( $skin, $result ) {
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if( $title instanceof Title ) {
+ return wfSpecialList(
+ $skin->makeLinkObj( $title ),
+ $this->makeWlhLink( $title, $skin, $result )
+ );
+ } else {
+ $tsafe = htmlspecialchars( $result->title );
+ return "Invalid title in result set; {$tsafe}";
+ }
+ }
+
+ /**
+ * 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->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
+ }
+}
+
+/**
+ * Execution function
+ *
+ * @param mixed $par Parameters passed to the page
+ */
+function wfSpecialMostlinkedtemplates( $par = false ) {
+ list( $limit, $offset ) = wfCheckLimits();
+ $mlt = new SpecialMostlinkedtemplates();
+ $mlt->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMostrevisions.php b/includes/specials/SpecialMostrevisions.php
new file mode 100644
index 00000000..f5a0f8c0
--- /dev/null
+++ b/includes/specials/SpecialMostrevisions.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * A special page to show pages in the
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class MostrevisionsPage extends QueryPage {
+
+ function getName() { return 'Mostrevisions'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $revision, $page ) = $dbr->tableNamesN( 'revision', 'page' );
+ return
+ "
+ SELECT
+ 'Mostrevisions' as type,
+ page_namespace as namespace,
+ page_title as title,
+ COUNT(*) as value
+ FROM $revision
+ JOIN $page ON page_id = rev_page
+ WHERE page_namespace = " . NS_MAIN . "
+ GROUP BY page_namespace, page_title
+ HAVING COUNT(*) > 1
+ ";
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+
+ $nt = Title::makeTitle( $result->namespace, $result->title );
+ $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+ $plink = $skin->makeKnownLinkObj( $nt, $text );
+
+ $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' );
+
+ return wfSpecialList($plink, $nlink);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostrevisions() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new MostrevisionsPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php
new file mode 100644
index 00000000..efd2dcfd
--- /dev/null
+++ b/includes/specials/SpecialMovepage.php
@@ -0,0 +1,452 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialMovepage( $par = null ) {
+ global $wgUser, $wgOut, $wgRequest, $action;
+
+ # Check for database lock
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ $target = isset( $par ) ? $par : $wgRequest->getVal( 'target' );
+ $oldTitleText = $wgRequest->getText( 'wpOldTitle', $target );
+ $newTitleText = $wgRequest->getText( 'wpNewTitle' );
+
+ $oldTitle = Title::newFromText( $oldTitleText );
+ $newTitle = Title::newFromText( $newTitleText );
+
+ if( is_null( $oldTitle ) ) {
+ $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ return;
+ }
+ if( !$oldTitle->exists() ) {
+ $wgOut->showErrorPage( 'nopagetitle', 'nopagetext' );
+ return;
+ }
+
+ # Check rights
+ $permErrors = $oldTitle->getUserPermissionsErrors( 'move', $wgUser );
+ if( !empty( $permErrors ) ) {
+ $wgOut->showPermissionsErrorPage( $permErrors );
+ return;
+ }
+
+ $form = new MovePageForm( $oldTitle, $newTitle );
+
+ if ( 'submit' == $action && $wgRequest->wasPosted()
+ && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $form->doSubmit();
+ } else {
+ $form->showForm( '' );
+ }
+}
+
+/**
+ * HTML form for Special:Movepage
+ * @ingroup SpecialPage
+ */
+class MovePageForm {
+ var $oldTitle, $newTitle, $reason; # Text input
+ var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects;
+
+ private $watch = false;
+
+ function MovePageForm( $oldTitle, $newTitle ) {
+ global $wgRequest;
+ $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
+ $this->oldTitle = $oldTitle;
+ $this->newTitle = $newTitle;
+ $this->reason = $wgRequest->getText( 'wpReason' );
+ if ( $wgRequest->wasPosted() ) {
+ $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', false );
+ $this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', false );
+ } else {
+ $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true );
+ $this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', true );
+ }
+ $this->moveSubpages = $wgRequest->getBool( 'wpMovesubpages', false );
+ $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' );
+ $this->watch = $wgRequest->getCheck( 'wpWatch' );
+ }
+
+ function showForm( $err, $hookErr = '' ) {
+ global $wgOut, $wgUser;
+
+ $skin = $wgUser->getSkin();
+
+ $oldTitleLink = $skin->makeLinkObj( $this->oldTitle );
+ $oldTitle = $this->oldTitle->getPrefixedText();
+
+ $wgOut->setPagetitle( wfMsg( 'move-page', $oldTitle ) );
+ $wgOut->setSubtitle( wfMsg( 'move-page-backlink', $oldTitleLink ) );
+
+ if( $this->newTitle == '' ) {
+ # Show the current title as a default
+ # when the form is first opened.
+ $newTitle = $oldTitle;
+ } else {
+ if( $err == '' ) {
+ $nt = Title::newFromURL( $this->newTitle );
+ if( $nt ) {
+ # If a title was supplied, probably from the move log revert
+ # link, check for validity. We can then show some diagnostic
+ # information and save a click.
+ $newerr = $this->oldTitle->isValidMoveOperation( $nt );
+ if( is_string( $newerr ) ) {
+ $err = $newerr;
+ }
+ }
+ }
+ $newTitle = $this->newTitle;
+ }
+
+ if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
+ $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle );
+ $movepagebtn = wfMsg( 'delete_and_move' );
+ $submitVar = 'wpDeleteAndMove';
+ $confirm = "
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) .
+ "</td>
+ </tr>";
+ $err = '';
+ } else {
+ $wgOut->addWikiMsg( 'movepagetext' );
+ $movepagebtn = wfMsg( 'movepagebtn' );
+ $submitVar = 'wpMove';
+ $confirm = false;
+ }
+
+ $oldTalk = $this->oldTitle->getTalkPage();
+ $considerTalk = ( !$this->oldTitle->isTalkPage() && $oldTalk->exists() );
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $hasRedirects = $dbr->selectField( 'redirect', '1',
+ array(
+ 'rd_namespace' => $this->oldTitle->getNamespace(),
+ 'rd_title' => $this->oldTitle->getDBkey(),
+ ) , __METHOD__ );
+
+ if ( $considerTalk ) {
+ $wgOut->addWikiMsg( 'movepagetalktext' );
+ }
+
+ $titleObj = SpecialPage::getTitleFor( 'Movepage' );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ if ( $err != '' ) {
+ $wgOut->setSubtitle( wfMsg( 'formerror' ) );
+ if( $err == 'hookaborted' ) {
+ $errMsg = "<p><strong class=\"error\">$hookErr</strong></p>\n";
+ $wgOut->addHTML( $errMsg );
+ } else {
+ $wgOut->wrapWikiMsg( '<p><strong class="error">$1</strong></p>', $err );
+ }
+ }
+
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'move-page-legend' ) ) .
+ Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-movepage-table' ) ) .
+ "<tr>
+ <td class='mw-label'>" .
+ wfMsgHtml( 'movearticle' ) .
+ "</td>
+ <td class='mw-input'>
+ <strong>{$oldTitleLink}</strong>
+ </td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'newtitle' ), 'wpNewTitle' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpNewTitle', 40, $newTitle, array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) .
+ Xml::hidden( 'wpOldTitle', $oldTitle ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ 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 ) ) .
+ "</td>
+ </tr>"
+ );
+
+ if( $considerTalk ) {
+ $wgOut->addHTML( "
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) .
+ "</td>
+ </tr>"
+ );
+ }
+
+ if ( $hasRedirects ) {
+ $wgOut->addHTML( "
+ <tr>
+ <td></td>
+ <td class='mw-input' >" .
+ Xml::checkLabel( wfMsg( 'fix-double-redirects' ), 'wpFixRedirects',
+ 'wpFixRedirects', $this->fixRedirects ) .
+ "</td>
+ </td>"
+ );
+ }
+
+ if( ($this->oldTitle->hasSubpages() || $this->oldTitle->getTalkPage()->hasSubpages())
+ && $this->oldTitle->userCan( 'move-subpages' ) ) {
+ $wgOut->addHTML( "
+ <tr>
+ <td></td>
+ <td class=\"mw-input\">" .
+ Xml::checkLabel( wfMsgHtml(
+ $this->oldTitle->hasSubpages()
+ ? 'move-subpages'
+ : 'move-talk-subpages'
+ ),
+ '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( "
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) .
+ "</td>
+ </tr>
+ {$confirm}
+ <tr>
+ <td>&nbsp;</td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::hidden( 'wpEditToken', $token ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) .
+ "\n"
+ );
+
+ $this->showLogFragment( $this->oldTitle, $wgOut );
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgUser, $wgRequest, $wgMaximumMovedPages, $wgLang;
+
+ if ( $wgUser->pingLimiter( 'move' ) ) {
+ $wgOut->rateLimited();
+ return;
+ }
+
+ $ot = $this->oldTitle;
+ $nt = $this->newTitle;
+
+ # Delete to make way if requested
+ if ( $wgUser->isAllowed( 'delete' ) && $this->deleteAndMove ) {
+ $article = new Article( $nt );
+
+ # Disallow deletions of big articles
+ $bigHistory = $article->isBigDeletion();
+ if( $bigHistory && !$nt->userCan( 'bigdelete' ) ) {
+ global $wgLang, $wgDeleteRevisionsLimit;
+ $this->showForm( array('delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
+ return;
+ }
+
+ // This may output an error message and exit
+ $article->doDelete( wfMsgForContent( 'delete_and_move_reason' ) );
+ }
+
+ # don't allow moving to pages with # in
+ if ( !$nt || $nt->getFragment() != '' ) {
+ $this->showForm( 'badtitletext' );
+ return;
+ }
+
+ $error = $ot->moveTo( $nt, true, $this->reason );
+ if ( $error !== true ) {
+ # FIXME: showForm() should handle multiple errors
+ call_user_func_array(array($this, 'showForm'), $error[0]);
+ return;
+ }
+
+ if ( $this->fixRedirects ) {
+ DoubleRedirectJob::fixRedirects( 'move', $ot, $nt );
+ }
+
+ wfRunHooks( 'SpecialMovepageAfterMove', array( &$this , &$ot , &$nt ) ) ;
+
+ $wgOut->setPagetitle( wfMsg( 'pagemovedsub' ) );
+
+ $oldUrl = $ot->getFullUrl( 'redirect=no' );
+ $newUrl = $nt->getFullUrl();
+ $oldText = $ot->getPrefixedText();
+ $newText = $nt->getPrefixedText();
+ $oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>";
+ $newLink = "<span class='plainlinks'>[$newUrl $newText]</span>";
+
+ $wgOut->addWikiMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText );
+
+ # Now we move extra pages we've been asked to move: subpages and talk
+ # pages. First, if the old page or the new page is a talk page, we
+ # can't move any talk pages: cancel that.
+ if( $ot->isTalkPage() || $nt->isTalkPage() ) {
+ $this->moveTalk = false;
+ }
+
+ if( !$ot->userCan( 'move-subpages' ) ) {
+ $this->moveSubpages = false;
+ }
+
+ # Next make a list of id's. This might be marginally less efficient
+ # than a more direct method, but this is not a highly performance-cri-
+ # tical code path and readable code is more important here.
+ #
+ # Note: this query works nicely on MySQL 5, but the optimizer in MySQL
+ # 4 might get confused. If so, consider rewriting as a UNION.
+ #
+ # If the target namespace doesn't allow subpages, moving with subpages
+ # would mean that you couldn't move them back in one operation, which
+ # is bad. FIXME: A specific error message should be given in this
+ # case.
+ $dbr = wfGetDB( DB_MASTER );
+ if( $this->moveSubpages && (
+ MWNamespace::hasSubpages( $nt->getNamespace() ) || (
+ $this->moveTalk &&
+ MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
+ )
+ ) ) {
+ $conds = array(
+ 'page_title LIKE '.$dbr->addQuotes( $dbr->escapeLike( $ot->getDBkey() ) . '/%' )
+ .' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
+ );
+ $conds['page_namespace'] = array();
+ if( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
+ $conds['page_namespace'] []= $ot->getNamespace();
+ }
+ if( $this->moveTalk && MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() ) ) {
+ $conds['page_namespace'] []= $ot->getTalkPage()->getNamespace();
+ }
+ } elseif( $this->moveTalk ) {
+ $conds = array(
+ 'page_namespace' => $ot->getTalkPage()->getNamespace(),
+ 'page_title' => $ot->getDBKey()
+ );
+ } else {
+ # Skip the query
+ $conds = null;
+ }
+
+ $extrapages = array();
+ if( !is_null( $conds ) ) {
+ $extrapages = $dbr->select( 'page',
+ array( 'page_id', 'page_namespace', 'page_title' ),
+ $conds,
+ __METHOD__
+ );
+ }
+
+ $extraOutput = array();
+ $skin = $wgUser->getSkin();
+ $count = 1;
+ foreach( $extrapages as $row ) {
+ if( $row->page_id == $ot->getArticleId() ) {
+ # Already did this one.
+ continue;
+ }
+
+ $oldSubpage = Title::newFromRow( $row );
+ $newPageName = preg_replace(
+ '#^'.preg_quote( $ot->getDBKey(), '#' ).'#',
+ $nt->getDBKey(),
+ $oldSubpage->getDBKey()
+ );
+ if( $oldSubpage->isTalkPage() ) {
+ $newNs = $nt->getTalkPage()->getNamespace();
+ } else {
+ $newNs = $nt->getSubjectPage()->getNamespace();
+ }
+ # Bug 14385: we need makeTitleSafe because the new page names may
+ # be longer than 255 characters.
+ $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
+ if( !$newSubpage ) {
+ $oldLink = $skin->makeKnownLinkObj( $oldSubpage );
+ $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink,
+ htmlspecialchars(Title::makeName( $newNs, $newPageName )));
+ continue;
+ }
+
+ # This was copy-pasted from Renameuser, bleh.
+ if ( $newSubpage->exists() && !$oldSubpage->isValidMoveTarget( $newSubpage ) ) {
+ $link = $skin->makeKnownLinkObj( $newSubpage );
+ $extraOutput []= wfMsgHtml( 'movepage-page-exists', $link );
+ } else {
+ $success = $oldSubpage->moveTo( $newSubpage, true, $this->reason );
+ if( $success === true ) {
+ if ( $this->fixRedirects ) {
+ DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage );
+ }
+ $oldLink = $skin->makeKnownLinkObj( $oldSubpage, '', 'redirect=no' );
+ $newLink = $skin->makeKnownLinkObj( $newSubpage );
+ $extraOutput []= wfMsgHtml( 'movepage-page-moved', $oldLink, $newLink );
+ } else {
+ $oldLink = $skin->makeKnownLinkObj( $oldSubpage );
+ $newLink = $skin->makeLinkObj( $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() ) {
+ $wgOut->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
+ }
+
+ # Deal with watches (we don't watch subpages)
+ if( $this->watch ) {
+ $wgUser->addWatch( $ot );
+ $wgUser->addWatch( $nt );
+ } else {
+ $wgUser->removeWatch( $ot );
+ $wgUser->removeWatch( $nt );
+ }
+ }
+
+ function showLogFragment( $title, &$out ) {
+ $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
new file mode 100644
index 00000000..e57f6fc1
--- /dev/null
+++ b/includes/specials/SpecialNewimages.php
@@ -0,0 +1,211 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ * FIXME: this code is crap, should use Pager and Database::select().
+ */
+
+/**
+ *
+ */
+function wfSpecialNewimages( $par, $specialPage ) {
+ global $wgUser, $wgOut, $wgLang, $wgRequest, $wgGroupPermissions, $wgMiserMode;
+
+ $wpIlMatch = $wgRequest->getText( 'wpIlMatch' );
+ $dbr = wfGetDB( DB_SLAVE );
+ $sk = $wgUser->getSkin();
+ $shownav = !$specialPage->including();
+ $hidebots = $wgRequest->getBool('hidebots',1);
+
+ $hidebotsql = '';
+ if ($hidebots) {
+
+ /** Make a list of group names which have the 'bot' flag
+ set.
+ */
+ $botconds=array();
+ foreach ($wgGroupPermissions as $groupname=>$perms) {
+ if(array_key_exists('bot',$perms) && $perms['bot']) {
+ $botconds[]="ug_group='$groupname'";
+ }
+ }
+
+ /* If not bot groups, do not set $hidebotsql */
+ if ($botconds) {
+ $isbotmember=$dbr->makeList($botconds, LIST_OR);
+
+ /** This join, in conjunction with WHERE ug_group
+ IS NULL, returns only those rows from IMAGE
+ where the uploading user is not a member of
+ a group which has the 'bot' permission set.
+ */
+ $ug = $dbr->tableName('user_groups');
+ $hidebotsql = " LEFT OUTER JOIN $ug ON img_user=ug_user AND ($isbotmember)";
+ }
+ }
+
+ $image = $dbr->tableName('image');
+
+ $sql="SELECT img_timestamp from $image";
+ if ($hidebotsql) {
+ $sql .= "$hidebotsql WHERE ug_group IS NULL";
+ }
+ $sql.=' ORDER BY img_timestamp DESC LIMIT 1';
+ $res = $dbr->query($sql, 'wfSpecialNewImages');
+ $row = $dbr->fetchRow($res);
+ if($row!==false) {
+ $ts=$row[0];
+ } else {
+ $ts=false;
+ }
+ $dbr->freeResult($res);
+ $sql='';
+
+ /** If we were clever, we'd use this to cache. */
+ $latestTimestamp = wfTimestamp( TS_MW, $ts);
+
+ /** Hardcode this for now. */
+ $limit = 48;
+
+ if ( $parval = intval( $par ) ) {
+ if ( $parval <= $limit && $parval > 0 ) {
+ $limit = $parval;
+ }
+ }
+
+ $where = array();
+ $searchpar = '';
+ if ( $wpIlMatch != '' && !$wgMiserMode) {
+ $nt = Title::newFromUrl( $wpIlMatch );
+ if($nt ) {
+ $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
+ $m = str_replace( '%', "\\%", $m );
+ $m = str_replace( '_', "\\_", $m );
+ $where[] = "LOWER(img_name) LIKE '%{$m}%'";
+ $searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch );
+ }
+ }
+
+ $invertSort = false;
+ if( $until = $wgRequest->getVal( 'until' ) ) {
+ $where[] = "img_timestamp < '" . $dbr->timestamp( $until ) . "'";
+ }
+ if( $from = $wgRequest->getVal( 'from' ) ) {
+ $where[] = "img_timestamp >= '" . $dbr->timestamp( $from ) . "'";
+ $invertSort = true;
+ }
+ $sql='SELECT img_size, img_name, img_user, img_user_text,'.
+ "img_description,img_timestamp FROM $image";
+
+ if($hidebotsql) {
+ $sql .= $hidebotsql;
+ $where[]='ug_group IS NULL';
+ }
+ if(count($where)) {
+ $sql.=' WHERE '.$dbr->makeList($where, LIST_AND);
+ }
+ $sql.=' ORDER BY img_timestamp '. ( $invertSort ? '' : ' DESC' );
+ $sql.=' LIMIT '.($limit+1);
+ $res = $dbr->query($sql, 'wfSpecialNewImages');
+
+ /**
+ * We have to flip things around to get the last N after a certain date
+ */
+ $images = array();
+ while ( $s = $dbr->fetchObject( $res ) ) {
+ if( $invertSort ) {
+ array_unshift( $images, $s );
+ } else {
+ array_push( $images, $s );
+ }
+ }
+ $dbr->freeResult( $res );
+
+ $gallery = new ImageGallery();
+ $firstTimestamp = null;
+ $lastTimestamp = null;
+ $shownImages = 0;
+ foreach( $images as $s ) {
+ if( ++$shownImages > $limit ) {
+ # One extra just to test for whether to show a page link;
+ # don't actually show it.
+ break;
+ }
+
+ $name = $s->img_name;
+ $ut = $s->img_user_text;
+
+ $nt = Title::newFromText( $name, NS_IMAGE );
+ $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut );
+
+ $gallery->add( $nt, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" );
+
+ $timestamp = wfTimestamp( TS_MW, $s->img_timestamp );
+ if( empty( $firstTimestamp ) ) {
+ $firstTimestamp = $timestamp;
+ }
+ $lastTimestamp = $timestamp;
+ }
+
+ $bydate = wfMsg( 'bydate' );
+ $lt = $wgLang->formatNum( min( $shownImages, $limit ) );
+ if ($shownav) {
+ $text = wfMsgExt( 'imagelisttext', array('parse'), $lt, $bydate );
+ $wgOut->addHTML( $text . "\n" );
+ }
+
+ $sub = wfMsg( 'ilsubmit' );
+ $titleObj = SpecialPage::getTitleFor( 'Newimages' );
+ $action = $titleObj->escapeLocalURL( $hidebots ? '' : 'hidebots=0' );
+ if ($shownav && !$wgMiserMode) {
+ $wgOut->addHTML( "<form id=\"imagesearch\" method=\"post\" action=\"" .
+ "{$action}\">" .
+ Xml::input( 'wpIlMatch', 20, $wpIlMatch ) . ' ' .
+ Xml::submitButton( $sub, array( 'name' => 'wpIlSubmit' ) ) .
+ "</form>" );
+ }
+
+ /**
+ * Paging controls...
+ */
+
+ # If we change bot visibility, this needs to be carried along.
+ if(!$hidebots) {
+ $botpar='&hidebots=0';
+ } else {
+ $botpar='';
+ }
+ $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);
+
+
+ $opts = array( 'parsemag', 'escapenoentities' );
+ $prevLink = wfMsgExt( 'prevn', $opts, $wgLang->formatNum( $limit ) );
+ if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) {
+ $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar );
+ }
+
+ $nextLink = wfMsgExt( 'nextn', $opts, $wgLang->formatNum( $limit ) );
+ if( $shownImages > $limit && $lastTimestamp ) {
+ $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar );
+ }
+
+ $prevnext = '<p>' . $botLink . ' '. wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $dateLink ) .'</p>';
+
+ if ($shownav)
+ $wgOut->addHTML( $prevnext );
+
+ if( count( $images ) ) {
+ $wgOut->addHTML( $gallery->toHTML() );
+ if ($shownav)
+ $wgOut->addHTML( $prevnext );
+ } else {
+ $wgOut->addWikiMsg( 'noimages' );
+ }
+}
diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php
new file mode 100644
index 00000000..1a410ae0
--- /dev/null
+++ b/includes/specials/SpecialNewpages.php
@@ -0,0 +1,437 @@
+<?php
+
+/**
+ * implements Special:Newpages
+ * @ingroup SpecialPage
+ */
+class SpecialNewpages extends SpecialPage {
+
+ // Stored objects
+ protected $opts, $skin;
+
+ // Some internal settings
+ protected $showNavigation = false;
+
+ public function __construct(){
+ parent::__construct( 'Newpages' );
+ $this->includable( true );
+ }
+
+ protected function setup( $par ) {
+ global $wgRequest, $wgUser, $wgEnableNewpagesUserFilter;
+
+ // Options
+ $opts = new FormOptions();
+ $this->opts = $opts; // bind
+ $opts->add( 'hideliu', false );
+ $opts->add( 'hidepatrolled', false );
+ $opts->add( 'hidebots', false );
+ $opts->add( 'limit', 50 );
+ $opts->add( 'offset', '' );
+ $opts->add( 'namespace', '0' );
+ $opts->add( 'username', '' );
+ $opts->add( 'feed', '' );
+
+ // Set values
+ $opts->fetchValuesFromRequest( $wgRequest );
+ if ( $par ) $this->parseParams( $par );
+
+ // Validate
+ $opts->validateIntBounds( 'limit', 0, 5000 );
+ if( !$wgEnableNewpagesUserFilter ) {
+ $opts->setValue( 'username', '' );
+ }
+
+ // Store some objects
+ $this->skin = $wgUser->getSkin();
+ }
+
+ protected function parseParams( $par ) {
+ global $wgLang;
+ $bits = preg_split( '/\s*,\s*/', trim( $par ) );
+ foreach ( $bits as $bit ) {
+ if ( 'shownav' == $bit )
+ $this->showNavigation = true;
+ if ( 'hideliu' === $bit )
+ $this->opts->setValue( 'hideliu', true );
+ if ( 'hidepatrolled' == $bit )
+ $this->opts->setValue( 'hidepatrolled', true );
+ if ( 'hidebots' == $bit )
+ $this->opts->setValue( 'hidebots', true );
+ if ( is_numeric( $bit ) )
+ $this->opts->setValue( 'limit', intval( $bit ) );
+
+ $m = array();
+ if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) )
+ $this->opts->setValue( 'limit', intval($m[1]) );
+ // PG offsets not just digits!
+ if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) )
+ $this->opts->setValue( 'offset', intval($m[1]) );
+ if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
+ $ns = $wgLang->getNsIndex( $m[1] );
+ if( $ns !== false ) {
+ $this->opts->setValue( 'namespace', $ns );
+ }
+ }
+ }
+ }
+
+ /**
+ * Show a form for filtering namespace and username
+ *
+ * @param string $par
+ * @return string
+ */
+ public function execute( $par ) {
+ global $wgLang, $wgGroupPermissions, $wgUser, $wgOut;
+
+ $this->setHeaders();
+ $this->outputHeader();
+
+ $this->showNavigation = !$this->including(); // Maybe changed in setup
+ $this->setup( $par );
+
+ if( !$this->including() ) {
+ // Settings
+ $this->form();
+
+ $this->setSyndicated();
+ $feedType = $this->opts->getValue( 'feed' );
+ if( $feedType ) {
+ return $this->feed( $feedType );
+ }
+ }
+
+ $pager = new NewPagesPager( $this, $this->opts );
+ $pager->mLimit = $this->opts->getValue( 'limit' );
+ $pager->mOffset = $this->opts->getValue( 'offset' );
+
+ if( $pager->getNumRows() ) {
+ $navigation = '';
+ if ( $this->showNavigation ) $navigation = $pager->getNavigationBar();
+ $wgOut->addHTML( $navigation . $pager->getBody() . $navigation );
+ } else {
+ $wgOut->addWikiMsg( 'specialpage-empty' );
+ }
+ }
+
+ protected function filterLinks() {
+ global $wgGroupPermissions, $wgUser;
+
+ // show/hide links
+ $showhide = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) );
+
+ // Option value -> message mapping
+ $filters = array(
+ 'hideliu' => 'rcshowhideliu',
+ 'hidepatrolled' => 'rcshowhidepatr',
+ 'hidebots' => 'rcshowhidebots'
+ );
+
+ // Disable some if needed
+ if ( $wgGroupPermissions['*']['createpage'] !== true )
+ unset($filters['hideliu']);
+
+ if ( !$wgUser->useNPPatrol() )
+ unset($filters['hidepatrolled']);
+
+ $links = array();
+ $changed = $this->opts->getChangedValues();
+ unset($changed['offset']); // Reset offset if query type changes
+
+ $self = $this->getTitle();
+ foreach ( $filters as $key => $msg ) {
+ $onoff = 1 - $this->opts->getValue($key);
+ $link = $this->skin->makeKnownLinkObj( $self, $showhide[$onoff],
+ wfArrayToCGI( array( $key => $onoff ), $changed )
+ );
+ $links[$key] = wfMsgHtml( $msg, $link );
+ }
+
+ return implode( ' | ', $links );
+ }
+
+ protected function form() {
+ global $wgOut, $wgEnableNewpagesUserFilter, $wgScript;
+
+ // Consume values
+ $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
+ $namespace = $this->opts->consumeValue( 'namespace' );
+ $username = $this->opts->consumeValue( 'username' );
+
+ // Check username input validity
+ $ut = Title::makeTitleSafe( NS_USER, $username );
+ $userText = $ut ? $ut->getText() : '';
+
+ // Store query values in hidden fields so that form submission doesn't lose them
+ $hidden = array();
+ foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
+ $hidden[] = Xml::hidden( $key, $value );
+ }
+ $hidden = implode( "\n", $hidden );
+
+ $form = Xml::openElement( 'form', array( 'action' => $wgScript ) ) .
+ Xml::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
+ Xml::fieldset( wfMsg( 'newpages' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'mw-newpages-table' ) ) .
+ "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::namespaceSelector( $namespace, 'all' ) .
+ "</td>
+ </tr>" .
+ ($wgEnableNewpagesUserFilter ?
+ "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'username', 30, $userText, array( 'id' => 'mw-np-username' ) ) .
+ "</td>
+ </tr>" : "" ) .
+ "<tr> <td></td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+ "</td>
+ </tr>" .
+ "<tr>
+ <td></td>
+ <td class='mw-input'>" .
+ $this->filterLinks() .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' ) .
+ $hidden .
+ Xml::closeElement( 'form' );
+
+ $wgOut->addHTML( $form );
+ }
+
+ protected function setSyndicated() {
+ global $wgOut;
+ $queryParams = array(
+ 'namespace' => $this->opts->getValue( 'namespace' ),
+ 'username' => $this->opts->getValue( 'username' )
+ );
+ $wgOut->setSyndicated( true );
+ $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) );
+ }
+
+ /**
+ * Format a row, providing the timestamp, links to the page/history, size, user links, and a comment
+ *
+ * @param $skin Skin to use
+ * @param $result Result row
+ * @return string
+ */
+ public function formatRow( $result ) {
+ global $wgLang, $wgContLang, $wgUser;
+ $dm = $wgContLang->getDirMark();
+
+ $title = Title::makeTitleSafe( $result->page_namespace, $result->page_title );
+ $time = $wgLang->timeAndDate( $result->rc_timestamp, true );
+ $plink = $this->skin->makeKnownLinkObj( $title, '', $this->patrollable( $result ) ? 'rcid=' . $result->rc_id : '' );
+ $hist = $this->skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
+ $length = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->length ) );
+ $ulink = $this->skin->userLink( $result->rc_user, $result->rc_user_text ) . ' ' .
+ $this->skin->userToolLinks( $result->rc_user, $result->rc_user_text );
+ $comment = $this->skin->commentBlock( $result->rc_comment );
+ $css = $this->patrollable( $result ) ? " class='not-patrolled'" : '';
+
+ return "<li{$css}>{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment}</li>\n";
+ }
+
+ /**
+ * Should a specific result row provide "patrollable" links?
+ *
+ * @param $result Result row
+ * @return bool
+ */
+ protected function patrollable( $result ) {
+ global $wgUser;
+ return ( $wgUser->useNPPatrol() && !$result->rc_patrolled );
+ }
+
+ /**
+ * Output a subscription feed listing recent edits to this page.
+ * @param string $type
+ */
+ protected function feed( $type ) {
+ global $wgFeed, $wgFeedClasses;
+
+ if ( !$wgFeed ) {
+ global $wgOut;
+ $wgOut->addWikiMsg( 'feed-unavailable' );
+ return;
+ }
+
+ if( !isset( $wgFeedClasses[$type] ) ) {
+ global $wgOut;
+ $wgOut->addWikiMsg( 'feed-invalid' );
+ return;
+ }
+
+ $feed = new $wgFeedClasses[$type](
+ $this->feedTitle(),
+ wfMsg( 'tagline' ),
+ $this->getTitle()->getFullUrl() );
+
+ $pager = new NewPagesPager( $this, $this->opts );
+ $limit = $this->opts->getValue( 'limit' );
+ global $wgFeedLimit;
+ if( $limit > $wgFeedLimit ) {
+ $limit = $wgFeedLimit;
+ }
+ $pager->mLimit = $limit;
+
+ $feed->outHeader();
+ if( $pager->getNumRows() > 0 ) {
+ while( $row = $pager->mResult->fetchObject() ) {
+ $feed->outItem( $this->feedItem( $row ) );
+ }
+ }
+ $feed->outFooter();
+ }
+
+ protected function feedTitle() {
+ global $wgContLanguageCode, $wgSitename;
+ $page = SpecialPage::getPage( 'Newpages' );
+ $desc = $page->getDescription();
+ return "$wgSitename - $desc [$wgContLanguageCode]";
+ }
+
+ protected function feedItem( $row ) {
+ $title = Title::MakeTitle( intval( $row->page_namespace ), $row->page_title );
+ if( $title ) {
+ $date = $row->rc_timestamp;
+ $comments = $title->getTalkPage()->getFullURL();
+
+ return new FeedItem(
+ $title->getPrefixedText(),
+ $this->feedItemDesc( $row ),
+ $title->getFullURL(),
+ $date,
+ $this->feedItemAuthor( $row ),
+ $comments);
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Quickie hack... strip out wikilinks to more legible form from the comment.
+ */
+ protected function stripComment( $text ) {
+ return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
+ }
+
+ protected function feedItemAuthor( $row ) {
+ return isset( $row->rc_user_text ) ? $row->rc_user_text : '';
+ }
+
+ protected function feedItemDesc( $row ) {
+ $revision = Revision::newFromId( $row->rev_id );
+ if( $revision ) {
+ return '<p>' . htmlspecialchars( $revision->getUserText() ) . ': ' .
+ htmlspecialchars( $revision->getComment() ) .
+ "</p>\n<hr />\n<div>" .
+ nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+ }
+ return '';
+ }
+}
+
+/**
+ * @ingroup SpecialPage Pager
+ */
+class NewPagesPager extends ReverseChronologicalPager {
+ // Stored opts
+ protected $opts, $mForm;
+
+ private $hideliu, $hidepatrolled, $hidebots, $namespace, $user, $spTitle;
+
+ function __construct( $form, FormOptions $opts ) {
+ parent::__construct();
+ $this->mForm = $form;
+ $this->opts = $opts;
+ }
+
+ function getTitle(){
+ static $title = null;
+ if ( $title === null )
+ $title = $this->mForm->getTitle();
+ return $title;
+ }
+
+ function getQueryInfo() {
+ global $wgEnableNewpagesUserFilter, $wgGroupPermissions, $wgUser;
+ $conds = array();
+ $conds['rc_new'] = 1;
+
+ $namespace = $this->opts->getValue( 'namespace' );
+ $namespace = ( $namespace === 'all' ) ? false : intval( $namespace );
+
+ $username = $this->opts->getValue( 'username' );
+ $user = Title::makeTitleSafe( NS_USER, $username );
+
+ if( $namespace !== false ) {
+ $conds['page_namespace'] = $namespace;
+ $rcIndexes = array( 'new_name_timestamp' );
+ } else {
+ $rcIndexes = array( 'rc_timestamp' );
+ }
+ $conds[] = 'page_id = rc_cur_id';
+ $conds['page_is_redirect'] = 0;
+ # $wgEnableNewpagesUserFilter - temp WMF hack
+ if( $wgEnableNewpagesUserFilter && $user ) {
+ $conds['rc_user_text'] = $user->getText();
+ $rcIndexes = 'rc_user_text';
+ # If anons cannot make new pages, don't "exclude logged in users"!
+ } elseif( $wgGroupPermissions['*']['createpage'] && $this->opts->getValue( 'hideliu' ) ) {
+ $conds['rc_user'] = 0;
+ }
+ # If this user cannot see patrolled edits or they are off, don't do dumb queries!
+ if( $this->opts->getValue( 'hidepatrolled' ) && $wgUser->useNPPatrol() ) {
+ $conds['rc_patrolled'] = 0;
+ }
+ if( $this->opts->getValue( 'hidebots' ) ) {
+ $conds['rc_bot'] = 0;
+ }
+
+ return array(
+ 'tables' => array( 'recentchanges', 'page' ),
+ 'fields' => 'page_namespace,page_title, rc_cur_id, rc_user,rc_user_text,rc_comment,
+ rc_timestamp,rc_patrolled,rc_id,page_len as length, page_latest as rev_id',
+ 'conds' => $conds,
+ 'options' => array( 'USE INDEX' => array('recentchanges' => $rcIndexes) )
+ );
+ }
+
+ function getIndexField() {
+ return 'rc_timestamp';
+ }
+
+ function formatRow( $row ) {
+ return $this->mForm->formatRow( $row );
+ }
+
+ function getStartBody() {
+ # Do a batch existence check on pages
+ $linkBatch = new LinkBatch();
+ while( $row = $this->mResult->fetchObject() ) {
+ $linkBatch->add( NS_USER, $row->rc_user_text );
+ $linkBatch->add( NS_USER_TALK, $row->rc_user_text );
+ $linkBatch->add( $row->page_namespace, $row->page_title );
+ }
+ $linkBatch->execute();
+ return "<ul>";
+ }
+
+ function getEndBody() {
+ return "</ul>";
+ }
+}
diff --git a/includes/specials/SpecialPopularpages.php b/includes/specials/SpecialPopularpages.php
new file mode 100644
index 00000000..eb572736
--- /dev/null
+++ b/includes/specials/SpecialPopularpages.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Popularpages
+ * @ingroup SpecialPage
+ */
+class PopularPagesPage extends QueryPage {
+
+ function getName() {
+ return "Popularpages";
+ }
+
+ function isExpensive() {
+ # page_counter is not indexed
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+
+ $query =
+ "SELECT 'Popularpages' as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_counter as value
+ FROM $page ";
+ $where =
+ "WHERE page_is_redirect=0 AND page_namespace";
+
+ global $wgContentNamespaces;
+ if( empty( $wgContentNamespaces ) ) {
+ $where .= '='.NS_MAIN;
+ } else if( count( $wgContentNamespaces ) > 1 ) {
+ $where .= ' in (' . implode( ', ', $wgContentNamespaces ) . ')';
+ } else {
+ $where .= '='.$wgContentNamespaces[0];
+ }
+
+ return $query . $where;
+ }
+
+ 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 ) );
+ return wfSpecialList($link, $nv);
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialPopularpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $ppp = new PopularPagesPage();
+
+ return $ppp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php
new file mode 100644
index 00000000..b3468a3c
--- /dev/null
+++ b/includes/specials/SpecialPreferences.php
@@ -0,0 +1,1126 @@
+<?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, $mOldpass, $mNewpass, $mRetypePass, $mStubs;
+ var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick;
+ var $mUserLanguage, $mUserVariant;
+ var $mSearch, $mRecent, $mRecentDays, $mHourDiff, $mSearchLines, $mSearchChars, $mAction;
+ var $mReset, $mPosted, $mToggles, $mUseAjaxSearch, $mSearchNs, $mRealName, $mImageSize;
+ var $mUnderline, $mWatchlistEdits;
+
+ /**
+ * Constructor
+ * Load some values
+ */
+ function PreferencesForm( &$request ) {
+ global $wgContLang, $wgUser, $wgAllowRealName;
+
+ $this->mQuickbar = $request->getVal( 'wpQuickbar' );
+ $this->mOldpass = $request->getVal( 'wpOldpass' );
+ $this->mNewpass = $request->getVal( 'wpNewpass' );
+ $this->mRetypePass =$request->getVal( 'wpRetypePass' );
+ $this->mStubs = $request->getVal( 'wpStubs' );
+ $this->mRows = $request->getVal( 'wpRows' );
+ $this->mCols = $request->getVal( 'wpCols' );
+ $this->mSkin = $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->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->mPosted = $request->wasPosted();
+ $this->mSuccess = $request->getCheck( 'success' );
+ $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
+ $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
+ $this->mUseAjaxSearch = $request->getCheck( 'wpUseAjaxSearch' );
+ $this->mDisableMWSuggest = $request->getCheck( 'wpDisableMWSuggest' );
+
+ $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 ) );
+ }
+
+ function execute() {
+ global $wgUser, $wgOut;
+
+ if ( $wgUser->isAnon() ) {
+ $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext' );
+ return;
+ }
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ if ( $this->mReset ) {
+ $this->resetPrefs();
+ $this->mainPrefsForm( 'reset', wfMsg( 'prefsreset' ) );
+ } else if ( $this->mSaveprefs ) {
+ $this->savePreferences();
+ } 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 '00:00' if fed bogus data.
+ * Note: It's not a 100% correct implementation timezone-wise, it will
+ * accept stuff like '14:30',
+ * @access private
+ * @param string $s the user input
+ * @return string
+ */
+ function validateTimeZone( $s ) {
+ if ( $s !== '' ) {
+ if ( strpos( $s, ':' ) ) {
+ # HH:MM
+ $array = explode( ':' , $s );
+ $hour = intval( $array[0] );
+ $minute = intval( $array[1] );
+ } else {
+ $minute = intval( $s * 60 );
+ $hour = intval( $minute / 60 );
+ $minute = abs( $minute ) % 60;
+ }
+ # Max is +14:00 and min is -12:00, see:
+ # http://en.wikipedia.org/wiki/Timezone
+ $hour = min( $hour, 14 );
+ $hour = max( $hour, -12 );
+ $minute = min( $minute, 59 );
+ $minute = max( $minute, 0 );
+ $s = sprintf( "%02d:%02d", $hour, $minute );
+ }
+ return $s;
+ }
+
+ /**
+ * @access private
+ */
+ function savePreferences() {
+ global $wgUser, $wgOut, $wgParser;
+ global $wgEnableUserEmail, $wgEnableEmail;
+ global $wgEmailAuthentication, $wgRCMaxAge;
+ global $wgAuth, $wgEmailConfirmToEdit;
+
+
+ if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) {
+ if ( $this->mNewpass != $this->mRetypePass ) {
+ wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'badretype' ) );
+ $this->mainPrefsForm( 'error', wfMsg( 'badretype' ) );
+ return;
+ }
+
+ if (!$wgUser->checkPassword( $this->mOldpass )) {
+ wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'wrongpassword' ) );
+ $this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) );
+ return;
+ }
+
+ try {
+ $wgUser->setPassword( $this->mNewpass );
+ wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'success' ) );
+ $this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
+ } catch( PasswordError $e ) {
+ wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'error' ) );
+ $this->mainPrefsForm( 'error', $e->getMessage() );
+ return;
+ }
+ }
+ $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 ) ) );
+ 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 );
+ $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->mHourDiff, -12, 14 ) );
+ $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( 'ajaxsearch', $this->mUseAjaxSearch );
+ $wgUser->setOption( 'disablesuggest', $this->mDisableMWSuggest );
+
+ # 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 ) );
+ }
+ }
+
+ if( !$wgAuth->updateExternalDB( $wgUser ) ){
+ $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
+ return;
+ }
+
+ $msg = '';
+ if ( !wfRunHooks( 'SavePreferences', array( $this, $wgUser, &$msg, $oldOptions ) ) ) {
+ $this->mainPrefsForm( 'error', $msg );
+ return;
+ }
+
+ $wgUser->setCookies();
+ $wgUser->saveSettings();
+
+ if( $needRedirect && $error === false ) {
+ $title = SpecialPage::getTitleFor( 'Preferences' );
+ $wgOut->redirect( $title->getFullURL( 'success' ) );
+ return;
+ }
+
+ $wgOut->parserOptions( ParserOptions::newFromUser( $wgUser ) );
+ $this->mainPrefsForm( $error === false ? 'success' : 'error', $error);
+ }
+
+ /**
+ * @access private
+ */
+ function resetPrefs() {
+ global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName;
+
+ $this->mOldpass = $this->mNewpass = $this->mRetypePass = '';
+ $this->mUserEmail = $wgUser->getEmail();
+ $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp();
+ $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : '';
+
+ # language value might be blank, default to content language
+ $this->mUserLanguage = $wgUser->getOption( 'language', $wgContLanguageCode );
+
+ $this->mUserVariant = $wgUser->getOption( 'variant');
+ $this->mEmailFlag = $wgUser->getOption( 'disablemail' ) == 1 ? 1 : 0;
+ $this->mNick = $wgUser->getOption( 'nickname' );
+
+ $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' );
+ $this->mHourDiff = $wgUser->getOption( 'timecorrection' );
+ $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->mUseAjaxSearch = $wgUser->getBoolOption( 'ajaxsearch' );
+ $this->mDisableMWSuggest = $wgUser->getBoolOption( 'disablesuggest' );
+
+ $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 ) );
+ }
+
+ /**
+ * @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;
+ global $wgAllowRealName, $wgImageLimits, $wgThumbLimits;
+ global $wgDisableLangConversion;
+ global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits;
+ global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
+ global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
+ global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth;
+ global $wgEmailConfirmToEdit, $wgAjaxSearch, $wgEnableMWSuggest;
+
+ $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();
+ $skinNames = $wgLang->getSkinNames();
+ $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;
+
+
+ if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; }
+ else { $emfc = ''; }
+
+
+ if ($wgEmailAuthentication && ($this->mUserEmail != '') ) {
+ if( $wgUser->getEmailAuthenticationTimestamp() ) {
+ $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'<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 ) ),
+ implode( wfMsg( 'comma-separator' ), $userEffectiveGroupsArray ) .
+ '<br />(' . implode( ' | ', $toolLinks ) . ')'
+ ) .
+
+ $this->tableRow(
+ wfMsgHtml( 'prefs-edits' ),
+ $wgLang->formatNum( User::edits( $wgUser->getId() ) )
+ );
+
+ 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(
+ '&nbsp;',
+ 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(
+ '&nbsp;',
+ 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( '&nbsp;', $this->getToggle( 'fancysig' ) )
+ );
+
+ list( $lsLabel, $lsSelect) = Xml::languageSelector( $this->mUserLanguage );
+ $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
+ )
+ )
+ );
+ }
+ }
+
+ # Password
+ if( $wgAuth->allowPasswordChange() ) {
+ $wgOut->addHTML(
+ $this->tableRow( Xml::element( 'h2', null, wfMsg( 'changepassword' ) ) ) .
+ $this->tableRow(
+ Xml::label( wfMsg( 'oldpassword' ), 'wpOldpass' ),
+ Xml::password( 'wpOldpass', 25, $this->mOldpass, array( 'id' => 'wpOldpass' ) )
+ ) .
+ $this->tableRow(
+ Xml::label( wfMsg( 'newpassword' ), 'wpNewpass' ),
+ Xml::password( 'wpNewpass', 25, $this->mNewpass, array( 'id' => 'wpNewpass' ) )
+ ) .
+ $this->tableRow(
+ Xml::label( wfMsg( 'retypenew' ), 'wpRetypePass' ),
+ Xml::password( 'wpRetypePass', 25, $this->mRetypePass, array( 'id' => 'wpRetypePass' ) )
+ ) .
+ Xml::tags( 'tr', null,
+ Xml::tags( 'td', array( 'colspan' => '2' ),
+ $this->getToggle( "rememberpassword" )
+ )
+ )
+ );
+ }
+
+ # <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( wfHidden( 'wpQuickbar', $this->mQuickbar ) );
+ }
+
+ # Skin
+ #
+ $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg('skin') . "</legend>\n" );
+ $mptitle = Title::newMainPage();
+ $previewtext = wfMsg('skinpreview');
+ # Only show members of Skin::getSkinNames() rather than
+ # $skinNames (skins is all skin names from Language.php)
+ $validSkinNames = Skin::getSkinNames();
+ # 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 ) {
+ if ( isset( $skinNames[$skinkey] ) ) {
+ $skinname = $skinNames[$skinkey];
+ }
+ }
+ asort($validSkinNames);
+ foreach ($validSkinNames as $skinkey => $sn ) {
+ if ( in_array( $skinkey, $wgSkipSkins ) ) {
+ continue;
+ }
+ $checked = $skinkey == $this->mSkin ? ' checked="checked"' : '';
+
+ $mplink = htmlspecialchars($mptitle->getLocalURL("useskin=$skinkey"));
+ $previewlink = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
+ 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<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
+ #
+ $wgOut->addHTML(
+ "<fieldset>\n" . Xml::element( 'legend', null, wfMsg( 'files' ) ) . "\n"
+ );
+
+ $imageLimitOptions = null;
+ foreach ( $wgImageLimits as $index => $limits ) {
+ $selected = ($index == $this->mImageSize);
+ $imageLimitOptions .= Xml::option( "{$limits[0]}×{$limits[1]}" .
+ wfMsg('unit-pixel'), $index, $selected );
+ }
+
+ $imageSizeId = 'wpImageSize';
+ $wgOut->addHTML(
+ "<div>" . Xml::label( wfMsg('imagemaxsize'), $imageSizeId ) . " " .
+ Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) .
+ $imageLimitOptions .
+ Xml::closeElement( 'select' ) . "</div>\n"
+ );
+
+ $imageThumbOptions = null;
+ foreach ( $wgThumbLimits as $index => $size ) {
+ $selected = ($index == $this->mThumbSize);
+ $imageThumbOptions .= Xml::option($size . wfMsg('unit-pixel'), $index,
+ $selected);
+ }
+
+ $thumbSizeId = 'wpThumbSize';
+ $wgOut->addHTML(
+ "<div>" . Xml::label( wfMsg('thumbsize'), $thumbSizeId ) . " " .
+ Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) .
+ $imageThumbOptions .
+ Xml::closeElement( 'select' ) . "</div>\n"
+ );
+
+ $wgOut->addHTML( "</fieldset>\n\n" );
+
+ # 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 = $wgLang->time( $now = wfTimestampNow(), true );
+ $nowserver = $wgLang->time( $now, false );
+
+ $wgOut->addHTML(
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'timezonelegend' ) ) .
+ Xml::openElement( 'table' ) .
+ $this->addRow( wfMsg( 'servertime' ), $nowserver ) .
+ $this->addRow( wfMsg( 'localtime' ), $nowlocal ) .
+ $this->addRow(
+ Xml::label( wfMsg( 'timezoneoffset' ), 'wpHourDiff' ),
+ Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( 'id' => 'wpHourDiff' ) ) ) .
+ "<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( '<fieldset><legend>' . wfMsg( 'textboxsize' ) . '</legend>
+ <div>' .
+ wfInputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) .
+ ' ' .
+ wfInputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) .
+ "</div>" .
+ $this->getToggles( array(
+ 'editsection',
+ 'editsectiononrightclick',
+ 'editondblclick',
+ 'editwidth',
+ 'showtoolbar',
+ 'previewonfirst',
+ 'previewontop',
+ 'minordefault',
+ 'externaleditor',
+ 'externaldiff',
+ $wgLivePreview ? 'uselivepreview' : false,
+ 'forceeditsummary',
+ ) ) . '</fieldset>'
+ );
+
+ # Recent changes
+ $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-rc' ) . '</legend>' );
+
+ $rc = '<table><tr>';
+ $rc .= '<td>' . Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) . '</td>';
+ $rc .= '<td>' . Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . '</td>';
+ $rc .= '</tr><tr>';
+ $rc .= '<td>' . Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) . '</td>';
+ $rc .= '<td>' . Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) . '</td>';
+ $rc .= '</tr></table>';
+ $wgOut->addHtml( $rc );
+
+ $wgOut->addHtml( '<br />' );
+
+ $toggles[] = 'hideminor';
+ if( $wgRCShowWatchingUsers )
+ $toggles[] = 'shownumberswatching';
+ $toggles[] = 'usenewrc';
+ $wgOut->addHtml( $this->getToggles( $toggles ) );
+
+ $wgOut->addHtml( '</fieldset>' );
+
+ # Watchlist
+ $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-watchlist' ) . '</legend>' );
+
+ $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) );
+ $wgOut->addHtml( '<br /><br />' );
+
+ $wgOut->addHtml( $this->getToggle( 'extendwatchlist' ) );
+ $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) );
+ $wgOut->addHtml( '<br /><br />' );
+
+ $wgOut->addHtml( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'watchlisthideminor' ) ) );
+
+ 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( '</fieldset>' );
+
+ # Search
+ $ajaxsearch = $wgAjaxSearch ?
+ $this->addRow(
+ Xml::label( wfMsg( 'useajaxsearch' ), 'wpUseAjaxSearch' ),
+ Xml::check( 'wpUseAjaxSearch', $this->mUseAjaxSearch, array( 'id' => 'wpUseAjaxSearch' ) )
+ ) : '';
+ $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' ) .
+ $ajaxsearch .
+ $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
+ #
+ $wgOut->addHTML('<fieldset><legend>' . wfMsg('prefs-misc') . '</legend>');
+ $wgOut->addHtml( '<label for="wpStubs">' . wfMsg( 'stub-threshold' ) . '</label>&nbsp;' );
+ $wgOut->addHtml( Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) );
+ $msgUnderline = htmlspecialchars( wfMsg ( 'tog-underline' ) );
+ $msgUnderlinenever = htmlspecialchars( wfMsg ( 'underline-never' ) );
+ $msgUnderlinealways = htmlspecialchars( wfMsg ( 'underline-always' ) );
+ $msgUnderlinedefault = htmlspecialchars( wfMsg ( 'underline-default' ) );
+ $uopt = $wgUser->getOption("underline");
+ $s0 = $uopt == 0 ? ' selected="selected"' : '';
+ $s1 = $uopt == 1 ? ' selected="selected"' : '';
+ $s2 = $uopt == 2 ? ' selected="selected"' : '';
+ $wgOut->addHTML("
+<div class='toggle'><p><label for='wpOpunderline'>$msgUnderline</label>
+<select name='wpOpunderline' id='wpOpunderline'>
+<option value=\"0\"$s0>$msgUnderlinenever</option>
+<option value=\"1\"$s1>$msgUnderlinealways</option>
+<option value=\"2\"$s2>$msgUnderlinedefault</option>
+</select></p></div>");
+
+ foreach ( $togs as $tname ) {
+ if( !array_key_exists( $tname, $this->mUsedToggles ) ) {
+ $wgOut->addHTML( $this->getToggle( $tname ) );
+ }
+ }
+ $wgOut->addHTML( '</fieldset>' );
+
+ wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) );
+
+ $token = htmlspecialchars( $wgUser->editToken() );
+ $skin = $wgUser->getSkin();
+ $wgOut->addHTML( "
+ <div id='prefsubmit'>
+ <div>
+ <input type='submit' name='wpSaveprefs' class='btnSavePrefs' value=\"" . wfMsgHtml( 'saveprefs' ) . '"'.$skin->tooltipAndAccesskey('save')." />
+ <input type='submit' name='wpReset' value=\"" . wfMsgHtml( 'resetprefs' ) . "\" />
+ </div>
+
+ </div>
+
+ <input type='hidden' name='wpEditToken' value=\"{$token}\" />
+ </div></form>\n" );
+
+ $wgOut->addHtml( Xml::tags( 'div', array( 'class' => "prefcache" ),
+ wfMsgExt( 'clearyourcache', 'parseinline' ) )
+ );
+ }
+}
diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php
new file mode 100644
index 00000000..9c880349
--- /dev/null
+++ b/includes/specials/SpecialPrefixindex.php
@@ -0,0 +1,152 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point : initialise variables and call subfunctions.
+ * @param $par String: becomes "FOO" when called like Special:Prefixindex/FOO (default NULL)
+ * @param $specialPage SpecialPage object.
+ */
+function wfSpecialPrefixIndex( $par=NULL, $specialPage ) {
+ global $wgRequest, $wgOut, $wgContLang;
+
+ # GET values
+ $from = $wgRequest->getVal( 'from' );
+ $prefix = $wgRequest->getVal( 'prefix' );
+ $namespace = $wgRequest->getInt( 'namespace' );
+ $namespaces = $wgContLang->getNamespaces();
+
+ $indexPage = new SpecialPrefixIndex();
+
+ $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) )
+ ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) )
+ : wfMsg( 'allarticles' )
+ );
+
+ if ( isset($par) ) {
+ $indexPage->showChunk( $namespace, $par, $specialPage->including(), $from );
+ } elseif ( isset($prefix) ) {
+ $indexPage->showChunk( $namespace, $prefix, $specialPage->including(), $from );
+ } elseif ( isset($from) ) {
+ $indexPage->showChunk( $namespace, $from, $specialPage->including(), $from );
+ } else {
+ $wgOut->addHtml($indexPage->namespaceForm ( $namespace, null ));
+ }
+}
+
+/**
+ * implements Special:Prefixindex
+ * @ingroup SpecialPage
+ */
+class SpecialPrefixindex extends SpecialAllpages {
+ // Inherit $maxPerPage
+
+ // Define other properties
+ protected $name = 'Prefixindex';
+ protected $nsfromMsg = 'allpagesprefix';
+
+ /**
+ * @param integer $namespace (Default NS_MAIN)
+ * @param string $from list all pages from this name (default FALSE)
+ */
+ function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = null ) {
+ global $wgOut, $wgUser, $wgContLang;
+
+ $fname = 'indexShowChunk';
+
+ $sk = $wgUser->getSkin();
+
+ if (!isset($from)) $from = $prefix;
+
+ $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+ $prefixList = $this->getNamespaceKeyAndText($namespace, $prefix);
+ $namespaces = $wgContLang->getNamespaces();
+ $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+ if ( !$prefixList || !$fromList ) {
+ $out = wfMsgWikiHtml( 'allpagesbadtitle' );
+ } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
+ // Show errormessage and reset to NS_MAIN
+ $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
+ $namespace = NS_MAIN;
+ } else {
+ list( $namespace, $prefixKey, $prefix ) = $prefixList;
+ list( /* $fromNs */, $fromKey, $from ) = $fromList;
+
+ ### FIXME: should complain if $fromNs != $namespace
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $res = $dbr->select( 'page',
+ array( 'page_namespace', 'page_title', 'page_is_redirect' ),
+ array(
+ 'page_namespace' => $namespace,
+ 'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'',
+ 'page_title >= ' . $dbr->addQuotes( $fromKey ),
+ ),
+ $fname,
+ array(
+ 'ORDER BY' => 'page_title',
+ 'LIMIT' => $this->maxPerPage + 1,
+ 'USE INDEX' => 'name_title',
+ )
+ );
+
+ ### FIXME: side link to previous
+
+ $n = 0;
+ if( $res->numRows() > 0 ) {
+ $out = '<table style="background: inherit;" border="0" width="100%">';
+
+ while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ $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 ) .
+ ($s->page_is_redirect ? '</div>' : '' );
+ } else {
+ $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
+ }
+ if( $n % 3 == 0 ) {
+ $out .= '<tr>';
+ }
+ $out .= "<td>$link</td>";
+ $n++;
+ if( $n % 3 == 0 ) {
+ $out .= '</tr>';
+ }
+ }
+ if( ($n % 3) != 0 ) {
+ $out .= '</tr>';
+ }
+ $out .= '</table>';
+ } else {
+ $out = '';
+ }
+ }
+
+ if ( $including ) {
+ $out2 = '';
+ } else {
+ $nsForm = $this->namespaceForm ( $namespace, $prefix );
+ $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->makeKnownLink( $wgContLang->specialPage( $this->name ),
+ wfMsg ( 'allpages' ) );
+ if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+ $namespaceparam = $namespace ? "&namespace=$namespace" : "";
+ $out2 .= " | " . $sk->makeKnownLink(
+ $wgContLang->specialPage( $this->name ),
+ wfMsgHtml( 'nextpage', htmlspecialchars( $s->page_title ) ),
+ "from=" . wfUrlEncode ( $s->page_title ) .
+ "&prefix=" . wfUrlEncode ( $prefix ) . $namespaceparam );
+ }
+ $out2 .= "</td></tr></table><hr />";
+ }
+
+ $wgOut->addHtml( $out2 . $out );
+ }
+}
diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php
new file mode 100644
index 00000000..3025c055
--- /dev/null
+++ b/includes/specials/SpecialProtectedpages.php
@@ -0,0 +1,309 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ * @ingroup SpecialPage
+ */
+class ProtectedPagesForm {
+
+ protected $IdLevel = 'level';
+ protected $IdType = 'type';
+
+ public function showList( $msg = '' ) {
+ global $wgOut, $wgRequest;
+
+ $wgOut->setPagetitle( wfMsg( "protectedpages" ) );
+ if ( "" != $msg ) {
+ $wgOut->setSubtitle( $msg );
+ }
+
+ // Purge expired entries on one in every 10 queries
+ if ( !mt_rand( 0, 10 ) ) {
+ Title::purgeExpiredRestrictions();
+ }
+
+ $type = $wgRequest->getVal( $this->IdType );
+ $level = $wgRequest->getVal( $this->IdLevel );
+ $sizetype = $wgRequest->getVal( 'sizetype' );
+ $size = $wgRequest->getIntOrNull( 'size' );
+ $NS = $wgRequest->getIntOrNull( 'namespace' );
+ $indefOnly = $wgRequest->getBool( 'indefonly' ) ? 1 : 0;
+
+ $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size, $indefOnly );
+
+ $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly ) );
+
+ if ( $pager->getNumRows() ) {
+ $s = $pager->getNavigationBar();
+ $s .= "<ul>" .
+ $pager->getBody() .
+ "</ul>";
+ $s .= $pager->getNavigationBar();
+ } else {
+ $s = '<p>' . wfMsgHtml( 'protectedpagesempty' ) . '</p>';
+ }
+ $wgOut->addHTML( $s );
+ }
+
+ /**
+ * Callback function to output a restriction
+ * @param $row object Protected title
+ * @return string Formatted <li> element
+ */
+ public function formatRow( $row ) {
+ global $wgUser, $wgLang, $wgContLang;
+
+ wfProfileIn( __METHOD__ );
+
+ static $skin=null;
+
+ if( is_null( $skin ) )
+ $skin = $wgUser->getSkin();
+
+ $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+ $link = $skin->makeLinkObj( $title );
+
+ $description_items = array ();
+
+ $protType = wfMsgHtml( 'restriction-level-' . $row->pr_level );
+
+ $description_items[] = $protType;
+
+ if ( $row->pr_cascade ) {
+ $description_items[] = wfMsg( 'protect-summary-cascade' );
+ }
+
+ $expiry_description = '';
+ $stxt = '';
+
+ if ( $row->pr_expiry != 'infinity' && strlen($row->pr_expiry) ) {
+ $expiry = Block::decodeExpiry( $row->pr_expiry );
+
+ $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
+
+ $description_items[] = $expiry_description;
+ }
+
+ if (!is_null($size = $row->page_len)) {
+ $stxt = $wgContLang->getDirMark() . ' ' . $skin->formatRevisionSize( $size );
+ }
+
+ # 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' ) . ')';
+ } else {
+ $ltitle = SpecialPage::getTitleFor( 'Log' );
+ $changeProtection = ' (' . $skin->makeKnownLinkObj( $ltitle, wfMsgHtml( 'protectlogpage' ), 'type=protect&page=' . $title->getPrefixedUrl() ) . ')';
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . $changeProtection . "</li>\n";
+ }
+
+ /**
+ * @param $namespace int
+ * @param $type string
+ * @param $level string
+ * @param $minsize int
+ * @param $indefOnly bool
+ * @return string Input form
+ * @private
+ */
+ protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly ) {
+ global $wgScript;
+ $title = SpecialPage::getTitleFor( 'ProtectedPages' );
+ return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) .
+ Xml::hidden( 'title', $title->getPrefixedDBkey() ) . "&nbsp;\n" .
+ $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
+ $this->getTypeMenu( $type ) . "&nbsp;\n" .
+ $this->getLevelMenu( $level ) . "&nbsp;\n" .
+ "<br /><span style='white-space: nowrap'>&nbsp;&nbsp;" .
+ $this->getExpiryCheck( $indefOnly ) . "&nbsp;\n" .
+ $this->getSizeLimit( $sizetype, $size ) . "&nbsp;\n" .
+ "</span>" .
+ "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' );
+ }
+
+ /**
+ * Prepare the namespace filter drop-down; standard namespace
+ * selector, sans the MediaWiki namespace
+ *
+ * @param mixed $namespace Pre-select namespace
+ * @return string
+ */
+ protected function getNamespaceMenu( $namespace = null ) {
+ return "<span style='white-space: nowrap'>" .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;'
+ . Xml::namespaceSelector( $namespace, '' ) . "</span>";
+ }
+
+ /**
+ * @return string Formatted HTML
+ */
+ protected function getExpiryCheck( $indefOnly ) {
+ return
+ Xml::checkLabel( wfMsg('protectedpages-indef'), 'indefonly', 'indefonly', $indefOnly ) . "\n";
+ }
+
+ /**
+ * @return string Formatted HTML
+ */
+ protected function getSizeLimit( $sizetype, $size ) {
+ $max = $sizetype === 'max';
+
+ return
+ Xml::radioLabel( wfMsg('minimum-size'), 'sizetype', 'min', 'wpmin', !$max ) .
+ '&nbsp;' .
+ Xml::radioLabel( wfMsg('maximum-size'), 'sizetype', 'max', 'wpmax', $max ) .
+ '&nbsp;' .
+ Xml::input( 'size', 9, $size, array( 'id' => 'wpsize' ) ) .
+ '&nbsp;' .
+ Xml::label( wfMsg('pagesize'), 'wpsize' );
+ }
+
+ /**
+ * @return string Formatted HTML
+ */
+ protected function getTypeMenu( $pr_type ) {
+ global $wgRestrictionTypes;
+
+ $m = array(); // Temporary array
+ $options = array();
+
+ // First pass to load the log names
+ foreach( $wgRestrictionTypes as $type ) {
+ $text = wfMsg("restriction-$type");
+ $m[$text] = $type;
+ }
+
+ // Third pass generates sorted XHTML content
+ foreach( $m as $text => $type ) {
+ $selected = ($type == $pr_type );
+ $options[] = Xml::option( $text, $type, $selected ) . "\n";
+ }
+
+ return "<span style='white-space: nowrap'>" .
+ Xml::label( wfMsg('restriction-type') , $this->IdType ) . '&nbsp;' .
+ Xml::tags( 'select',
+ array( 'id' => $this->IdType, 'name' => $this->IdType ),
+ implode( "\n", $options ) ) . "</span>";
+ }
+
+ /**
+ * @return string Formatted HTML
+ */
+ protected function getLevelMenu( $pr_level ) {
+ global $wgRestrictionLevels;
+
+ $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+ $options = array();
+
+ // First pass to load the log names
+ foreach( $wgRestrictionLevels as $type ) {
+ if ( $type !='' && $type !='*') {
+ $text = wfMsg("restriction-level-$type");
+ $m[$text] = $type;
+ }
+ }
+
+ // Third pass generates sorted XHTML content
+ foreach( $m as $text => $type ) {
+ $selected = ($type == $pr_level );
+ $options[] = Xml::option( $text, $type, $selected );
+ }
+
+ return
+ Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
+ Xml::tags( 'select',
+ array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
+ implode( "\n", $options ) );
+ }
+}
+
+/**
+ * @todo document
+ * @ingroup Pager
+ */
+class ProtectedPagesPager extends AlphabeticPager {
+ public $mForm, $mConds;
+ private $type, $level, $namespace, $sizetype, $size, $indefonly;
+
+ function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0, $indefonly=false ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ $this->type = ( $type ) ? $type : 'edit';
+ $this->level = $level;
+ $this->namespace = $namespace;
+ $this->sizetype = $sizetype;
+ $this->size = intval($size);
+ $this->indefonly = (bool)$indefonly;
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $lb = new LinkBatch;
+ while( $row = $this->mResult->fetchObject() ) {
+ $lb->add( $row->page_namespace, $row->page_title );
+ }
+ $lb->execute();
+
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ return $this->mForm->formatRow( $row );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds[] = 'pr_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+ $conds[] = 'page_id=pr_page';
+ $conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type );
+
+ if( $this->sizetype=='min' ) {
+ $conds[] = 'page_len>=' . $this->size;
+ } else if( $this->sizetype=='max' ) {
+ $conds[] = 'page_len<=' . $this->size;
+ }
+
+ if( $this->indefonly ) {
+ $conds[] = "pr_expiry = 'infinity' OR pr_expiry IS NULL";
+ }
+
+ if( $this->level )
+ $conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level );
+ if( !is_null($this->namespace) )
+ $conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace );
+ return array(
+ 'tables' => array( 'page_restrictions', 'page' ),
+ 'fields' => 'pr_id,page_namespace,page_title,page_len,pr_type,pr_level,pr_expiry,pr_cascade',
+ 'conds' => $conds
+ );
+ }
+
+ function getIndexField() {
+ return 'pr_id';
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialProtectedpages() {
+
+ $ppForm = new ProtectedPagesForm();
+
+ $ppForm->showList();
+}
diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php
new file mode 100644
index 00000000..2ec68a66
--- /dev/null
+++ b/includes/specials/SpecialProtectedtitles.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ * @ingroup SpecialPage
+ */
+class ProtectedTitlesForm {
+
+ protected $IdLevel = 'level';
+ protected $IdType = 'type';
+
+ function showList( $msg = '' ) {
+ global $wgOut, $wgRequest;
+
+ $wgOut->setPagetitle( wfMsg( "protectedtitles" ) );
+ if ( "" != $msg ) {
+ $wgOut->setSubtitle( $msg );
+ }
+
+ // Purge expired entries on one in every 10 queries
+ if ( !mt_rand( 0, 10 ) ) {
+ Title::purgeExpiredRestrictions();
+ }
+
+ $type = $wgRequest->getVal( $this->IdType );
+ $level = $wgRequest->getVal( $this->IdLevel );
+ $sizetype = $wgRequest->getVal( 'sizetype' );
+ $size = $wgRequest->getIntOrNull( 'size' );
+ $NS = $wgRequest->getIntOrNull( 'namespace' );
+
+ $pager = new ProtectedTitlesPager( $this, array(), $type, $level, $NS, $sizetype, $size );
+
+ $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size ) );
+
+ if ( $pager->getNumRows() ) {
+ $s = $pager->getNavigationBar();
+ $s .= "<ul>" .
+ $pager->getBody() .
+ "</ul>";
+ $s .= $pager->getNavigationBar();
+ } else {
+ $s = '<p>' . wfMsgHtml( 'protectedtitlesempty' ) . '</p>';
+ }
+ $wgOut->addHTML( $s );
+ }
+
+ /**
+ * Callback function to output a restriction
+ */
+ function formatRow( $row ) {
+ global $wgUser, $wgLang, $wgContLang;
+
+ wfProfileIn( __METHOD__ );
+
+ static $skin=null;
+
+ if( is_null( $skin ) )
+ $skin = $wgUser->getSkin();
+
+ $title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title );
+ $link = $skin->makeLinkObj( $title );
+
+ $description_items = array ();
+
+ $protType = wfMsgHtml( 'restriction-level-' . $row->pt_create_perm );
+
+ $description_items[] = $protType;
+
+ $expiry_description = ''; $stxt = '';
+
+ if ( $row->pt_expiry != 'infinity' && strlen($row->pt_expiry) ) {
+ $expiry = Block::decodeExpiry( $row->pt_expiry );
+
+ $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
+
+ $description_items[] = $expiry_description;
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . "</li>\n";
+ }
+
+ /**
+ * @param $namespace int
+ * @param $type string
+ * @param $level string
+ * @param $minsize int
+ * @private
+ */
+ function showOptions( $namespace, $type='edit', $level, $sizetype, $size ) {
+ global $wgScript;
+ $action = htmlspecialchars( $wgScript );
+ $title = SpecialPage::getTitleFor( 'ProtectedTitles' );
+ $special = htmlspecialchars( $title->getPrefixedDBkey() );
+ return "<form action=\"$action\" method=\"get\">\n" .
+ '<fieldset>' .
+ Xml::element( 'legend', array(), wfMsg( 'protectedtitles' ) ) .
+ Xml::hidden( 'title', $special ) . "&nbsp;\n" .
+ $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
+ // $this->getLevelMenu( $level ) . "<br/>\n" .
+ "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ "</fieldset></form>";
+ }
+
+ /**
+ * Prepare the namespace filter drop-down; standard namespace
+ * selector, sans the MediaWiki namespace
+ *
+ * @param mixed $namespace Pre-select namespace
+ * @return string
+ */
+ function getNamespaceMenu( $namespace = null ) {
+ return Xml::label( wfMsg( 'namespace' ), 'namespace' )
+ . '&nbsp;'
+ . Xml::namespaceSelector( $namespace, '' );
+ }
+
+ /**
+ * @return string Formatted HTML
+ * @private
+ */
+ function getLevelMenu( $pr_level ) {
+ global $wgRestrictionLevels;
+
+ $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+ $options = array();
+
+ // First pass to load the log names
+ foreach( $wgRestrictionLevels as $type ) {
+ if ( $type !='' && $type !='*') {
+ $text = wfMsg("restriction-level-$type");
+ $m[$text] = $type;
+ }
+ }
+
+ // Third pass generates sorted XHTML content
+ foreach( $m as $text => $type ) {
+ $selected = ($type == $pr_level );
+ $options[] = Xml::option( $text, $type, $selected );
+ }
+
+ return
+ Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
+ Xml::tags( 'select',
+ array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
+ implode( "\n", $options ) );
+ }
+}
+
+/**
+ * @todo document
+ * @ingroup Pager
+ */
+class ProtectedtitlesPager extends AlphabeticPager {
+ public $mForm, $mConds;
+
+ function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0 ) {
+ $this->mForm = $form;
+ $this->mConds = $conds;
+ $this->level = $level;
+ $this->namespace = $namespace;
+ $this->size = intval($size);
+ parent::__construct();
+ }
+
+ function getStartBody() {
+ wfProfileIn( __METHOD__ );
+ # Do a link batch query
+ $this->mResult->seek( 0 );
+ $lb = new LinkBatch;
+
+ while ( $row = $this->mResult->fetchObject() ) {
+ $lb->add( $row->pt_namespace, $row->pt_title );
+ }
+
+ $lb->execute();
+ wfProfileOut( __METHOD__ );
+ return '';
+ }
+
+ function formatRow( $row ) {
+ return $this->mForm->formatRow( $row );
+ }
+
+ function getQueryInfo() {
+ $conds = $this->mConds;
+ $conds[] = 'pt_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+
+ if( !is_null($this->namespace) )
+ $conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace );
+ return array(
+ 'tables' => 'protected_titles',
+ 'fields' => 'pt_namespace,pt_title,pt_create_perm,pt_expiry,pt_timestamp',
+ 'conds' => $conds
+ );
+ }
+
+ function getIndexField() {
+ return 'pt_timestamp';
+ }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialProtectedtitles() {
+
+ $ppForm = new ProtectedTitlesForm();
+
+ $ppForm->showList();
+}
diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php
new file mode 100644
index 00000000..0e7ada1d
--- /dev/null
+++ b/includes/specials/SpecialRandompage.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * Special page to direct the user to a random page
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>, Ilmari Karonen
+ * @license GNU General Public Licence 2.0 or later
+ */
+class RandomPage extends SpecialPage {
+ private $namespace = NS_MAIN; // namespace to select pages from
+
+ function __construct( $name = 'Randompage' ){
+ parent::__construct( $name );
+ }
+
+ public function getNamespace() {
+ return $this->namespace;
+ }
+
+ public function setNamespace ( $ns ) {
+ if( $ns < NS_MAIN ) $ns = NS_MAIN;
+ $this->namespace = $ns;
+ }
+
+ // select redirects instead of normal pages?
+ // Overriden by SpecialRandomredirect
+ public function isRedirect(){
+ return false;
+ }
+
+ public function execute( $par ) {
+ global $wgOut, $wgContLang;
+
+ if ($par)
+ $this->setNamespace( $wgContLang->getNsIndex( $par ) );
+
+ $title = $this->getRandomTitle();
+
+ if( is_null( $title ) ) {
+ $this->setHeaders();
+ $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages' );
+ return;
+ }
+
+ $query = $this->isRedirect() ? 'redirect=no' : '';
+ $wgOut->redirect( $title->getFullUrl( $query ) );
+ }
+
+
+ /**
+ * Choose a random title.
+ * @return Title object (or null if nothing to choose from)
+ */
+ public function getRandomTitle() {
+ $randstr = wfRandom();
+ $row = $this->selectRandomPageFromDB( $randstr );
+
+ /* If we picked a value that was higher than any in
+ * the DB, wrap around and select the page with the
+ * lowest value instead! One might think this would
+ * skew the distribution, but in fact it won't cause
+ * any more bias than what the page_random scheme
+ * causes anyway. Trust me, I'm a mathematician. :)
+ */
+ if( !$row )
+ $row = $this->selectRandomPageFromDB( "0" );
+
+ if( $row )
+ return Title::makeTitleSafe( $this->namespace, $row->page_title );
+ else
+ return null;
+ }
+
+ private function selectRandomPageFromDB( $randstr ) {
+ global $wgExtraRandompageSQL;
+ $fname = 'RandomPage::selectRandomPageFromDB';
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $use_index = $dbr->useIndexClause( 'page_random' );
+ $page = $dbr->tableName( 'page' );
+
+ $ns = (int) $this->namespace;
+ $redirect = $this->isRedirect() ? 1 : 0;
+
+ $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : "";
+ $sql = "SELECT page_title
+ FROM $page $use_index
+ WHERE page_namespace = $ns
+ AND page_is_redirect = $redirect
+ AND page_random >= $randstr
+ $extra
+ ORDER BY page_random";
+
+ $sql = $dbr->limitResult( $sql, 1, 0 );
+ $res = $dbr->query( $sql, $fname );
+ return $dbr->fetchObject( $res );
+ }
+}
diff --git a/includes/specials/SpecialRandomredirect.php b/includes/specials/SpecialRandomredirect.php
new file mode 100644
index 00000000..629d5b3c
--- /dev/null
+++ b/includes/specials/SpecialRandomredirect.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * Special page to direct the user to a random redirect page (minus the second redirect)
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>, Ilmari Karonen
+ * @license GNU General Public Licence 2.0 or later
+ */
+class SpecialRandomredirect extends RandomPage {
+ function __construct(){
+ parent::__construct( 'Randomredirect' );
+ }
+
+ // Override parent::isRedirect()
+ public function isRedirect(){
+ return true;
+ }
+}
diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php
new file mode 100644
index 00000000..cb718bdc
--- /dev/null
+++ b/includes/specials/SpecialRecentchanges.php
@@ -0,0 +1,662 @@
+<?php
+
+/**
+ * Implements Special:Recentchanges
+ * @ingroup SpecialPage
+ */
+class SpecialRecentChanges extends SpecialPage {
+ public function __construct() {
+ SpecialPage::SpecialPage( 'Recentchanges' );
+ $this->includable( true );
+ }
+
+ /**
+ * Get a FormOptions object containing the default options
+ *
+ * @return FormOptions
+ */
+ public function getDefaultOptions() {
+ $opts = new FormOptions();
+
+ $opts->add( 'days', (int)User::getDefaultOption( 'rcdays' ) );
+ $opts->add( 'limit', (int)User::getDefaultOption( 'rclimit' ) );
+ $opts->add( 'from', '' );
+
+ $opts->add( 'hideminor', false );
+ $opts->add( 'hidebots', true );
+ $opts->add( 'hideanons', false );
+ $opts->add( 'hideliu', false );
+ $opts->add( 'hidepatrolled', false );
+ $opts->add( 'hidemyself', false );
+
+ $opts->add( 'namespace', '', FormOptions::INTNULL );
+ $opts->add( 'invert', false );
+
+ $opts->add( 'categories', '' );
+ $opts->add( 'categories_any', false );
+
+ return $opts;
+ }
+
+ /**
+ * Get a FormOptions object with options as specified by the user
+ *
+ * @return FormOptions
+ */
+ public function setup( $parameters ) {
+ global $wgUser, $wgRequest;
+
+ $opts = $this->getDefaultOptions();
+ $opts['days'] = (int)$wgUser->getOption( 'rcdays', $opts['days'] );
+ $opts['limit'] = (int)$wgUser->getOption( 'rclimit', $opts['limit'] );
+ $opts['hideminor'] = $wgUser->getOption( 'hideminor', $opts['hideminor'] );
+ $opts->fetchValuesFromRequest( $wgRequest );
+
+ // Give precedence to subpage syntax
+ if ( $parameters !== null ) {
+ $this->parseParameters( $parameters, $opts );
+ }
+
+ $opts->validateIntBounds( 'limit', 0, 5000 );
+ return $opts;
+ }
+
+ /**
+ * Get a FormOptions object sepcific for feed requests
+ *
+ * @return FormOptions
+ */
+ public function feedSetup() {
+ global $wgFeedLimit, $wgRequest;
+ $opts = $this->getDefaultOptions();
+ $opts->fetchValuesFromRequest( $wgRequest, array( 'days', 'limit', 'hideminor' ) );
+ $opts->validateIntBounds( 'limit', 0, $wgFeedLimit );
+ return $opts;
+ }
+
+ /**
+ * Main execution point
+ *
+ * @param $parameters string
+ */
+ public function execute( $parameters ) {
+ global $wgRequest, $wgOut;
+ $feedFormat = $wgRequest->getVal( 'feed' );
+
+ # 10 seconds server-side caching max
+ $wgOut->setSquidMaxage( 10 );
+
+ $lastmod = $this->checkLastModified( $feedFormat );
+ if( $lastmod === false ){
+ return;
+ }
+
+ $opts = $feedFormat ? $this->feedSetup() : $this->setup( $parameters );
+ $this->setHeaders();
+ $this->outputHeader();
+
+ // Fetch results, prepare a batch link existence check query
+ $rows = array();
+ $batch = new LinkBatch;
+ $conds = $this->buildMainQueryConds( $opts );
+ $res = $this->doMainQuery( $conds, $opts );
+ if( $res === false ){
+ $this->doHeader( $opts );
+ return;
+ }
+ $dbr = wfGetDB( DB_SLAVE );
+ while( $row = $dbr->fetchObject( $res ) ){
+ $rows[] = $row;
+ if ( !$feedFormat ) {
+ // User page and talk links
+ $batch->add( NS_USER, $row->rc_user_text );
+ $batch->add( NS_USER_TALK, $row->rc_user_text );
+ }
+
+ }
+ $dbr->freeResult( $res );
+
+ if ( $feedFormat ) {
+ list( $feed, $feedObj ) = $this->getFeedObject( $feedFormat );
+ $feed->execute( $feedObj, $rows, $opts['limit'], $opts['hideminor'], $lastmod );
+ } else {
+ $batch->execute();
+ $this->webOutput( $rows, $opts );
+ }
+
+ }
+
+ /**
+ * Return an array with a ChangesFeed object and ChannelFeed object
+ *
+ * @return array
+ */
+ public function getFeedObject( $feedFormat ){
+ $feed = new ChangesFeed( $feedFormat, 'rcfeed' );
+ $feedObj = $feed->getFeedObject(
+ wfMsgForContent( 'recentchanges' ),
+ wfMsgForContent( 'recentchanges-feed-description' )
+ );
+ return array( $feed, $feedObj );
+ }
+
+ /**
+ * Process $par and put options found if $opts
+ * Mainly used when including the page
+ *
+ * @param $par String
+ * @param $opts FormOptions
+ */
+ public function parseParameters( $par, FormOptions $opts ) {
+ $bits = preg_split( '/\s*,\s*/', trim( $par ) );
+ foreach ( $bits as $bit ) {
+ if ( 'hidebots' === $bit ) $opts['hidebots'] = true;
+ if ( 'bots' === $bit ) $opts['hidebots'] = false;
+ if ( 'hideminor' === $bit ) $opts['hideminor'] = true;
+ if ( 'minor' === $bit ) $opts['hideminor'] = false;
+ if ( 'hideliu' === $bit ) $opts['hideliu'] = true;
+ if ( 'hidepatrolled' === $bit ) $opts['hidepatrolled'] = true;
+ if ( 'hideanons' === $bit ) $opts['hideanons'] = true;
+ if ( 'hidemyself' === $bit ) $opts['hidemyself'] = true;
+
+ if ( is_numeric( $bit ) ) $opts['limit'] = $bit;
+
+ $m = array();
+ if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) $opts['limit'] = $m[1];
+ if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) $opts['days'] = $m[1];
+ }
+ }
+
+ /**
+ * Get last modified date, for client caching
+ * Don't use this if we are using the patrol feature, patrol changes don't
+ * update the timestamp
+ *
+ * @param $feedFormat String
+ * @return int or false
+ */
+ public function checkLastModified( $feedFormat ) {
+ global $wgUseRCPatrol, $wgOut;
+ $dbr = wfGetDB( DB_SLAVE );
+ $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __FUNCTION__ );
+ if ( $feedFormat || !$wgUseRCPatrol ) {
+ if( $lastmod && $wgOut->checkLastModified( $lastmod ) ){
+ # Client cache fresh and headers sent, nothing more to do.
+ return false;
+ }
+ }
+ return $lastmod;
+ }
+
+ /**
+ * Return an array of conditions depending of options set in $opts
+ *
+ * @param $opts FormOptions
+ * @return array
+ */
+ public function buildMainQueryConds( FormOptions $opts ) {
+ global $wgUser;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $conds = array();
+
+ # It makes no sense to hide both anons and logged-in users
+ # Where this occurs, force anons to be shown
+ $forcebot = false;
+ if( $opts['hideanons'] && $opts['hideliu'] ){
+ # Check if the user wants to show bots only
+ if( $opts['hidebots'] ){
+ $opts['hideanons'] = false;
+ } else {
+ $forcebot = true;
+ $opts['hidebots'] = false;
+ }
+ }
+
+ // Calculate cutoff
+ $cutoff_unixtime = time() - ( $opts['days'] * 86400 );
+ $cutoff_unixtime = $cutoff_unixtime - ($cutoff_unixtime % 86400);
+ $cutoff = $dbr->timestamp( $cutoff_unixtime );
+
+ $fromValid = preg_match('/^[0-9]{14}$/', $opts['from']);
+ if( $fromValid && $opts['from'] > wfTimestamp(TS_MW,$cutoff) ) {
+ $cutoff = $dbr->timestamp($opts['from']);
+ } else {
+ $opts->reset( 'from' );
+ }
+
+ $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
+
+
+ $hidePatrol = $wgUser->useRCPatrol() && $opts['hidepatrolled'];
+ $hideLoggedInUsers = $opts['hideliu'] && !$forcebot;
+ $hideAnonymousUsers = $opts['hideanons'] && !$forcebot;
+
+ if ( $opts['hideminor'] ) $conds['rc_minor'] = 0;
+ if ( $opts['hidebots'] ) $conds['rc_bot'] = 0;
+ if ( $hidePatrol ) $conds['rc_patrolled'] = 0;
+ if ( $forcebot ) $conds['rc_bot'] = 1;
+ if ( $hideLoggedInUsers ) $conds[] = 'rc_user = 0';
+ if ( $hideAnonymousUsers ) $conds[] = 'rc_user != 0';
+
+ if( $opts['hidemyself'] ) {
+ if( $wgUser->getId() ) {
+ $conds[] = 'rc_user != ' . $dbr->addQuotes( $wgUser->getId() );
+ } else {
+ $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $wgUser->getName() );
+ }
+ }
+
+ # Namespace filtering
+ if ( $opts['namespace'] !== '' ) {
+ if ( !$opts['invert'] ) {
+ $conds[] = 'rc_namespace = ' . $dbr->addQuotes( $opts['namespace'] );
+ } else {
+ $conds[] = 'rc_namespace != ' . $dbr->addQuotes( $opts['namespace'] );
+ }
+ }
+
+ return $conds;
+ }
+
+ /**
+ * Process the query
+ *
+ * @param $conds array
+ * @param $opts FormOptions
+ * @return database result or false (for Recentchangeslinked only)
+ */
+ public function doMainQuery( $conds, $opts ) {
+ global $wgUser;
+
+ $tables = array( 'recentchanges' );
+ $join_conds = array();
+
+ $uid = $wgUser->getId();
+ $dbr = wfGetDB( DB_SLAVE );
+ $limit = $opts['limit'];
+ $namespace = $opts['namespace'];
+ $invert = $opts['invert'];
+
+ // JOIN on watchlist for users
+ if( $uid ) {
+ $tables[] = 'watchlist';
+ $join_conds = array( 'watchlist' => array('LEFT JOIN',"wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace") );
+ }
+
+ wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) );
+
+ // Is there either one namespace selected or excluded?
+ // Also, if this is "all" or main namespace, just use timestamp index.
+ if( is_null($namespace) || $invert || $namespace == NS_MAIN ) {
+ $res = $dbr->select( $tables, '*', $conds, __METHOD__,
+ array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit,
+ 'USE INDEX' => array('recentchanges' => 'rc_timestamp') ),
+ $join_conds );
+ // We have a new_namespace_time index! UNION over new=(0,1) and sort result set!
+ } else {
+ // New pages
+ $sqlNew = $dbr->selectSQLText( $tables, '*',
+ array( 'rc_new' => 1 ) + $conds,
+ __METHOD__,
+ array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit,
+ 'USE INDEX' => array('recentchanges' => 'new_name_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') ),
+ $join_conds );
+ # Join the two fast queries, and sort the result set
+ $sql = "($sqlNew) UNION ($sqlOld) ORDER BY rc_timestamp DESC LIMIT $limit";
+ $res = $dbr->query( $sql, __METHOD__ );
+ }
+
+ return $res;
+ }
+
+ /**
+ * Send output to $wgOut, only called if not used feeds
+ *
+ * @param $rows array of database rows
+ * @param $opts FormOptions
+ */
+ public function webOutput( $rows, $opts ) {
+ global $wgOut, $wgUser, $wgRCShowWatchingUsers, $wgShowUpdatedMarker;
+ global $wgAllowCategorizedRecentChanges;
+
+ $limit = $opts['limit'];
+
+ if ( !$this->including() ) {
+ // Output options box
+ $this->doHeader( $opts );
+ }
+
+ // And now for the content
+ $wgOut->setSyndicated( true );
+
+ $list = ChangesList::newFromUser( $wgUser );
+
+ if ( $wgAllowCategorizedRecentChanges ) {
+ $this->filterByCategories( $rows, $opts );
+ }
+
+ $s = $list->beginRecentChangesList();
+ $counter = 1;
+
+ $showWatcherCount = $wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' );
+ $watcherCache = array();
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ foreach( $rows as $obj ){
+ if( $limit == 0) {
+ break;
+ }
+
+ if ( ! ( $opts['hideminor'] && $obj->rc_minor ) &&
+ ! ( $opts['hidepatrolled'] && $obj->rc_patrolled ) ) {
+ $rc = RecentChange::newFromRow( $obj );
+ $rc->counter = $counter++;
+
+ if ($wgShowUpdatedMarker
+ && !empty( $obj->wl_notificationtimestamp )
+ && ($obj->rc_timestamp >= $obj->wl_notificationtimestamp)) {
+ $rc->notificationtimestamp = true;
+ } else {
+ $rc->notificationtimestamp = false;
+ }
+
+ $rc->numberofWatchingusers = 0; // Default
+ if ($showWatcherCount && $obj->rc_namespace >= 0) {
+ if (!isset($watcherCache[$obj->rc_namespace][$obj->rc_title])) {
+ $watcherCache[$obj->rc_namespace][$obj->rc_title] =
+ $dbr->selectField( 'watchlist',
+ 'COUNT(*)',
+ array(
+ 'wl_namespace' => $obj->rc_namespace,
+ 'wl_title' => $obj->rc_title,
+ ),
+ __METHOD__ . '-watchers' );
+ }
+ $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
+ }
+ $s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ) );
+ --$limit;
+ }
+ }
+ $s .= $list->endRecentChangesList();
+ $wgOut->addHTML( $s );
+ }
+
+ /**
+ * Return the text to be displayed above the changes
+ *
+ * @param $opts FormOptions
+ * @return String: XHTML
+ */
+ public function doHeader( $opts ) {
+ global $wgScript, $wgOut;
+
+ $this->setTopText( $wgOut, $opts );
+
+ $defaults = $opts->getAllValues();
+ $nondefaults = $opts->getChangedValues();
+ $opts->consumeValues( array( 'namespace', 'invert' ) );
+
+ $panel = array();
+ $panel[] = $this->optionsPanel( $defaults, $nondefaults );
+ $panel[] = '<hr />';
+
+ $extraOpts = $this->getExtraOptions( $opts );
+
+ $out = Xml::openElement( 'table' );
+ foreach ( $extraOpts as $optionRow ) {
+ $out .= Xml::openElement( 'tr' );
+ if ( is_array($optionRow) ) {
+ $out .= Xml::tags( 'td', null, $optionRow[0] );
+ $out .= Xml::tags( 'td', null, $optionRow[1] );
+ } else {
+ $out .= Xml::tags( 'td', array( 'colspan' => 2 ), $optionRow );
+ }
+ $out .= Xml::closeElement( 'tr' );
+ }
+ $out .= Xml::closeElement( 'table' );
+
+ $unconsumed = $opts->getUnconsumedValues();
+ foreach ( $unconsumed as $key => $value ) {
+ $out .= Xml::hidden( $key, $value );
+ }
+
+ $t = $this->getTitle();
+ $out .= Xml::hidden( 'title', $t->getPrefixedText() );
+ $form = Xml::tags( 'form', array( 'action' => $wgScript ), $out );
+ $panel[] = $form;
+ $panelString = implode( "\n", $panel );
+
+ $wgOut->addHTML(
+ Xml::fieldset( wfMsg( strtolower( $this->mName ) ), $panelString, array( 'class' => 'rcoptions' ) )
+ );
+
+ $this->setBottomText( $wgOut, $opts );
+ }
+
+ /**
+ * Get options to be displayed in a form
+ *
+ * @param $opts FormOptions
+ * @return array
+ */
+ function getExtraOptions( $opts ){
+ $extraOpts = array();
+ $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
+
+ global $wgAllowCategorizedRecentChanges;
+ if ( $wgAllowCategorizedRecentChanges ) {
+ $extraOpts['category'] = $this->categoryFilterForm( $opts );
+ }
+
+ wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) );
+ $extraOpts['submit'] = Xml::submitbutton( wfMsg('allpagessubmit') );
+ return $extraOpts;
+ }
+
+ /**
+ * Send the text to be displayed above the options
+ *
+ * @param $out OutputPage
+ * @param $opts FormOptions
+ */
+ function setTopText( &$out, $opts ){
+ $out->addWikiText( wfMsgForContentNoTrans( 'recentchangestext' ) );
+ }
+
+ /**
+ * Send the text to be displayed after the options, for use in
+ * Recentchangeslinked
+ *
+ * @param $out OutputPage
+ * @param $opts FormOptions
+ */
+ function setBottomText( &$out, $opts ){}
+
+ /**
+ * Creates the choose namespace selection
+ *
+ * @param $opts FormOptions
+ * @return string
+ */
+ protected function namespaceFilterForm( FormOptions $opts ) {
+ $nsSelect = HTMLnamespaceselector( $opts['namespace'], '' );
+ $nsLabel = Xml::label( wfMsg('namespace'), 'namespace' );
+ $invert = Xml::checkLabel( wfMsg('invert'), 'invert', 'nsinvert', $opts['invert'] );
+ return array( $nsLabel, "$nsSelect $invert" );
+ }
+
+ /**
+ * Create a input to filter changes by categories
+ *
+ * @param $opts FormOptions
+ * @return array
+ */
+ protected function categoryFilterForm( FormOptions $opts ) {
+ list( $label, $input ) = Xml::inputLabelSep( wfMsg('rc_categories'),
+ 'categories', 'mw-categories', false, $opts['categories'] );
+
+ $input .= ' ' . Xml::checkLabel( wfMsg('rc_categories_any'),
+ 'categories_any', 'mw-categories_any', $opts['categories_any'] );
+
+ return array( $label, $input );
+ }
+
+ /**
+ * Filter $rows by categories set in $opts
+ *
+ * @param $rows array of database rows
+ * @param $opts FormOptions
+ */
+ function filterByCategories( &$rows, FormOptions $opts ) {
+ $categories = array_map( 'trim', explode( "|" , $opts['categories'] ) );
+
+ if( empty($categories) ) {
+ return;
+ }
+
+ # Filter categories
+ $cats = array();
+ foreach ( $categories as $cat ) {
+ $cat = trim( $cat );
+ if ( $cat == "" ) continue;
+ $cats[] = $cat;
+ }
+
+ # Filter articles
+ $articles = array();
+ $a2r = array();
+ foreach ( $rows AS $k => $r ) {
+ $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
+ $id = $nt->getArticleID();
+ if ( $id == 0 ) continue; # Page might have been deleted...
+ if ( !in_array($id, $articles) ) {
+ $articles[] = $id;
+ }
+ if ( !isset($a2r[$id]) ) {
+ $a2r[$id] = array();
+ }
+ $a2r[$id][] = $k;
+ }
+
+ # Shortcut?
+ if ( !count($articles) || !count($cats) )
+ return ;
+
+ # Look up
+ $c = new Categoryfinder ;
+ $c->seed( $articles, $cats, $opts['categories_any'] ? "OR" : "AND" ) ;
+ $match = $c->run();
+
+ # Filter
+ $newrows = array();
+ foreach ( $match AS $id ) {
+ foreach ( $a2r[$id] AS $rev ) {
+ $k = $rev;
+ $newrows[$k] = $rows[$k];
+ }
+ }
+ $rows = $newrows;
+ }
+
+ /**
+ * Makes change an option link which carries all the other options
+ * @param $title see Title
+ * @param $override
+ * @param $options
+ */
+ function makeOptionsLink( $title, $override, $options, $active = false ) {
+ global $wgUser;
+ $sk = $wgUser->getSkin();
+ return $sk->makeKnownLinkObj( $this->getTitle(), htmlspecialchars( $title ),
+ wfArrayToCGI( $override, $options ), '', '', $active ? 'style="font-weight: bold;"' : '' );
+ }
+
+ /**
+ * Creates the options panel.
+ * @param $defaults array
+ * @param $nondefaults array
+ */
+ function optionsPanel( $defaults, $nondefaults ) {
+ global $wgLang, $wgUser, $wgRCLinkLimits, $wgRCLinkDays;
+
+ $options = $nondefaults + $defaults;
+
+ if( $options['from'] )
+ $note = wfMsgExt( 'rcnotefrom', array( 'parseinline' ),
+ $wgLang->formatNum( $options['limit'] ),
+ $wgLang->timeanddate( $options['from'], true ) );
+ else
+ $note = wfMsgExt( 'rcnote', array( 'parseinline' ),
+ $wgLang->formatNum( $options['limit'] ),
+ $wgLang->formatNum( $options['days'] ),
+ $wgLang->timeAndDate( wfTimestampNow(), true ),
+ $wgLang->date( wfTimestampNow(), true ),
+ $wgLang->time( wfTimestampNow(), true ) );
+
+ # Sort data for display and make sure it's unique after we've added user data.
+ $wgRCLinkLimits[] = $options['limit'];
+ $wgRCLinkDays[] = $options['days'];
+ sort($wgRCLinkLimits);
+ sort($wgRCLinkDays);
+ $wgRCLinkLimits = array_unique($wgRCLinkLimits);
+ $wgRCLinkDays = array_unique($wgRCLinkDays);
+
+ // limit links
+ foreach( $wgRCLinkLimits as $value ) {
+ $cl[] = $this->makeOptionsLink( $wgLang->formatNum( $value ),
+ array( 'limit' => $value ), $nondefaults, $value == $options['limit'] ) ;
+ }
+ $cl = implode( ' | ', $cl);
+
+ // day links, reset 'from' to none
+ foreach( $wgRCLinkDays as $value ) {
+ $dl[] = $this->makeOptionsLink( $wgLang->formatNum( $value ),
+ array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] ) ;
+ }
+ $dl = implode( ' | ', $dl);
+
+
+ // show/hide links
+ $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ));
+ $minorLink = $this->makeOptionsLink( $showhide[1-$options['hideminor']],
+ array( 'hideminor' => 1-$options['hideminor'] ), $nondefaults);
+ $botLink = $this->makeOptionsLink( $showhide[1-$options['hidebots']],
+ array( 'hidebots' => 1-$options['hidebots'] ), $nondefaults);
+ $anonsLink = $this->makeOptionsLink( $showhide[ 1 - $options['hideanons'] ],
+ array( 'hideanons' => 1 - $options['hideanons'] ), $nondefaults );
+ $liuLink = $this->makeOptionsLink( $showhide[1-$options['hideliu']],
+ array( 'hideliu' => 1-$options['hideliu'] ), $nondefaults);
+ $patrLink = $this->makeOptionsLink( $showhide[1-$options['hidepatrolled']],
+ array( 'hidepatrolled' => 1-$options['hidepatrolled'] ), $nondefaults);
+ $myselfLink = $this->makeOptionsLink( $showhide[1-$options['hidemyself']],
+ array( 'hidemyself' => 1-$options['hidemyself'] ), $nondefaults);
+
+ $links[] = wfMsgHtml( 'rcshowhideminor', $minorLink );
+ $links[] = wfMsgHtml( 'rcshowhidebots', $botLink );
+ $links[] = wfMsgHtml( 'rcshowhideanons', $anonsLink );
+ $links[] = wfMsgHtml( 'rcshowhideliu', $liuLink );
+ if( $wgUser->useRCPatrol() )
+ $links[] = wfMsgHtml( 'rcshowhidepatr', $patrLink );
+ $links[] = wfMsgHtml( 'rcshowhidemine', $myselfLink );
+ $hl = implode( ' | ', $links );
+
+ // show from this onward link
+ $now = $wgLang->timeanddate( wfTimestampNow(), true );
+ $tl = $this->makeOptionsLink( $now, array( 'from' => wfTimestampNow()), $nondefaults );
+
+ $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter'),
+ $cl, $dl, $hl );
+ $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter'), $tl );
+ return "$note<br />$rclinks<br />$rclistfrom";
+ }
+}
diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php
new file mode 100644
index 00000000..d773fb77
--- /dev/null
+++ b/includes/specials/SpecialRecentchangeslinked.php
@@ -0,0 +1,178 @@
+<?php
+
+/**
+ * This is to display changes made to all articles linked in an article.
+ * @ingroup SpecialPage
+ */
+class SpecialRecentchangeslinked extends SpecialRecentchanges {
+
+ function __construct(){
+ SpecialPage::SpecialPage( 'Recentchangeslinked' );
+ }
+
+ public function getDefaultOptions() {
+ $opts = parent::getDefaultOptions();
+ $opts->add( 'target', '' );
+ $opts->add( 'showlinkedto', false );
+ return $opts;
+ }
+
+ public function parseParameters( $par, FormOptions $opts ) {
+ $opts['target'] = $par;
+ }
+
+ public function feedSetup(){
+ global $wgRequest;
+ $opts = parent::feedSetup();
+ $opts['target'] = $wgRequest->getVal( 'target' );
+ return $opts;
+ }
+
+ public function getFeedObject( $feedFormat ){
+ $feed = new ChangesFeed( $feedFormat, false );
+ $feedObj = $feed->getFeedObject(
+ wfMsgForContent( 'recentchangeslinked-title', $this->mTargetTitle->getPrefixedText() ),
+ wfMsgForContent( 'recentchangeslinked' )
+ );
+ return array( $feed, $feedObj );
+ }
+
+ public function doMainQuery( $conds, $opts ) {
+ global $wgUser, $wgOut;
+
+ $target = $opts['target'];
+ $showlinkedto = $opts['showlinkedto'];
+ $limit = $opts['limit'];
+
+ if ( $target === '' ) {
+ return false;
+ }
+ $title = Title::newFromURL( $target );
+ if( !$title || $title->getInterwiki() != '' ){
+ $wgOut->wrapWikiMsg( '<div class="errorbox">$1</div><br clear="both" />', 'allpagesbadtitle' );
+ return false;
+ }
+ $this->mTargetTitle = $title;
+
+ $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $title->getPrefixedText() ) );
+
+ /*
+ * Ordinary links are in the pagelinks table, while transclusions are
+ * in the templatelinks table, categorizations in categorylinks and
+ * image use in imagelinks. We need to somehow combine all these.
+ * Special:Whatlinkshere does this by firing multiple queries and
+ * merging the results, but the code we inherit from our parent class
+ * expects only one result set so we use UNION instead.
+ */
+
+ $dbr = wfGetDB( DB_SLAVE, 'recentchangeslinked' );
+ $id = $title->getArticleId();
+ $ns = $title->getNamespace();
+ $dbkey = $title->getDBkey();
+
+ $tables = array( 'recentchanges' );
+ $select = array( $dbr->tableName( 'recentchanges' ) . '.*' );
+ $join_conds = array();
+
+ // left join with watchlist table to highlight watched rows
+ if( $uid = $wgUser->getId() ) {
+ $tables[] = 'watchlist';
+ $select[] = 'wl_user';
+ $join_conds['watchlist'] = array( 'LEFT JOIN', "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace" );
+ }
+
+ // XXX: parent class does this, should we too?
+ // wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) );
+
+ if( $ns == NS_CATEGORY && !$showlinkedto ) {
+ // special handling for categories
+ // XXX: should try to make this less klugy
+ $link_tables = array( 'categorylinks' );
+ $showlinkedto = true;
+ } else {
+ // for now, always join on these tables; really should be configurable as in whatlinkshere
+ $link_tables = array( 'pagelinks', 'templatelinks' );
+ // imagelinks only contains links to pages in NS_IMAGE
+ if( $ns == NS_IMAGE || !$showlinkedto ) $link_tables[] = 'imagelinks';
+ }
+
+ // field name prefixes for all the various tables we might want to join with
+ $prefix = array( 'pagelinks' => 'pl', 'templatelinks' => 'tl', 'categorylinks' => 'cl', 'imagelinks' => 'il' );
+
+ $subsql = array(); // SELECT statements to combine with UNION
+
+ foreach( $link_tables as $link_table ) {
+ $pfx = $prefix[$link_table];
+
+ // imagelinks and categorylinks tables have no xx_namespace field, and have xx_to instead of xx_title
+ if( $link_table == 'imagelinks' ) $link_ns = NS_IMAGE;
+ else if( $link_table == 'categorylinks' ) $link_ns = NS_CATEGORY;
+ else $link_ns = 0;
+
+ if( $showlinkedto ) {
+ // find changes to pages linking to this page
+ if( $link_ns ) {
+ if( $ns != $link_ns ) continue; // should never happen, but check anyway
+ $subconds = array( "{$pfx}_to" => $dbkey );
+ } else {
+ $subconds = array( "{$pfx}_namespace" => $ns, "{$pfx}_title" => $dbkey );
+ }
+ $subjoin = "rc_cur_id = {$pfx}_from";
+ } else {
+ // find changes to pages linked from this page
+ $subconds = array( "{$pfx}_from" => $id );
+ if( $link_table == 'imagelinks' || $link_table == 'categorylinks' ) {
+ $subconds["rc_namespace"] = $link_ns;
+ $subjoin = "rc_title = {$pfx}_to";
+ } else {
+ $subjoin = "rc_namespace = {$pfx}_namespace AND rc_title = {$pfx}_title";
+ }
+ }
+
+ $subsql[] = $dbr->selectSQLText( array_merge( $tables, array( $link_table ) ), $select, $conds + $subconds,
+ __METHOD__, array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ),
+ $join_conds + array( $link_table => array( 'INNER JOIN', $subjoin ) ) );
+ }
+
+ if( count($subsql) == 0 )
+ return false; // should never happen
+ if( count($subsql) == 1 )
+ $sql = $subsql[0];
+ else {
+ // need to resort and relimit after union
+ $sql = "(" . implode( ") UNION (", $subsql ) . ") ORDER BY rc_timestamp DESC LIMIT {$limit}";
+ }
+
+ $res = $dbr->query( $sql, __METHOD__ );
+
+ if( $dbr->numRows( $res ) == 0 )
+ $this->mResultEmpty = true;
+
+ return $res;
+ }
+
+ function getExtraOptions( $opts ){
+ $opts->consumeValues( array( 'showlinkedto', 'target' ) );
+ $extraOpts = array();
+ $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
+ $extraOpts['target'] = array( wfMsg( 'recentchangeslinked-page' ),
+ Xml::input( 'target', 40, str_replace('_',' ',$opts['target']) ) .
+ Xml::check( 'showlinkedto', $opts['showlinkedto'], array('id' => 'showlinkedto') ) . ' ' .
+ Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) );
+ $extraOpts['submit'] = Xml::submitbutton( wfMsg('allpagessubmit') );
+ return $extraOpts;
+ }
+
+ function setTopText( &$out, $opts ){}
+
+ function setBottomText( &$out, $opts ){
+ if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) ){
+ global $wgUser;
+ $out->setFeedAppendQuery( "target=" . urlencode( $this->mTargetTitle->getPrefixedDBkey() ) );
+ $out->addHTML("&lt; ".$wgUser->getSkin()->makeLinkObj( $this->mTargetTitle, "", "redirect=no" )."<hr />\n");
+ }
+ if( isset( $this->mResultEmpty ) && $this->mResultEmpty ){
+ $out->addWikiMsg( 'recentchangeslinked-noresult' );
+ }
+ }
+}
diff --git a/includes/specials/SpecialResetpass.php b/includes/specials/SpecialResetpass.php
new file mode 100644
index 00000000..707b941d
--- /dev/null
+++ b/includes/specials/SpecialResetpass.php
@@ -0,0 +1,167 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/** Constructor */
+function wfSpecialResetpass( $par ) {
+ $form = new PasswordResetForm();
+ $form->execute( $par );
+}
+
+/**
+ * Let users recover their password.
+ * @ingroup SpecialPage
+ */
+class PasswordResetForm extends SpecialPage {
+ function __construct( $name=null, $reset=null ) {
+ if( $name !== null ) {
+ $this->mName = $name;
+ $this->mTemporaryPassword = $reset;
+ } else {
+ global $wgRequest;
+ $this->mName = $wgRequest->getVal( 'wpName' );
+ $this->mTemporaryPassword = $wgRequest->getVal( 'wpPassword' );
+ }
+ }
+
+ /**
+ * Main execution point
+ */
+ function execute( $par ) {
+ global $wgUser, $wgAuth, $wgOut, $wgRequest;
+
+ if( !$wgAuth->allowPasswordChange() ) {
+ $this->error( wfMsg( 'resetpass_forbidden' ) );
+ return;
+ }
+
+ if( $this->mName === null && !$wgRequest->wasPosted() ) {
+ $this->error( wfMsg( 'resetpass_missing' ) );
+ return;
+ }
+
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) {
+ $newpass = $wgRequest->getVal( 'wpNewPassword' );
+ $retype = $wgRequest->getVal( 'wpRetype' );
+ try {
+ $this->attemptReset( $newpass, $retype );
+ $wgOut->addWikiMsg( 'resetpass_success' );
+
+ $data = array(
+ 'action' => 'submitlogin',
+ 'wpName' => $this->mName,
+ 'wpPassword' => $newpass,
+ 'returnto' => $wgRequest->getVal( 'returnto' ),
+ );
+ if( $wgRequest->getCheck( 'wpRemember' ) ) {
+ $data['wpRemember'] = 1;
+ }
+ $login = new LoginForm( new FauxRequest( $data, true ) );
+ $login->execute();
+
+ return;
+ } catch( PasswordError $e ) {
+ $this->error( $e->getMessage() );
+ }
+ }
+ $this->showForm();
+ }
+
+ function error( $msg ) {
+ global $wgOut;
+ $wgOut->addHtml( '<div class="errorbox">' .
+ htmlspecialchars( $msg ) .
+ '</div>' );
+ }
+
+ function showForm() {
+ global $wgOut, $wgUser, $wgRequest;
+
+ $wgOut->disallowUserJs();
+
+ $self = SpecialPage::getTitleFor( 'Resetpass' );
+ $form =
+ '<div id="userloginForm">' .
+ wfOpenElement( 'form',
+ array(
+ 'method' => 'post',
+ 'action' => $self->getLocalUrl() ) ) .
+ '<h2>' . wfMsgHtml( 'resetpass_header' ) . '</h2>' .
+ '<div id="userloginprompt">' .
+ wfMsgExt( 'resetpass_text', array( 'parse' ) ) .
+ '</div>' .
+ '<table>' .
+ wfHidden( 'token', $wgUser->editToken() ) .
+ wfHidden( 'wpName', $this->mName ) .
+ wfHidden( 'wpPassword', $this->mTemporaryPassword ) .
+ wfHidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) .
+ $this->pretty( array(
+ array( 'wpName', 'username', 'text', $this->mName ),
+ array( 'wpNewPassword', 'newpassword', 'password', '' ),
+ array( 'wpRetype', 'yourpasswordagain', 'password', '' ),
+ ) ) .
+ '<tr>' .
+ '<td></td>' .
+ '<td>' .
+ Xml::checkLabel( wfMsg( 'remembermypassword' ),
+ 'wpRemember', 'wpRemember',
+ $wgRequest->getCheck( 'wpRemember' ) ) .
+ '</td>' .
+ '</tr>' .
+ '<tr>' .
+ '<td></td>' .
+ '<td>' .
+ wfSubmitButton( wfMsgHtml( 'resetpass_submit' ) ) .
+ '</td>' .
+ '</tr>' .
+ '</table>' .
+ wfCloseElement( 'form' ) .
+ '</div>';
+ $wgOut->addHtml( $form );
+ }
+
+ function pretty( $fields ) {
+ $out = '';
+ foreach( $fields as $list ) {
+ list( $name, $label, $type, $value ) = $list;
+ if( $type == 'text' ) {
+ $field = '<tt>' . htmlspecialchars( $value ) . '</tt>';
+ } else {
+ $field = Xml::input( $name, 20, $value,
+ array( 'id' => $name, 'type' => $type ) );
+ }
+ $out .= '<tr>';
+ $out .= '<td align="right">';
+ $out .= Xml::label( wfMsg( $label ), $name );
+ $out .= '</td>';
+ $out .= '<td>';
+ $out .= $field;
+ $out .= '</td>';
+ $out .= '</tr>';
+ }
+ return $out;
+ }
+
+ /**
+ * @throws PasswordError when cannot set the new password because requirements not met.
+ */
+ function attemptReset( $newpass, $retype ) {
+ $user = User::newFromName( $this->mName );
+ if( $user->isAnon() ) {
+ throw new PasswordError( 'no such user' );
+ }
+
+ if( !$user->checkTemporaryPassword( $this->mTemporaryPassword ) ) {
+ throw new PasswordError( wfMsg( 'resetpass_bad_temporary' ) );
+ }
+
+ if( $newpass !== $retype ) {
+ throw new PasswordError( wfMsg( 'badretype' ) );
+ }
+
+ $user->setPassword( $newpass );
+ $user->saveSettings();
+ }
+}
diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php
new file mode 100644
index 00000000..e94fc222
--- /dev/null
+++ b/includes/specials/SpecialRevisiondelete.php
@@ -0,0 +1,1474 @@
+<?php
+/**
+ * Special page allowing users with the appropriate permissions to view
+ * and hide revisions. Log items can also be hidden.
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialRevisiondelete( $par = null ) {
+ global $wgOut, $wgRequest, $wgUser;
+ # Handle our many different possible input types
+ $target = $wgRequest->getText( 'target' );
+ $oldid = $wgRequest->getArray( 'oldid' );
+ $artimestamp = $wgRequest->getArray( 'artimestamp' );
+ $logid = $wgRequest->getArray( 'logid' );
+ $img = $wgRequest->getArray( 'oldimage' );
+ $fileid = $wgRequest->getArray( 'fileid' );
+ # For reviewing deleted files...
+ $file = $wgRequest->getVal( 'file' );
+ # If this is a revision, then we need a target page
+ $page = Title::newFromUrl( $target );
+ if( is_null($page) ) {
+ $wgOut->addWikiMsg( 'undelete-header' );
+ return;
+ }
+ # Only one target set at a time please!
+ $i = (bool)$file + (bool)$oldid + (bool)$logid + (bool)$artimestamp + (bool)$fileid + (bool)$img;
+ if( $i !== 1 ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+ # Logs must have a type given
+ if( $logid && !strpos($page->getDBKey(),'/') ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+ # Either submit or create our form
+ $form = new RevisionDeleteForm( $page, $oldid, $logid, $artimestamp, $fileid, $img, $file );
+ if( $wgRequest->wasPosted() ) {
+ $form->submit( $wgRequest );
+ } else if( $oldid || $artimestamp ) {
+ $form->showRevs();
+ } else if( $fileid || $img ) {
+ $form->showImages();
+ } else if( $logid ) {
+ $form->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', $page->getPrefixedText() );
+ if( $wgUser->isAllowed( 'suppressionlog' ) ){
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
+ LogEventsList::showLogExtract( $wgOut, 'suppress', $page->getPrefixedText() );
+ }
+}
+
+/**
+ * Implements the GUI for Revision Deletion.
+ * @ingroup SpecialPage
+ */
+class RevisionDeleteForm {
+ /**
+ * @param Title $page
+ * @param array $oldids
+ * @param array $logids
+ * @param array $artimestamps
+ * @param array $fileids
+ * @param array $img
+ * @param string $file
+ */
+ function __construct( $page, $oldids, $logids, $artimestamps, $fileids, $img, $file ) {
+ global $wgUser, $wgOut;
+
+ $this->page = $page;
+ # For reviewing deleted files...
+ if( $file ) {
+ $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $page, $file );
+ $oimage->load();
+ // Check if user is allowed to see this file
+ if( !$oimage->userCan(File::DELETED_FILE) ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ } else {
+ $this->showFile( $file );
+ }
+ return;
+ }
+ $this->skin = $wgUser->getSkin();
+ # Give a link to the log for this page
+ if( !is_null($this->page) && $this->page->getNamespace() > -1 ) {
+ $links = array();
+
+ $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->getPrefixedUrl() ) ) );
+ }
+ # Logs themselves don't have histories or archived revisions
+ $wgOut->setSubtitle( '<p>'.implode($links,' / ').'</p>' );
+ }
+ // At this point, we should only have one of these
+ if( $oldids ) {
+ $this->revisions = $oldids;
+ $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
+ $this->deleteKey='oldid';
+ } else if( $artimestamps ) {
+ $this->archrevs = $artimestamps;
+ $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
+ $this->deleteKey='artimestamp';
+ } else if( $img ) {
+ $this->ofiles = $img;
+ $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
+ $this->deleteKey='oldimage';
+ } else if( $fileids ) {
+ $this->afiles = $fileids;
+ $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
+ $this->deleteKey='fileid';
+ } else if( $logids ) {
+ $this->events = $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
+ $this->checks = array(
+ $hide_content_name,
+ 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 );
+ }
+ }
+
+ /**
+ * Show a deleted file version requested by the visitor.
+ */
+ private function showFile( $key ) {
+ global $wgOut, $wgRequest;
+ $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
+ # nab the image, and Squid will serve it
+ $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+ $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 );
+ }
+
+ /**
+ * This lets a user set restrictions for live and archived revisions
+ */
+ function showRevs() {
+ global $wgOut, $wgUser, $action;
+
+ $UserAllowed = true;
+
+ $count = ($this->deleteKey=='oldid') ?
+ count($this->revisions) : count($this->archrevs);
+ $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count );
+
+ $bitfields = 0;
+ $wgOut->addHtml( "<ul>" );
+
+ $where = $revObjs = array();
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $revisions = 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);
+ }
+ $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
+ $result = $dbr->select( array('revision','page'), '*',
+ array( 'rev_page' => $this->page->getArticleID(),
+ $whereClause, '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( $action != 'submit') {
+ $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->addQuotes( $timestamp );
+ }
+ $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
+ $result = $dbr->select( 'archive', '*',
+ array( 'ar_namespace' => $this->page->getNamespace(),
+ 'ar_title' => $this->page->getDBKey(),
+ $whereClause ),
+ __METHOD__ );
+ while( $row = $dbr->fetchObject( $result ) ) {
+ $revObjs[$row->ar_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' => $row->ar_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( $action != 'submit') {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
+ }
+ $UserAllowed = false;
+ }
+ $revisions++;
+ $wgOut->addHtml( $this->historyLine( $revObjs[$timestamp] ) );
+ $bitfields |= $revObjs[$timestamp]->mDeleted;
+ }
+ }
+ if( !$revisions ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+
+ $wgOut->addHtml( "</ul>" );
+
+ $wgOut->addWikiMsg( 'revdelete-text' );
+
+ // 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() );
+ } 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' ) )
+ );
+ // 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;
+ $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
+ }
+ foreach( $items as $item ) {
+ $wgOut->addHtml( Xml::tags( 'p', null, $item ) );
+ }
+ foreach( $hidden as $item ) {
+ $wgOut->addHtml( $item );
+ }
+ $wgOut->addHtml(
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+
+ }
+
+ /**
+ * This lets a user set restrictions for archived images
+ */
+ function showImages() {
+ // What is $action doing here???
+ global $wgOut, $wgUser, $action, $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_SLAVE );
+ // 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[] = $dbr->addQuotes( $timestamp.'!'.$this->page->getDbKey() );
+ }
+ $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
+ $result = $dbr->select( 'oldimage', '*',
+ array( 'oi_name' => $this->page->getDbKey(),
+ $whereClause ),
+ __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;
+ }
+ // 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( $action != 'submit' ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
+ }
+ $UserAllowed = false;
+ }
+ $revisions++;
+ // Inject history info
+ $wgOut->addHtml( $this->fileLine( $filesObjs[$archivename] ) );
+ $bitfields |= $filesObjs[$archivename]->deleted;
+ }
+ // Archived files...
+ } else {
+ // Run through and pull all our data in one query
+ foreach( $this->afiles as $id ) {
+ $where[] = intval($id);
+ }
+ $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
+ $result = $dbr->select( 'filearchive', '*',
+ array( 'fa_name' => $this->page->getDbKey(),
+ $whereClause ),
+ __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( $action != 'submit' ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
+ }
+ $UserAllowed = false;
+ }
+ $revisions++;
+ // Inject history info
+ $wgOut->addHtml( $this->archivedfileLine( $filesObjs[$fileid] ) );
+ $bitfields |= $filesObjs[$fileid]->deleted;
+ }
+ }
+ if( !$revisions ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+
+ $wgOut->addHtml( "</ul>" );
+
+ $wgOut->addWikiMsg('revdelete-text' );
+ //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' ) )
+ );
+ // FIXME: all items checked for just one file are checked, even if not set for the others
+ foreach( $this->checks as $item ) {
+ list( $message, $name, $field ) = $item;
+ $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
+ }
+ foreach( $items as $item ) {
+ $wgOut->addHtml( "<p>$item</p>" );
+ }
+ foreach( $hidden as $item ) {
+ $wgOut->addHtml( $item );
+ }
+
+ $wgOut->addHtml(
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+ }
+
+ /**
+ * This lets a user set restrictions for log items
+ */
+ function showLogItems() {
+ global $wgOut, $wgUser, $action, $wgMessageCache, $wgLang;
+
+ $UserAllowed = true;
+ $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->events) ) );
+
+ $bitfields = 0;
+ $wgOut->addHtml( "<ul>" );
+
+ $where = $logRows = array();
+ $dbr = wfGetDB( DB_SLAVE );
+ // Run through and pull all our data in one query
+ $logItems = 0;
+ foreach( $this->events as $logid ) {
+ $where[] = intval($logid);
+ }
+ list($log,$logtype) = explode( '/',$this->page->getDBKey(), 2 );
+ $whereClause = "log_type = '$logtype' AND log_id IN(" . implode(',',$where) . ")";
+ $result = $dbr->select( 'logging', '*',
+ array( $whereClause ),
+ __METHOD__ );
+ while( $row = $dbr->fetchObject( $result ) ) {
+ $logRows[$row->log_id] = $row;
+ }
+ $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( $action != 'submit') {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
+ }
+ $UserAllowed = false;
+ }
+ $logItems++;
+ $wgOut->addHtml( $this->logLine( $logRows[$logid] ) );
+ $bitfields |= $logRows[$logid]->log_deleted;
+ }
+ if( !$logItems ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+ return;
+ }
+
+ $wgOut->addHtml( "</ul>" );
+
+ $wgOut->addWikiMsg( 'revdelete-text' );
+ // 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 ) );
+ 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' ) )
+ );
+ // FIXME: all items checked for just on event are checked, even if not set for the others
+ foreach( $this->checks as $item ) {
+ list( $message, $name, $field ) = $item;
+ $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
+ }
+ foreach( $items as $item ) {
+ $wgOut->addHtml( "<p>$item</p>" );
+ }
+ foreach( $hidden as $item ) {
+ $wgOut->addHtml( $item );
+ }
+
+ $wgOut->addHtml(
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+ }
+
+ /**
+ * @param Revision $rev
+ * @returns string
+ */
+ private function historyLine( $rev ) {
+ global $wgLang;
+
+ $date = $wgLang->timeanddate( $rev->getTimestamp() );
+ $difflink = $del = '';
+ // Live revisions
+ if( $this->deleteKey=='oldid' ) {
+ $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() );
+ $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'),
+ 'diff=' . $rev->getId() . '&oldid=prev' ) . ')';
+ // Archived revisions
+ } else {
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $target = $this->page->getPrefixedText();
+ $revlink = $this->skin->makeLinkObj( $undelete, $date,
+ "target=$target&timestamp=" . $rev->getTimestamp() );
+ $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'),
+ "target=$target&diff=prev&timestamp=" . $rev->getTimestamp() ) . ')';
+ }
+
+ 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') . ')';
+ }
+ }
+
+ return "<li> $difflink $revlink ".$this->skin->revUserLink( $rev )." ".$this->skin->revComment( $rev )."$del</li>";
+ }
+
+ /**
+ * @param File $file
+ * @returns string
+ */
+ private function fileLine( $file ) {
+ global $wgLang, $wgTitle;
+
+ $target = $this->page->getPrefixedText();
+ $date = $wgLang->timeanddate( $file->getTimestamp(), true );
+
+ $del = '';
+ # Hidden files...
+ if( $file->isDeleted(File::DELETED_FILE) ) {
+ $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+ if( !$file->userCan(File::DELETED_FILE) ) {
+ $pageLink = $date;
+ } else {
+ $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date,
+ "target=$target&file=$file->sha1.".$file->getExtension() );
+ }
+ $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 );
+
+ return "<li>$pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
+ }
+
+ /**
+ * @param ArchivedFile $file
+ * @returns string
+ */
+ private function archivedfileLine( $file ) {
+ global $wgLang, $wgTitle;
+
+ $target = $this->page->getPrefixedText();
+ $date = $wgLang->timeanddate( $file->getTimestamp(), true );
+
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file={$file->getKey()}" );
+
+ $del = '';
+ if( $file->isDeleted(File::DELETED_FILE) ) {
+ $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+ }
+
+ $data = wfMsg( 'widthheight',
+ $wgLang->formatNum( $file->getWidth() ),
+ $wgLang->formatNum( $file->getHeight() ) ) .
+ ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')';
+ $data = htmlspecialchars( $data );
+
+ return "<li> $pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
+ }
+
+ /**
+ * @param Array $row row
+ * @returns string
+ */
+ private function logLine( $row ) {
+ global $wgLang;
+
+ $date = $wgLang->timeanddate( $row->log_timestamp );
+ $paramArray = LogPage::extractParams( $row->log_params );
+ $title = Title::makeTitle( $row->log_namespace, $row->log_title );
+
+ $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>';
+ } 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>';
+ }
+ // 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
+ */
+ 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 );
+ } else {
+ $link = wfMsgHtml( 'rev-deleted-user' );
+ }
+ if( $file->isDeleted( Revision::DELETED_USER ) ) {
+ return '<span class="history-deleted">' . $link . '</span>';
+ }
+ return $link;
+ }
+
+ /**
+ * Wrap and format the given file's comment block, if the current
+ * user is allowed to view it.
+ *
+ * @param ArchivedFile $file
+ * @return string HTML
+ */
+ 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;
+ }
+
+ /**
+ * @param WebRequest $request
+ */
+ function submit( $request ) {
+ global $wgUser, $wgOut;
+
+ $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();
+ }
+ }
+
+ private function success() {
+ global $wgOut;
+
+ $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
+
+ $wrap = '<span class="success">$1</span>';
+
+ 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();
+ }
+ }
+
+ /**
+ * Put together a rev_deleted bitfield from the submitted checkboxes
+ * @param WebRequest $request
+ * @return int
+ */
+ private function extractBitfield( $request ) {
+ $bitfield = 0;
+ foreach( $this->checks as $item ) {
+ list( /* message */ , $name, $field ) = $item;
+ if( $request->getCheck( $name ) ) {
+ $bitfield |= $field;
+ }
+ }
+ return $bitfield;
+ }
+
+ 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 );
+ }
+ }
+}
+
+/**
+ * Implements the actions for Revision Deletion.
+ * @ingroup SpecialPage
+ */
+class RevisionDeleter {
+ function __construct( $db ) {
+ $this->dbw = $db;
+ }
+
+ /**
+ * @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 setRevVisibility( $title, $items, $bitfield, $comment ) {
+ global $wgOut;
+
+ $userAllowedAll = $success = true;
+ $revIDs = array();
+ $revCount = 0;
+ // Run through and pull all our data in one query
+ foreach( $items as $revid ) {
+ $where[] = intval($revid);
+ }
+ $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
+ $result = $this->dbw->select( 'revision', '*',
+ array( 'rev_page' => $title->getArticleID(),
+ $whereClause ),
+ __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;
+
+ $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;
+ }
+
+ return $success;
+ }
+
+ /**
+ * @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 setArchiveVisibility( $title, $items, $bitfield, $comment ) {
+ global $wgOut;
+
+ $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->addQuotes( $timestamp );
+ }
+ $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
+ $result = $this->dbw->select( 'archive', '*',
+ array( 'ar_namespace' => $title->getNamespace(),
+ 'ar_title' => $title->getDBKey(),
+ $whereClause ),
+ __METHOD__ );
+ while( $row = $this->dbw->fetchObject( $result ) ) {
+ $revObjs[$row->ar_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' => $row->ar_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++;
+
+ $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;
+ }
+
+ return $success;
+ }
+
+ /**
+ * @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 setOldImgVisibility( $title, $items, $bitfield, $comment ) {
+ global $wgOut;
+
+ $userAllowedAll = $success = true;
+ $count = 0;
+ $set = array();
+ // Run through and pull all our data in one query
+ foreach( $items as $timestamp ) {
+ $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDbKey() );
+ }
+ $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
+ $result = $this->dbw->select( 'oldimage', '*',
+ array( 'oi_name' => $title->getDbKey(),
+ $whereClause ),
+ __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;
+ }
+
+ $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();
+ }
+ }
+
+ // 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;
+ }
+
+ return $success;
+ }
+
+ /**
+ * @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;
+
+ $userAllowedAll = $success = true;
+ $count = 0;
+ $Id_set = array();
+
+ // Run through and pull all our data in one query
+ foreach( $items as $id ) {
+ $where[] = intval($id);
+ }
+ $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
+ $result = $this->dbw->select( 'filearchive', '*',
+ array( 'fa_name' => $title->getDbKey(),
+ $whereClause ),
+ __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++;
+
+ $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;
+ }
+
+ return $success;
+ }
+
+ /**
+ * @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;
+
+ $userAllowedAll = $success = true;
+ $count = 0;
+ $log_Ids = array();
+
+ // Run through and pull all our data in one query
+ foreach( $items as $logid ) {
+ $where[] = intval($logid);
+ }
+ list($log,$logtype) = explode( '/',$title->getDBKey(), 2 );
+ $whereClause = "log_type ='$logtype' AND log_id IN(" . implode(',',$where) . ")";
+ $result = $this->dbw->select( 'logging', '*',
+ array( $whereClause ),
+ __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++;
+
+ $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;
+ }
+
+ return $success;
+ }
+
+ /**
+ * 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
+ */
+ 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
+ }
+ } else {
+ wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" );
+ $group = null;
+ $key = '';
+ $transaction = new FSTransaction(); // empty
+ }
+
+ 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;
+ }
+
+ wfDebug( __METHOD__.": set db items, applying file transactions\n" );
+ $transaction->commit();
+ FileStore::unlock();
+
+ $m = explode('!',$oimage->archive_name,2);
+ $timestamp = $m[0];
+
+ return $timestamp;
+ }
+
+ /**
+ * 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;
+ }
+
+ $store = FileStore::get( 'deleted' );
+ if( !$store ) {
+ wfDebug( __METHOD__.": skipping row with no file.\n" );
+ return false;
+ }
+
+ $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;
+ }
+ $transaction->add( $store->export( $key, $destPath, $flags ) );
+
+ wfDebug( __METHOD__.": set db items, applying file transactions\n" );
+ $transaction->commit();
+ FileStore::unlock();
+
+ $m = explode('!',$oimage->archive_name,2);
+ $timestamp = $m[0];
+
+ return $timestamp;
+ }
+
+ /**
+ * 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__ );
+ }
+
+ /**
+ * 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__ );
+ }
+
+ /**
+ * 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__ );
+ }
+
+ /**
+ * 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__ );
+ }
+
+ /**
+ * 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__ );
+ }
+
+ /**
+ * 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__ );
+ }
+
+ /**
+ * 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__ );
+ }
+
+ /**
+ * 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
+ */
+ function updatePage( $title ) {
+ $title->invalidateCache();
+ $title->purgeSquid();
+
+ // Extensions that require referencing previous revisions may need this
+ wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
+ }
+
+ /**
+ * 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.
+ */
+ 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 int $n The new bitfield.
+ * @param int $o The old bitfield.
+ * @return An array as described above.
+ */
+ function getChanges ( $n, $o ) {
+ $diff = $n ^ $o;
+ $ret = array ( 0 => array(), 1 => array(), 2 => array() );
+
+ $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 );
+
+ // 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' );
+ }
+
+ 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 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;
+
+ $s = '';
+ $changes = $this->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] )) {
+ if ($s)
+ $s .= ' (' . $changes[2][0] . ')';
+ else
+ $s = $changes[2][0];
+ }
+
+ $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
+ $ret = wfMsgExt ( $msg, array( 'parsemag', 'content' ),
+ $s, $wgContLang->formatNum( $count ) );
+
+ if ( $comment )
+ $ret .= ": $comment";
+
+ return $ret;
+
+ }
+
+ /**
+ * 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 );
+
+ $reason = $this->getLogMessage ( $count, $nbitfield, $obitfield, $comment, $param == 'logid' );
+
+ if( $param == 'logid' ) {
+ $params = array( implode( ',', $items) );
+ $log->addEntry( 'event', $title, $reason, $params );
+ } else {
+ // Add params for effected page and ids
+ $params = array( $param, implode( ',', $items) );
+ $log->addEntry( 'revision', $title, $reason, $params );
+ }
+ }
+}
diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php
new file mode 100644
index 00000000..f13c1676
--- /dev/null
+++ b/includes/specials/SpecialSearch.php
@@ -0,0 +1,651 @@
+<?php
+# Copyright (C) 2004 Brion Vibber <brion@pobox.com>
+# 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
+
+/**
+ * Run text & title search and display the output
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point
+ *
+ * @param $par String: (default '')
+ */
+function wfSpecialSearch( $par = '' ) {
+ global $wgRequest, $wgUser;
+
+ $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $par ) );
+ $searchPage = new SpecialSearch( $wgRequest, $wgUser );
+ if( $wgRequest->getVal( 'fulltext' )
+ || !is_null( $wgRequest->getVal( 'offset' ))
+ || !is_null( $wgRequest->getVal( 'searchx' ))) {
+ $searchPage->showResults( $search, 'search' );
+ } else {
+ $searchPage->goResult( $search );
+ }
+}
+
+/**
+ * implements Special:Search - Run text & title search and display the output
+ * @ingroup SpecialPage
+ */
+class SpecialSearch {
+
+ /**
+ * 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 SpecialSearch( &$request, &$user ) {
+ list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
+
+ $this->namespaces = $this->powerSearch( $request );
+ if( empty( $this->namespaces ) ) {
+ $this->namespaces = SearchEngine::userNamespaces( $user );
+ }
+
+ $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ $wgOut->wrapWikiMsg( "==$1==\n", 'notitlematches' );
+ if( $t->quickUserCan( 'create' ) && $t->quickUserCan( 'edit' ) ) {
+ $wgOut->addWikiMsg( 'noexactmatch', wfEscapeWikiText( $term ) );
+ } else {
+ $wgOut->addWikiMsg( 'noexactmatch-nocreate', wfEscapeWikiText( $term ) );
+ }
+
+ return $this->showResults( $term );
+ }
+
+ /**
+ * @param string $term
+ * @public
+ */
+ function showResults( $term ) {
+ $fname = 'SpecialSearch::showResults';
+ wfProfileIn( $fname );
+ global $wgOut, $wgUser;
+ $sk = $wgUser->getSkin();
+
+ $this->setupPage( $term );
+
+ $wgOut->addWikiMsg( 'searchresulttext' );
+
+ if( '' === trim( $term ) ) {
+ // Empty query -- straight view of search form
+ $wgOut->setSubtitle( '' );
+ $wgOut->addHTML( $this->powerSearchBox( $term ) );
+ $wgOut->addHTML( $this->powerSearchFocus() );
+ wfProfileOut( $fname );
+ return;
+ }
+
+ global $wgDisableTextSearch;
+ if ( $wgDisableTextSearch ) {
+ global $wgSearchForwardUrl;
+ if( $wgSearchForwardUrl ) {
+ $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
+ $wgOut->redirect( $url );
+ 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( $fname );
+ return;
+ }
+
+ $wgOut->addHTML( $this->shortDialog( $term ) );
+
+ $search = SearchEngine::create();
+ $search->setLimitOffset( $this->limit, $this->offset );
+ $search->setNamespaces( $this->namespaces );
+ $search->showRedirects = $this->searchRedirects;
+ $rewritten = $search->replacePrefixes($term);
+
+ $titleMatches = $search->searchTitle( $rewritten );
+
+ // 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( $fname );
+ return;
+ }
+
+ $textMatches = $search->searchText( $rewritten );
+
+ // did you mean... suggestions
+ if($textMatches && $textMatches->hasSuggestion()){
+ $st = SpecialPage::getTitleFor( 'Search' );
+ $stParams = wfArrayToCGI( array(
+ 'search' => $textMatches->getSuggestionQuery(),
+ 'fulltext' => wfMsg('search')),
+ $this->powerSearchOptions());
+
+ $suggestLink = '<a href="'.$st->escapeLocalURL($stParams).'">'.
+ $textMatches->getSuggestionSnippet().'</a>';
+
+ $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
+ }
+
+ // 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 );
+ } 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( $fname );
+ }
+
+ #------------------------------------------------------------------
+ # Private methods below this line
+
+ /**
+ *
+ */
+ function setupPage( $term ) {
+ global $wgOut;
+ if( !empty( $term ) )
+ $wgOut->setPageTitle( wfMsg( 'searchresults' ) );
+ $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 ) {
+ $fname = 'SpecialSearch::showMatches';
+ wfProfileIn( $fname );
+
+ 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( $fname );
+ return $out;
+ }
+
+ /**
+ * Format a single hit result
+ * @param SearchResult $result
+ * @param array $terms terms to highlight
+ */
+ function showHit( $result, $terms ) {
+ $fname = 'SpecialSearch::showHit';
+ wfProfileIn( $fname );
+ global $wgUser, $wgContLang, $wgLang;
+
+ if( $result->isBrokenTitle() ) {
+ wfProfileOut( $fname );
+ 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( $fname );
+ 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( $fname );
+ 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 = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'.
+ wfMsg('search-relatedarticle').'</a>';
+ }
+
+ // Include a thumbnail for media files...
+ if( $t->getNamespace() == NS_IMAGE ) {
+ $img = wfFindFile( $t );
+ if( $img ) {
+ $thumb = $img->getThumbnail( 120, 120 );
+ if( $thumb ) {
+ $desc = $img->getShortDesc();
+ wfProfileOut( $fname );
+ // 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( $fname );
+ 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 ) {
+ $fname = 'SpecialSearch::showInterwiki';
+ wfProfileIn( $fname );
+
+ 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( $fname );
+ 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){
+ $fname = 'SpecialSearch::showInterwikiHit';
+ wfProfileIn( $fname );
+ global $wgUser, $wgContLang, $wgLang;
+
+ if( $result->isBrokenTitle() ) {
+ wfProfileOut( $fname );
+ 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( $fname );
+ 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;
+
+ $namespaces = '';
+ foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+ $name = str_replace( '_', ' ', $name );
+ if( '' == $name ) {
+ $name = wfMsg( 'blanknamespace' );
+ }
+ $namespaces .= Xml::openElement( 'span', array( 'style' => 'white-space: nowrap' ) ) .
+ Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
+ Xml::closeElement( 'span' ) . "\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";
+
+ $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
+ Xml::fieldset( wfMsg( 'powersearch-legend' ),
+ Xml::hidden( 'title', 'Special:Search' ) .
+ "<p>" .
+ wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
+ "<br />" .
+ $namespaces .
+ "</p>" .
+ "<p>" .
+ $redirect . " " . $redirectLabel .
+ "</p>" .
+ wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) .
+ "&nbsp;" .
+ $searchField .
+ "&nbsp;" .
+ $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
+ ));
+ $out .= Xml::hidden( 'title', 'Special: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' );
+ }
+ }
+ $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) );
+ $out .= Xml::closeElement( 'form' );
+
+ return $out;
+ }
+}
diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php
new file mode 100644
index 00000000..2e7d24a5
--- /dev/null
+++ b/includes/specials/SpecialShortpages.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * SpecialShortpages extends QueryPage. It is used to return the shortest
+ * pages in the database.
+ * @ingroup SpecialPage
+ */
+class ShortPagesPage extends QueryPage {
+
+ function getName() {
+ return 'Shortpages';
+ }
+
+ /**
+ * This query is indexed as of 1.5
+ */
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ global $wgContentNamespaces;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $page = $dbr->tableName( 'page' );
+ $name = $dbr->addQuotes( $this->getName() );
+
+ $forceindex = $dbr->useIndexClause("page_len");
+
+ if ($wgContentNamespaces)
+ $nsclause = "page_namespace IN (" . $dbr->makeList($wgContentNamespaces) . ")";
+ else
+ $nsclause = "page_namespace = " . NS_MAIN;
+
+ return
+ "SELECT $name as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_len AS value
+ FROM $page $forceindex
+ WHERE $nsclause AND page_is_redirect=0";
+ }
+
+ function preprocessResults( $db, $res ) {
+ # There's no point doing a batch check if we aren't caching results;
+ # the page must exist for it to have been pulled out of the table
+ if( $this->isCached() ) {
+ $batch = new LinkBatch();
+ while( $row = $db->fetchObject( $res ) )
+ $batch->add( $row->namespace, $row->title );
+ $batch->execute();
+ if( $db->numRows( $res ) > 0 )
+ $db->dataSeek( $res, 0 );
+ }
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function formatResult( $skin, $result ) {
+ global $wgLang, $wgContLang;
+ $dm = $wgContLang->getDirMark();
+
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if ( !$title ) {
+ return '<!-- Invalid title ' . htmlspecialchars( "{$result->namespace}:{$result->title}" ). '-->';
+ }
+ $hlink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
+ $plink = $this->isCached()
+ ? $skin->makeLinkObj( $title )
+ : $skin->makeKnownLinkObj( $title );
+ $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->value ) ) );
+
+ return $title->exists()
+ ? "({$hlink}) {$dm}{$plink} {$dm}[{$size}]"
+ : "<s>({$hlink}) {$dm}{$plink} {$dm}[{$size}]</s>";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialShortpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $spp = new ShortPagesPage();
+
+ return $spp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php
new file mode 100644
index 00000000..ca91ad51
--- /dev/null
+++ b/includes/specials/SpecialSpecialpages.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialSpecialpages() {
+ global $wgOut, $wgUser, $wgMessageCache, $wgSortSpecialPages;
+
+ $wgMessageCache->loadAllMessages();
+
+ $wgOut->setRobotpolicy( 'noindex,nofollow' ); # Is this really needed?
+ $sk = $wgUser->getSkin();
+
+ $pages = SpecialPage::getUsablePages();
+
+ if( count( $pages ) == 0 ) {
+ # Yeah, that was pointless. Thanks for coming.
+ return;
+ }
+
+ /** Put them into a sortable array */
+ $groups = array();
+ foreach ( $pages as $page ) {
+ if ( $page->isListed() ) {
+ $group = SpecialPage::getGroup( $page );
+ if( !isset($groups[$group]) ) {
+ $groups[$group] = array();
+ }
+ $groups[$group][$page->getDescription()] = array( $page->getTitle(), $page->isRestricted() );
+ }
+ }
+
+ /** Sort */
+ if ( $wgSortSpecialPages ) {
+ foreach( $groups as $group => $sortedPages ) {
+ ksort( $groups[$group] );
+ }
+ }
+
+ /** Always move "other" to end */
+ if( array_key_exists('other',$groups) ) {
+ $other = $groups['other'];
+ unset( $groups['other'] );
+ $groups['other'] = $other;
+ }
+
+ /** Now output the HTML */
+ foreach ( $groups as $group => $sortedPages ) {
+ $middle = ceil( count($sortedPages)/2 );
+ $total = count($sortedPages);
+ $count = 0;
+
+ $wgOut->addHTML( "<h4 class='mw-specialpagesgroup'>".wfMsgHtml("specialpages-group-$group")."</h4>\n" );
+ $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 ) );
+ if( $restricted ) {
+ $wgOut->addHTML( "<li class='mw-specialpages-page mw-specialpagerestricted'>{$link}</li>\n" );
+ } else {
+ $wgOut->addHTML( "<li>{$link}</li>\n" );
+ }
+
+ # Split up the larger groups
+ $count++;
+ if( $total > 3 && $count == $middle ) {
+ $wgOut->addHTML( "</ul></td><td width='10%'></td><td width='30%' valign='top'><ul>" );
+ }
+ }
+ $wgOut->addHTML( "</ul></td><td width='30%' valign='top'></td></tr></table>\n" );
+ }
+ $wgOut->addHTML(
+ Xml::openElement('div', array( 'class' => 'mw-specialpages-notes' )).
+ wfMsgWikiHtml('specialpages-note').
+ Xml::closeElement('div')
+ );
+}
diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php
new file mode 100644
index 00000000..570a21c6
--- /dev/null
+++ b/includes/specials/SpecialStatistics.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * Special page lists various statistics, including the contents of
+ * `site_stats`, plus page view details if enabled
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Show the special page
+ *
+ * @param mixed $par (not used)
+ */
+function wfSpecialStatistics( $par = '' ) {
+ global $wgOut, $wgLang, $wgRequest;
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $views = SiteStats::views();
+ $edits = SiteStats::edits();
+ $good = SiteStats::articles();
+ $images = SiteStats::images();
+ $total = SiteStats::pages();
+ $users = SiteStats::users();
+ $admins = SiteStats::admins();
+ $numJobs = SiteStats::jobs();
+
+ if( $wgRequest->getVal( 'action' ) == 'raw' ) {
+ $wgOut->disable();
+ header( 'Pragma: nocache' );
+ echo "total=$total;good=$good;views=$views;edits=$edits;users=$users;admins=$admins;images=$images;jobs=$numJobs\n";
+ return;
+ } else {
+ $text = "__NOTOC__\n";
+ $text .= '==' . wfMsgNoTrans( 'sitestats' ) . "==\n";
+ $text .= wfMsgExt( 'sitestatstext', array( 'parsemag' ),
+ $wgLang->formatNum( $total ),
+ $wgLang->formatNum( $good ),
+ $wgLang->formatNum( $views ),
+ $wgLang->formatNum( $edits ),
+ $wgLang->formatNum( sprintf( '%.2f', $total ? $edits / $total : 0 ) ),
+ $wgLang->formatNum( sprintf( '%.2f', $edits ? $views / $edits : 0 ) ),
+ $wgLang->formatNum( $numJobs ),
+ $wgLang->formatNum( $images )
+ )."\n";
+
+ $text .= "==" . wfMsgNoTrans( 'userstats' ) . "==\n";
+ $text .= wfMsgExt( 'userstatstext', array ( 'parsemag' ),
+ $wgLang->formatNum( $users ),
+ $wgLang->formatNum( $admins ),
+ '[[' . wfMsgForContent( 'grouppage-sysop' ) . ']]', # TODO somehow remove, kept for backwards compatibility
+ $wgLang->formatNum( @sprintf( '%.2f', $admins / $users * 100 ) ),
+ User::makeGroupLinkWiki( 'sysop' )
+ )."\n";
+
+ global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang;
+ if( !$wgDisableCounters && !$wgMiserMode ) {
+ $res = $dbr->select(
+ 'page',
+ array(
+ 'page_namespace',
+ 'page_title',
+ 'page_counter',
+ ),
+ array(
+ 'page_is_redirect' => 0,
+ 'page_counter > 0',
+ ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'page_counter DESC',
+ 'LIMIT' => 10,
+ )
+ );
+ if( $res->numRows() > 0 ) {
+ $text .= "==" . wfMsgNoTrans( 'statistics-mostpopular' ) . "==\n";
+ while( $row = $res->fetchObject() ) {
+ $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+ if( $title instanceof Title )
+ $text .= '* [[:' . $title->getPrefixedText() . ']] (' . $wgLang->formatNum( $row->page_counter ) . ")\n";
+ }
+ $res->free();
+ }
+ }
+
+ $footer = wfMsgNoTrans( 'statistics-footer' );
+ if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' )
+ $text .= "\n" . $footer;
+
+ $wgOut->addWikiText( $text );
+ }
+}
diff --git a/includes/specials/SpecialUncategorizedcategories.php b/includes/specials/SpecialUncategorizedcategories.php
new file mode 100644
index 00000000..f23e89ce
--- /dev/null
+++ b/includes/specials/SpecialUncategorizedcategories.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Uncategorizedcategories
+ * @ingroup SpecialPage
+ */
+class UncategorizedCategoriesPage extends UncategorizedPagesPage {
+ function UncategorizedCategoriesPage() {
+ $this->requestedNamespace = NS_CATEGORY;
+ }
+
+ function getName() {
+ return "Uncategorizedcategories";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUncategorizedcategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new UncategorizedCategoriesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUncategorizedimages.php b/includes/specials/SpecialUncategorizedimages.php
new file mode 100644
index 00000000..986ec967
--- /dev/null
+++ b/includes/specials/SpecialUncategorizedimages.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Special page lists images which haven't been categorised
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class UncategorizedImagesPage extends ImageQueryPage {
+
+ function getName() {
+ return 'Uncategorizedimages';
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
+ $ns = NS_IMAGE;
+
+ return "SELECT 'Uncategorizedimages' AS type, page_namespace AS namespace,
+ page_title AS title, page_title AS value
+ FROM {$page} LEFT JOIN {$categorylinks} ON page_id = cl_from
+ WHERE cl_from IS NULL AND page_namespace = {$ns} AND page_is_redirect = 0";
+ }
+
+}
+
+function wfSpecialUncategorizedimages() {
+ $uip = new UncategorizedImagesPage();
+ list( $limit, $offset ) = wfCheckLimits();
+ return $uip->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUncategorizedpages.php b/includes/specials/SpecialUncategorizedpages.php
new file mode 100644
index 00000000..e7f0aaca
--- /dev/null
+++ b/includes/specials/SpecialUncategorizedpages.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page looking for page without any category.
+ * @ingroup SpecialPage
+ */
+class UncategorizedPagesPage extends PageQueryPage {
+ var $requestedNamespace = NS_MAIN;
+
+ function getName() {
+ return "Uncategorizedpages";
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
+ $name = $dbr->addQuotes( $this->getName() );
+
+ return
+ "
+ SELECT
+ $name as type,
+ page_namespace AS namespace,
+ page_title AS title,
+ page_title AS value
+ FROM $page
+ LEFT JOIN $categorylinks ON page_id=cl_from
+ WHERE cl_from IS NULL AND page_namespace={$this->requestedNamespace} AND page_is_redirect=0
+ ";
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUncategorizedpages() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $lpp = new UncategorizedPagesPage();
+
+ return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUncategorizedtemplates.php b/includes/specials/SpecialUncategorizedtemplates.php
new file mode 100644
index 00000000..cb2a6d40
--- /dev/null
+++ b/includes/specials/SpecialUncategorizedtemplates.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page lists all uncategorised pages in the
+ * template namespace
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class UncategorizedTemplatesPage extends UncategorizedPagesPage {
+
+ var $requestedNamespace = NS_TEMPLATE;
+
+ public function getName() {
+ return 'Uncategorizedtemplates';
+ }
+
+}
+
+/**
+ * Main execution point
+ *
+ * @param mixed $par Parameter passed to the page
+ */
+function wfSpecialUncategorizedtemplates() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $utp = new UncategorizedTemplatesPage();
+ $utp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php
new file mode 100644
index 00000000..fbbf89d6
--- /dev/null
+++ b/includes/specials/SpecialUndelete.php
@@ -0,0 +1,1276 @@
+<?php
+
+/**
+ * Special page allowing users with the appropriate permissions to view
+ * and restore deleted content
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialUndelete( $par ) {
+ global $wgRequest;
+
+ $form = new UndeleteForm( $wgRequest, $par );
+ $form->execute();
+}
+
+/**
+ * Used to show archived pages and eventually restore them.
+ * @ingroup SpecialPage
+ */
+class PageArchive {
+ protected $title;
+ var $fileStatus;
+
+ function __construct( $title ) {
+ if( is_null( $title ) ) {
+ throw new MWException( 'Archiver() given a null title.');
+ }
+ $this->title = $title;
+ }
+
+ /**
+ * List all deleted pages recorded in the archive table. Returns result
+ * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
+ * namespace/title.
+ *
+ * @return ResultWrapper
+ */
+ public static function listAllPages() {
+ $dbr = wfGetDB( DB_SLAVE );
+ return self::listPages( $dbr, '' );
+ }
+
+ /**
+ * List deleted pages recorded in the archive table matching the
+ * given title prefix.
+ * Returns result wrapper with (ar_namespace, ar_title, count) fields.
+ *
+ * @return ResultWrapper
+ */
+ public static function listPagesByPrefix( $prefix ) {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $title = Title::newFromText( $prefix );
+ if( $title ) {
+ $ns = $title->getNamespace();
+ $encPrefix = $dbr->escapeLike( $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%'",
+ );
+ return self::listPages( $dbr, $conds );
+ }
+
+ protected static function listPages( $dbr, $condition ) {
+ return $dbr->resultObject(
+ $dbr->select(
+ array( 'archive' ),
+ array(
+ 'ar_namespace',
+ 'ar_title',
+ 'COUNT(*) AS count'
+ ),
+ $condition,
+ __METHOD__,
+ array(
+ 'GROUP BY' => 'ar_namespace,ar_title',
+ 'ORDER BY' => 'ar_namespace,ar_title',
+ 'LIMIT' => 100,
+ )
+ )
+ );
+ }
+
+ /**
+ * List the revisions of the given page. Returns result wrapper with
+ * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
+ *
+ * @return ResultWrapper
+ */
+ function listRevisions() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'archive',
+ array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 'ar_len', 'ar_deleted' ),
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ),
+ 'PageArchive::listRevisions',
+ array( 'ORDER BY' => 'ar_timestamp DESC' ) );
+ $ret = $dbr->resultObject( $res );
+ return $ret;
+ }
+
+ /**
+ * List the deleted file revisions for this page, if it's a file page.
+ * Returns a result wrapper with various filearchive fields, or null
+ * if not a file page.
+ *
+ * @return ResultWrapper
+ * @todo Does this belong in Image for fuller encapsulation?
+ */
+ function listFiles() {
+ if( $this->title->getNamespace() == NS_IMAGE ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'filearchive',
+ array(
+ 'fa_id',
+ 'fa_name',
+ 'fa_archive_name',
+ 'fa_storage_key',
+ 'fa_storage_group',
+ 'fa_size',
+ 'fa_width',
+ 'fa_height',
+ 'fa_bits',
+ 'fa_metadata',
+ 'fa_media_type',
+ 'fa_major_mime',
+ 'fa_minor_mime',
+ 'fa_description',
+ 'fa_user',
+ 'fa_user_text',
+ 'fa_timestamp',
+ 'fa_deleted' ),
+ array( 'fa_name' => $this->title->getDBkey() ),
+ __METHOD__,
+ array( 'ORDER BY' => 'fa_timestamp DESC' ) );
+ $ret = $dbr->resultObject( $res );
+ return $ret;
+ }
+ return null;
+ }
+
+ /**
+ * Fetch (and decompress if necessary) the stored text for the deleted
+ * revision of the page with the given timestamp.
+ *
+ * @return string
+ * @deprecated Use getRevision() for more flexible information
+ */
+ function getRevisionText( $timestamp ) {
+ $rev = $this->getRevision( $timestamp );
+ return $rev ? $rev->getText() : null;
+ }
+
+ /**
+ * Return a Revision object containing data for the deleted revision.
+ * Note that the result *may* or *may not* have a null page ID.
+ * @param string $timestamp
+ * @return Revision
+ */
+ function getRevision( $timestamp ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'archive',
+ array(
+ 'ar_rev_id',
+ 'ar_text',
+ 'ar_comment',
+ 'ar_user',
+ 'ar_user_text',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_flags',
+ 'ar_text_id',
+ 'ar_deleted',
+ 'ar_len' ),
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ '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) );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return the most-previous revision, either live or deleted, against
+ * the deleted revision given by timestamp.
+ *
+ * May produce unexpected results in case of history merges or other
+ * unusual time issues.
+ *
+ * @param string $timestamp
+ * @return Revision or null
+ */
+ function getPreviousRevision( $timestamp ) {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ // Check the previous deleted revision...
+ $row = $dbr->selectRow( 'archive',
+ 'ar_timestamp',
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ 'ar_timestamp < ' .
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'ar_timestamp DESC',
+ 'LIMIT' => 1 ) );
+ $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
+
+ $row = $dbr->selectRow( array( 'page', 'revision' ),
+ array( 'rev_id', 'rev_timestamp' ),
+ array(
+ 'page_namespace' => $this->title->getNamespace(),
+ 'page_title' => $this->title->getDBkey(),
+ 'page_id = rev_page',
+ 'rev_timestamp < ' .
+ $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+ __METHOD__,
+ array(
+ 'ORDER BY' => 'rev_timestamp DESC',
+ 'LIMIT' => 1 ) );
+ $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
+ $prevLiveId = $row ? intval( $row->rev_id ) : null;
+
+ if( $prevLive && $prevLive > $prevDeleted ) {
+ // Most prior revision was live
+ return Revision::newFromId( $prevLiveId );
+ } elseif( $prevDeleted ) {
+ // Most prior revision was deleted
+ return $this->getRevision( $prevDeleted );
+ } else {
+ // No prior revision on this page.
+ return null;
+ }
+ }
+
+ /**
+ * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
+ */
+ function getTextFromRow( $row ) {
+ if( is_null( $row->ar_text_id ) ) {
+ // An old row from MediaWiki 1.4 or previous.
+ // Text is embedded in this row in classic compression format.
+ return Revision::getRevisionText( $row, "ar_" );
+ } else {
+ // New-style: keyed to the text storage backend.
+ $dbr = wfGetDB( DB_SLAVE );
+ $text = $dbr->selectRow( 'text',
+ array( 'old_text', 'old_flags' ),
+ array( 'old_id' => $row->ar_text_id ),
+ __METHOD__ );
+ return Revision::getRevisionText( $text );
+ }
+ }
+
+
+ /**
+ * Fetch (and decompress if necessary) the stored text of the most
+ * recently edited deleted revision of the page.
+ *
+ * If there are no archived revisions for the page, returns NULL.
+ *
+ * @return string
+ */
+ function getLastRevisionText() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow( 'archive',
+ array( 'ar_text', 'ar_flags', 'ar_text_id' ),
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ),
+ 'PageArchive::getLastRevisionText',
+ array( 'ORDER BY' => 'ar_timestamp DESC' ) );
+ if( $row ) {
+ return $this->getTextFromRow( $row );
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Quick check if any archived revisions are present for the page.
+ * @return bool
+ */
+ function isDeleted() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
+ array( 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey() ) );
+ return ($n > 0);
+ }
+
+ /**
+ * Restore the given (or all) text and file revisions for the page.
+ * Once restored, the items will be removed from the archive tables.
+ * The deletion log will be updated with an undeletion notice.
+ *
+ * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+ * @param string $comment
+ * @param array $fileVersions
+ * @param bool $unsuppress
+ *
+ * @return array(number of file revisions restored, number of image revisions restored, log message)
+ * on success, false on failure
+ */
+ function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false ) {
+ // If both the set of text revisions and file revisions are empty,
+ // restore everything. Otherwise, just restore the requested items.
+ $restoreAll = empty( $timestamps ) && empty( $fileVersions );
+
+ $restoreText = $restoreAll || !empty( $timestamps );
+ $restoreFiles = $restoreAll || !empty( $fileVersions );
+
+ if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
+ $img = wfLocalFile( $this->title );
+ $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
+ $filesRestored = $this->fileStatus->successCount;
+ } else {
+ $filesRestored = 0;
+ }
+
+ if( $restoreText ) {
+ $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress );
+ if($textRestored === false) // It must be one of UNDELETE_*
+ return false;
+ } else {
+ $textRestored = 0;
+ }
+
+ // Touch the log!
+ global $wgContLang;
+ $log = new LogPage( 'delete' );
+
+ if( $textRestored && $filesRestored ) {
+ $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ),
+ $wgContLang->formatNum( $textRestored ),
+ $wgContLang->formatNum( $filesRestored ) );
+ } elseif( $textRestored ) {
+ $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ),
+ $wgContLang->formatNum( $textRestored ) );
+ } elseif( $filesRestored ) {
+ $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ),
+ $wgContLang->formatNum( $filesRestored ) );
+ } else {
+ wfDebug( "Undelete: nothing undeleted...\n" );
+ return false;
+ }
+
+ if( trim( $comment ) != '' )
+ $reason .= ": {$comment}";
+ $log->addEntry( 'restore', $this->title, $reason );
+
+ return array($textRestored, $filesRestored, $reason);
+ }
+
+ /**
+ * This is the meaty bit -- restores archived revisions of the given page
+ * to the cur/old tables. If the page currently exists, all revisions will
+ * be stuffed into old, otherwise the most recent will go into cur.
+ *
+ * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+ * @param string $comment
+ * @param array $fileVersions
+ * @param bool $unsuppress, remove all ar_deleted/fa_deleted restrictions of seletected revs
+ *
+ * @return mixed number of revisions restored or false on failure
+ */
+ private function undeleteRevisions( $timestamps, $unsuppress = false ) {
+ if ( wfReadOnly() )
+ return false;
+ $restoreAll = empty( $timestamps );
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ # Does this page already exist? We'll have to update it...
+ $article = new Article( $this->title );
+ $options = 'FOR UPDATE';
+ $page = $dbw->selectRow( 'page',
+ array( 'page_id', 'page_latest' ),
+ array( 'page_namespace' => $this->title->getNamespace(),
+ 'page_title' => $this->title->getDBkey() ),
+ __METHOD__,
+ $options );
+ if( $page ) {
+ $makepage = false;
+ # Page already exists. Import the history, and if necessary
+ # we'll update the latest revision field in the record.
+ $newid = 0;
+ $pageId = $page->page_id;
+ $previousRevId = $page->page_latest;
+ # Get the time span of this page
+ $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
+ array( 'rev_id' => $previousRevId ),
+ __METHOD__ );
+ if( $previousTimestamp === false ) {
+ wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
+ return 0;
+ }
+ } else {
+ # Have to create a new article...
+ $makepage = true;
+ $previousRevId = 0;
+ $previousTimestamp = 0;
+ }
+
+ if( $restoreAll ) {
+ $oldones = '1 = 1'; # All revisions...
+ } else {
+ $oldts = implode( ',',
+ array_map( array( &$dbw, 'addQuotes' ),
+ array_map( array( &$dbw, 'timestamp' ),
+ $timestamps ) ) );
+
+ $oldones = "ar_timestamp IN ( {$oldts} )";
+ }
+
+ /**
+ * Select each archived revision...
+ */
+ $result = $dbw->select( 'archive',
+ /* fields */ array(
+ 'ar_rev_id',
+ 'ar_text',
+ 'ar_comment',
+ 'ar_user',
+ 'ar_user_text',
+ 'ar_timestamp',
+ 'ar_minor_edit',
+ 'ar_flags',
+ 'ar_text_id',
+ 'ar_deleted',
+ 'ar_page_id',
+ 'ar_len' ),
+ /* WHERE */ array(
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ $oldones ),
+ __METHOD__,
+ /* options */ array(
+ 'ORDER BY' => 'ar_timestamp' )
+ );
+ $ret = $dbw->resultObject( $result );
+
+ $rev_count = $dbw->numRows( $result );
+ if( $rev_count ) {
+ # We need to seek around as just using DESC in the ORDER BY
+ # would leave the revisions inserted in the wrong order
+ $first = $ret->fetchObject();
+ $ret->seek( $rev_count - 1 );
+ $last = $ret->fetchObject();
+ // We don't handle well changing the top revision's settings
+ if( !$unsuppress && $last->ar_deleted && $last->ar_timestamp > $previousTimestamp ) {
+ wfDebug( __METHOD__.": restoration would result in a deleted top revision\n" );
+ return false;
+ }
+ $ret->seek( 0 );
+ }
+
+ if( $makepage ) {
+ $newid = $article->insertOn( $dbw );
+ $pageId = $newid;
+ }
+
+ $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_' );
+ }
+ $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
+ ) );
+ $revision->insertOn( $dbw );
+ $restored++;
+
+ wfRunHooks( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) );
+ }
+ // Was anything restored at all?
+ if($restored == 0)
+ return 0;
+
+ if( $revision ) {
+ // Attach the latest revision to the page...
+ $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
+
+ if( $newid || $wasnew ) {
+ // Update site stats, link tables, etc
+ $article->createUpdates( $revision );
+ }
+
+ if( $newid ) {
+ wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) );
+ Article::onArticleCreate( $this->title );
+ } else {
+ wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
+ Article::onArticleEdit( $this->title );
+ }
+
+ if( $this->title->getNamespace() == NS_IMAGE ) {
+ $update = new HTMLCacheUpdate( $this->title, 'imagelinks' );
+ $update->doUpdate();
+ }
+ } else {
+ // Revision couldn't be created. This is very weird
+ return self::UNDELETE_UNKNOWNERR;
+ }
+
+ # Now that it's safely stored, take it out of the archive
+ $dbw->delete( 'archive',
+ /* WHERE */ array(
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ $oldones ),
+ __METHOD__ );
+
+ return $restored;
+ }
+
+ function getFileStatus() { return $this->fileStatus; }
+}
+
+/**
+ * The HTML form for Special:Undelete, which allows users with the appropriate
+ * permissions to view and restore deleted content.
+ * @ingroup SpecialPage
+ */
+class UndeleteForm {
+ var $mAction, $mTarget, $mTimestamp, $mRestore, $mTargetObj;
+ var $mTargetTimestamp, $mAllowed, $mComment;
+
+ function UndeleteForm( $request, $par = "" ) {
+ global $wgUser;
+ $this->mAction = $request->getVal( 'action' );
+ $this->mTarget = $request->getVal( 'target' );
+ $this->mSearchPrefix = $request->getText( 'prefix' );
+ $time = $request->getVal( 'timestamp' );
+ $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
+ $this->mFile = $request->getVal( 'file' );
+
+ $posted = $request->wasPosted() &&
+ $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+ $this->mRestore = $request->getCheck( 'restore' ) && $posted;
+ $this->mPreview = $request->getCheck( 'preview' ) && $posted;
+ $this->mDiff = $request->getCheck( 'diff' );
+ $this->mComment = $request->getText( 'wpComment' );
+ $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
+
+ if( $par != "" ) {
+ $this->mTarget = $par;
+ }
+ if ( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) {
+ $this->mAllowed = true;
+ } else {
+ $this->mAllowed = false;
+ $this->mTimestamp = '';
+ $this->mRestore = false;
+ }
+ if ( $this->mTarget !== "" ) {
+ $this->mTargetObj = Title::newFromURL( $this->mTarget );
+ } else {
+ $this->mTargetObj = NULL;
+ }
+ if( $this->mRestore ) {
+ $timestamps = array();
+ $this->mFileVersions = array();
+ foreach( $_REQUEST as $key => $val ) {
+ $matches = array();
+ if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
+ array_push( $timestamps, $matches[1] );
+ }
+
+ if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
+ $this->mFileVersions[] = intval( $matches[1] );
+ }
+ }
+ rsort( $timestamps );
+ $this->mTargetTimestamp = $timestamps;
+ }
+ }
+
+ function execute() {
+ global $wgOut, $wgUser;
+ if ( $this->mAllowed ) {
+ $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+ } else {
+ $wgOut->setPagetitle( wfMsg( "viewdeletedpage" ) );
+ }
+
+ if( is_null( $this->mTargetObj ) ) {
+ # Not all users can just browse every deleted page from the list
+ if( $wgUser->isAllowed( 'browsearchive' ) ) {
+ $this->showSearchForm();
+
+ # List undeletable articles
+ if( $this->mSearchPrefix ) {
+ $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
+ $this->showList( $result );
+ }
+ } else {
+ $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
+ }
+ return;
+ }
+ if( $this->mTimestamp !== '' ) {
+ return $this->showRevision( $this->mTimestamp );
+ }
+ 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' );
+ return false;
+ } else {
+ return $this->showFile( $this->mFile );
+ }
+ }
+ if( $this->mRestore && $this->mAction == "submit" ) {
+ return $this->undelete();
+ }
+ return $this->showHistory();
+ }
+
+ function showSearchForm() {
+ global $wgOut, $wgScript;
+ $wgOut->addWikiMsg( 'undelete-header' );
+
+ $wgOut->addHtml(
+ Xml::openElement( 'form', array(
+ 'method' => 'get',
+ 'action' => $wgScript ) ) .
+ '<fieldset>' .
+ Xml::element( 'legend', array(),
+ wfMsg( 'undelete-search-box' ) ) .
+ Xml::hidden( 'title',
+ SpecialPage::getTitleFor( 'Undelete' )->getPrefixedDbKey() ) .
+ Xml::inputLabel( wfMsg( 'undelete-search-prefix' ),
+ 'prefix', 'prefix', 20,
+ $this->mSearchPrefix ) .
+ Xml::submitButton( wfMsg( 'undelete-search-submit' ) ) .
+ '</fieldset>' .
+ '</form>' );
+ }
+
+ // Generic list of deleted pages
+ private function showList( $result ) {
+ global $wgLang, $wgContLang, $wgUser, $wgOut;
+
+ if( $result->numRows() == 0 ) {
+ $wgOut->addWikiMsg( 'undelete-no-results' );
+ return;
+ }
+
+ $wgOut->addWikiMsg( "undeletepagetext" );
+
+ $sk = $wgUser->getSkin();
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $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() );
+ #$revs = wfMsgHtml( 'undeleterevisions', $wgLang->formatNum( $row->count ) );
+ $revs = wfMsgExt( 'undeleterevisions',
+ array( 'parseinline' ),
+ $wgLang->formatNum( $row->count ) );
+ $wgOut->addHtml( "<li>{$link} ({$revs})</li>\n" );
+ }
+ $result->free();
+ $wgOut->addHTML( "</ul>\n" );
+
+ return true;
+ }
+
+ private function showRevision( $timestamp ) {
+ global $wgLang, $wgUser, $wgOut;
+ $self = SpecialPage::getTitleFor( 'Undelete' );
+ $skin = $wgUser->getSkin();
+
+ if(!preg_match("/[0-9]{14}/",$timestamp)) return 0;
+
+ $archive = new PageArchive( $this->mTargetObj );
+ $rev = $archive->getRevision( $timestamp );
+
+ if( !$rev ) {
+ $wgOut->addWikiMsg( 'undeleterevision-missing' );
+ return;
+ }
+
+ if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
+ if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+ $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+ return;
+ } else {
+ $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
+ $wgOut->addHTML( '<br/>' );
+ // and we are allowed to see...
+ }
+ }
+
+ $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
+
+ $link = $skin->makeKnownLinkObj(
+ SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ),
+ htmlspecialchars( $this->mTargetObj->getPrefixedText() )
+ );
+ $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) );
+ $user = $skin->revUserTools( $rev );
+
+ if( $this->mDiff ) {
+ $previousRev = $archive->getPreviousRevision( $timestamp );
+ if( $previousRev ) {
+ $this->showDiff( $previousRev, $rev );
+ if( $wgUser->getOption( 'diffonly' ) ) {
+ return;
+ } else {
+ $wgOut->addHtml( '<hr />' );
+ }
+ } else {
+ $wgOut->addHtml( wfMsgHtml( 'undelete-nodiff' ) );
+ }
+ }
+
+ $wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '</p>' );
+
+ wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
+
+ if( $this->mPreview ) {
+ $wgOut->addHtml( "<hr />\n" );
+
+ //Hide [edit]s
+ $popts = $wgOut->parserOptions();
+ $popts->setEditSection( false );
+ $wgOut->parserOptions( $popts );
+ $wgOut->addWikiTextTitleTidy( $rev->revText(), $this->mTargetObj, true );
+ }
+
+ $wgOut->addHtml(
+ wfElement( 'textarea', array(
+ 'readonly' => 'readonly',
+ 'cols' => intval( $wgUser->getOption( 'cols' ) ),
+ 'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
+ $rev->revText() . "\n" ) .
+ wfOpenElement( 'div' ) .
+ wfOpenElement( 'form', array(
+ 'method' => 'post',
+ 'action' => $self->getLocalURL( "action=submit" ) ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'target',
+ 'value' => $this->mTargetObj->getPrefixedDbKey() ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'timestamp',
+ 'value' => $timestamp ) ) .
+ wfElement( 'input', array(
+ 'type' => 'hidden',
+ 'name' => 'wpEditToken',
+ 'value' => $wgUser->editToken() ) ) .
+ wfElement( 'input', array(
+ 'type' => 'submit',
+ 'name' => 'preview',
+ 'value' => wfMsg( 'showpreview' ) ) ) .
+ wfElement( 'input', array(
+ 'name' => 'diff',
+ 'type' => 'submit',
+ 'value' => wfMsg( 'showdiff' ) ) ) .
+ wfCloseElement( 'form' ) .
+ wfCloseElement( 'div' ) );
+ }
+
+ /**
+ * Build a diff display between this and the previous either deleted
+ * or non-deleted edit.
+ * @param Revision $previousRev
+ * @param Revision $currentRev
+ * @return string HTML
+ */
+ function showDiff( $previousRev, $currentRev ) {
+ global $wgOut, $wgUser;
+
+ $diffEngine = new DifferenceEngine();
+ $diffEngine->showDiffStyle();
+ $wgOut->addHtml(
+ "<div>" .
+ "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+ "<col class='diff-marker' />" .
+ "<col class='diff-content' />" .
+ "<col class='diff-marker' />" .
+ "<col class='diff-content' />" .
+ "<tr>" .
+ "<td colspan='2' width='50%' align='center' class='diff-otitle'>" .
+ $this->diffHeader( $previousRev ) .
+ "</td>" .
+ "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" .
+ $this->diffHeader( $currentRev ) .
+ "</td>" .
+ "</tr>" .
+ $diffEngine->generateDiffBody(
+ $previousRev->getText(), $currentRev->getText() ) .
+ "</table>" .
+ "</div>\n" );
+
+ }
+
+ private function diffHeader( $rev ) {
+ global $wgUser, $wgLang, $wgLang;
+ $sk = $wgUser->getSkin();
+ $isDeleted = !( $rev->getId() && $rev->getTitle() );
+ if( $isDeleted ) {
+ /// @fixme $rev->getTitle() is null for deleted revs...?
+ $targetPage = SpecialPage::getTitleFor( 'Undelete' );
+ $targetQuery = 'target=' .
+ $this->mTargetObj->getPrefixedUrl() .
+ '&timestamp=' .
+ wfTimestamp( TS_MW, $rev->getTimestamp() );
+ } else {
+ /// @fixme getId() may return non-zero for deleted revs...
+ $targetPage = $rev->getTitle();
+ $targetQuery = 'oldid=' . $rev->getId();
+ }
+ return
+ '<div id="mw-diff-otitle1"><strong>' .
+ $sk->makeLinkObj( $targetPage,
+ wfMsgHtml( 'revisionasof',
+ $wgLang->timeanddate( $rev->getTimestamp(), true ) ),
+ $targetQuery ) .
+ ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) .
+ '</strong></div>' .
+ '<div id="mw-diff-otitle2">' .
+ $sk->revUserTools( $rev ) . '<br/>' .
+ '</div>' .
+ '<div id="mw-diff-otitle3">' .
+ $sk->revComment( $rev ) . '<br/>' .
+ '</div>';
+ }
+
+ /**
+ * Show a deleted file version requested by the visitor.
+ */
+ private function showFile( $key ) {
+ global $wgOut, $wgRequest;
+ $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
+ # nab the image, and Squid will serve it
+ $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+ $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 );
+ }
+
+ private function showHistory() {
+ global $wgLang, $wgUser, $wgOut;
+
+ $sk = $wgUser->getSkin();
+ if( $this->mAllowed ) {
+ $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+ } else {
+ $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
+ }
+
+ $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
+
+ $archive = new PageArchive( $this->mTargetObj );
+ /*
+ $text = $archive->getLastRevisionText();
+ if( is_null( $text ) ) {
+ $wgOut->addWikiMsg( "nohistory" );
+ return;
+ }
+ */
+ if ( $this->mAllowed ) {
+ $wgOut->addWikiMsg( "undeletehistory" );
+ $wgOut->addWikiMsg( "undeleterevdel" );
+ } else {
+ $wgOut->addWikiMsg( "undeletehistorynoadmin" );
+ }
+
+ # List all stored revisions
+ $revisions = $archive->listRevisions();
+ $files = $archive->listFiles();
+
+ $haveRevisions = $revisions && $revisions->numRows() > 0;
+ $haveFiles = $files && $files->numRows() > 0;
+
+ # Batch existence check on user and talk pages
+ if( $haveRevisions ) {
+ $batch = new LinkBatch();
+ while( $row = $revisions->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
+ }
+ $batch->execute();
+ $revisions->seek( 0 );
+ }
+ if( $haveFiles ) {
+ $batch = new LinkBatch();
+ while( $row = $files->fetchObject() ) {
+ $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
+ $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
+ }
+ $batch->execute();
+ $files->seek( 0 );
+ }
+
+ if ( $this->mAllowed ) {
+ $titleObj = SpecialPage::getTitleFor( "Undelete" );
+ $action = $titleObj->getLocalURL( "action=submit" );
+ # Start the form here
+ $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
+ $wgOut->addHtml( $top );
+ }
+
+ # Show relevant lines from the deletion log:
+ $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) . "\n" );
+ LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTargetObj->getPrefixedText() );
+
+ if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
+ # Format the user-visible controls (comment field, submission button)
+ # in a nice little table
+ if( $wgUser->isAllowed( 'suppressrevision' ) ) {
+ $unsuppressBox =
+ "<tr>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg('revdelete-unsuppress'), 'wpUnsuppress',
+ 'mw-undelete-unsuppress', $this->mUnsuppress ).
+ "</td>
+ </tr>";
+ } else {
+ $unsuppressBox = "";
+ }
+ $table =
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'undelete-fieldset-title' ) ).
+ Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
+ "<tr>
+ <td colspan='2'>" .
+ wfMsgWikiHtml( 'undeleteextrahelp' ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
+ Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) .
+ "</td>
+ </tr>" .
+ $unsuppressBox .
+ Xml::closeElement( 'table' ) .
+ Xml::closeElement( 'fieldset' );
+
+ $wgOut->addHtml( $table );
+ }
+
+ $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'history' ) ) . "\n" );
+
+ if( $haveRevisions ) {
+ # The page's stored (deleted) history:
+ $wgOut->addHTML("<ul>");
+ $target = urlencode( $this->mTarget );
+ $remaining = $revisions->numRows();
+ $earliestLiveTime = $this->getEarliestTime( $this->mTargetObj );
+
+ while( $row = $revisions->fetchObject() ) {
+ $remaining--;
+ $wgOut->addHTML( $this->formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) );
+ }
+ $revisions->free();
+ $wgOut->addHTML("</ul>");
+ } else {
+ $wgOut->addWikiMsg( "nohistory" );
+ }
+
+ if( $haveFiles ) {
+ $wgOut->addHtml( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" );
+ $wgOut->addHtml( "<ul>" );
+ while( $row = $files->fetchObject() ) {
+ $wgOut->addHTML( $this->formatFileRow( $row, $sk ) );
+ }
+ $files->free();
+ $wgOut->addHTML( "</ul>" );
+ }
+
+ if ( $this->mAllowed ) {
+ # Slip in the hidden controls here
+ $misc = Xml::hidden( 'target', $this->mTarget );
+ $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
+ $misc .= Xml::closeElement( 'form' );
+ $wgOut->addHtml( $misc );
+ }
+
+ return true;
+ }
+
+ 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 ) );
+
+ $stxt = '';
+ $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
+ if( $this->mAllowed ) {
+ $checkBox = Xml::check( "ts$ts" );
+ $titleObj = SpecialPage::getTitleFor( "Undelete" );
+ $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
+ # Last link
+ if( !$rev->userCan( Revision::DELETED_TEXT ) ) {
+ $last = wfMsgHtml('diff');
+ } else if( $remaining > 0 || ($earliestLiveTime && $ts > $earliestLiveTime) ) {
+ $last = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml('diff'),
+ "target=" . $this->mTargetObj->getPrefixedUrl() . "&timestamp=$ts&diff=prev" );
+ } else {
+ $last = wfMsgHtml('diff');
+ }
+ } else {
+ $checkBox = '';
+ $pageLink = $wgLang->timeanddate( $ts, true );
+ $last = wfMsgHtml('diff');
+ }
+ $userLink = $sk->revUserTools( $rev );
+
+ if(!is_null($size = $row->ar_len)) {
+ $stxt = $sk->formatRevisionSize( $size );
+ }
+ $comment = $sk->revComment( $rev );
+ $revdlink = '';
+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+ // If revision was hidden from sysops
+ $del = wfMsgHtml('rev-delundel');
+ } else {
+ $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
+ $del = $sk->makeKnownLinkObj( $revdel,
+ wfMsgHtml('rev-delundel'),
+ 'target=' . $this->mTargetObj->getPrefixedUrl() . "&artimestamp=$ts" );
+ // Bolden oversighted content
+ if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
+ $del = "<strong>$del</strong>";
+ }
+ $revdlink = "<tt>(<small>$del</small>)</tt>";
+ }
+
+ return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
+ }
+
+ private function formatFileRow( $row, $sk ) {
+ global $wgUser, $wgLang;
+
+ $file = ArchivedFile::newFromRow( $row );
+
+ $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
+ if( $this->mAllowed && $row->fa_storage_key ) {
+ $checkBox = Xml::check( "fileid" . $row->fa_id );
+ $key = urlencode( $row->fa_storage_key );
+ $target = urlencode( $this->mTarget );
+ $titleObj = SpecialPage::getTitleFor( "Undelete" );
+ $pageLink = $this->getFileLink( $file, $titleObj, $ts, $key, $sk );
+ } else {
+ $checkBox = '';
+ $pageLink = $wgLang->timeanddate( $ts, true );
+ }
+ $userLink = $this->getFileUser( $file, $sk );
+ $data =
+ wfMsg( 'widthheight',
+ $wgLang->formatNum( $row->fa_width ),
+ $wgLang->formatNum( $row->fa_height ) ) .
+ ' (' .
+ wfMsg( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
+ ')';
+ $data = htmlspecialchars( $data );
+ $comment = $this->getFileComment( $file, $sk );
+ $revdlink = '';
+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
+ // If revision was hidden from sysops
+ $del = wfMsgHtml('rev-delundel');
+ } else {
+ $del = $sk->makeKnownLinkObj( $revdel,
+ wfMsgHtml('rev-delundel'),
+ 'target=' . $this->mTargetObj->getPrefixedUrl() .
+ '&fileid=' . $row->fa_id );
+ // Bolden oversighted content
+ if( $file->isDeleted( File::DELETED_RESTRICTED ) )
+ $del = "<strong>$del</strong>";
+ }
+ $revdlink = "<tt>(<small>$del</small>)</tt>";
+ }
+ return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
+ }
+
+ private function getEarliestTime( $title ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ if( $title->exists() ) {
+ $min = $dbr->selectField( 'revision',
+ 'MIN(rev_timestamp)',
+ array( 'rev_page' => $title->getArticleId() ),
+ __METHOD__ );
+ return wfTimestampOrNull( TS_MW, $min );
+ }
+ return null;
+ }
+
+ /**
+ * Fetch revision text link if it's available to all users
+ * @return string
+ */
+ function getPageLink( $rev, $titleObj, $ts, $sk ) {
+ global $wgLang;
+
+ if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+ return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+ } else {
+ $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
+ "target=".$this->mTargetObj->getPrefixedUrl()."&timestamp=$ts" );
+ if( $rev->isDeleted(Revision::DELETED_TEXT) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ /**
+ * Fetch image view link if it's available to all users
+ * @return string
+ */
+ function getFileLink( $file, $titleObj, $ts, $key, $sk ) {
+ global $wgLang;
+
+ 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" );
+ if( $file->isDeleted(File::DELETED_FILE) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ /**
+ * Fetch file's user id if it's available to this user
+ * @return string
+ */
+ function getFileUser( $file, $sk ) {
+ if( !$file->userCan(File::DELETED_USER) ) {
+ return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+ } else {
+ $link = $sk->userLink( $file->getRawUser(), $file->getRawUserText() ) .
+ $sk->userToolLinks( $file->getRawUser(), $file->getRawUserText() );
+ if( $file->isDeleted(File::DELETED_USER) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ /**
+ * Fetch file upload comment if it's available to this user
+ * @return string
+ */
+ function getFileComment( $file, $sk ) {
+ if( !$file->userCan(File::DELETED_COMMENT) ) {
+ return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
+ } else {
+ $link = $sk->commentBlock( $file->getRawDescription() );
+ if( $file->isDeleted(File::DELETED_COMMENT) )
+ $link = '<span class="history-deleted">' . $link . '</span>';
+ return $link;
+ }
+ }
+
+ function undelete() {
+ global $wgOut, $wgUser;
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+ if( !is_null( $this->mTargetObj ) ) {
+ $archive = new PageArchive( $this->mTargetObj );
+ $ok = $archive->undelete(
+ $this->mTargetTimestamp,
+ $this->mComment,
+ $this->mFileVersions,
+ $this->mUnsuppress );
+
+ if( is_array($ok) ) {
+ if ( $ok[1] ) // Undeleted file count
+ wfRunHooks( 'FileUndeleteComplete', array(
+ $this->mTargetObj, $this->mFileVersions,
+ $wgUser, $this->mComment) );
+
+ $skin = $wgUser->getSkin();
+ $link = $skin->makeKnownLinkObj( $this->mTargetObj );
+ $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
+ } else {
+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+ $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
+ }
+
+ // Show file deletion warnings and errors
+ $status = $archive->getFileStatus();
+ if( $status && !$status->isGood() ) {
+ $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
+ }
+ } else {
+ $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+ }
+ return false;
+ }
+}
diff --git a/includes/specials/SpecialUnlockdb.php b/includes/specials/SpecialUnlockdb.php
new file mode 100644
index 00000000..0bf7e5aa
--- /dev/null
+++ b/includes/specials/SpecialUnlockdb.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialUnlockdb() {
+ global $wgUser, $wgOut, $wgRequest;
+
+ if( !$wgUser->isAllowed( 'siteadmin' ) ) {
+ $wgOut->permissionRequired( 'siteadmin' );
+ return;
+ }
+
+ $action = $wgRequest->getVal( 'action' );
+ $f = new DBUnlockForm();
+
+ if ( "success" == $action ) {
+ $f->showSuccess();
+ } else if ( "submit" == $action && $wgRequest->wasPosted() &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+ $f->doSubmit();
+ } else {
+ $f->showForm( "" );
+ }
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+class DBUnlockForm {
+ function showForm( $err )
+ {
+ global $wgOut, $wgUser;
+
+ global $wgReadOnlyFile;
+ if( !file_exists( $wgReadOnlyFile ) ) {
+ $wgOut->addWikiMsg( 'databasenotlocked' );
+ return;
+ }
+
+ $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
+ $wgOut->addWikiMsg( "unlockdbtext" );
+
+ if ( "" != $err ) {
+ $wgOut->setSubtitle( wfMsg( "formerror" ) );
+ $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
+ }
+ $lc = htmlspecialchars( wfMsg( "unlockconfirm" ) );
+ $lb = htmlspecialchars( wfMsg( "unlockbtn" ) );
+ $titleObj = SpecialPage::getTitleFor( "Unlockdb" );
+ $action = $titleObj->escapeLocalURL( "action=submit" );
+ $token = htmlspecialchars( $wgUser->editToken() );
+
+ $wgOut->addHTML( <<<END
+
+<form id="unlockdb" method="post" action="{$action}">
+<table border="0">
+ <tr>
+ <td align="right">
+ <input type="checkbox" name="wpLockConfirm" />
+ </td>
+ <td align="left">{$lc}</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td align="left">
+ <input type="submit" name="wpLock" value="{$lb}" />
+ </td>
+ </tr>
+</table>
+<input type="hidden" name="wpEditToken" value="{$token}" />
+</form>
+END
+);
+
+ }
+
+ function doSubmit() {
+ global $wgOut, $wgRequest, $wgReadOnlyFile;
+
+ $wpLockConfirm = $wgRequest->getCheck( 'wpLockConfirm' );
+ if ( ! $wpLockConfirm ) {
+ $this->showForm( wfMsg( "locknoconfirm" ) );
+ return;
+ }
+ if ( @! unlink( $wgReadOnlyFile ) ) {
+ $wgOut->showFileDeleteError( $wgReadOnlyFile );
+ return;
+ }
+ $titleObj = SpecialPage::getTitleFor( "Unlockdb" );
+ $success = $titleObj->getFullURL( "action=success" );
+ $wgOut->redirect( $success );
+ }
+
+ function showSuccess() {
+ global $wgOut;
+ global $ip;
+
+ $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
+ $wgOut->setSubtitle( wfMsg( "unlockdbsuccesssub" ) );
+ $wgOut->addWikiMsg( "unlockdbsuccesstext", $ip );
+ }
+}
diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php
new file mode 100644
index 00000000..406f7944
--- /dev/null
+++ b/includes/specials/SpecialUnusedcategories.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class UnusedCategoriesPage extends QueryPage {
+
+ function isExpensive() { return true; }
+
+ function getName() {
+ return 'Unusedcategories';
+ }
+
+ function getPageHeader() {
+ return wfMsgExt( 'unusedcategoriestext', array( 'parse' ) );
+ }
+
+ function getSQL() {
+ $NScat = NS_CATEGORY;
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' );
+ return "SELECT 'Unusedcategories' as type,
+ {$NScat} as namespace, page_title as title, page_title as value
+ FROM $page
+ LEFT JOIN $categorylinks ON page_title=cl_to
+ WHERE cl_from IS NULL
+ AND page_namespace = {$NScat}
+ AND page_is_redirect = 0";
+ }
+
+ function formatResult( $skin, $result ) {
+ $title = Title::makeTitle( NS_CATEGORY, $result->title );
+ return $skin->makeLinkObj( $title, $title->getText() );
+ }
+}
+
+/** constructor */
+function wfSpecialUnusedCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $uc = new UnusedCategoriesPage();
+ return $uc->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php
new file mode 100644
index 00000000..d71b638f
--- /dev/null
+++ b/includes/specials/SpecialUnusedimages.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Unusedimages
+ * @ingroup SpecialPage
+ */
+class UnusedimagesPage extends ImageQueryPage {
+
+ function isExpensive() { return true; }
+
+ function getName() {
+ return 'Unusedimages';
+ }
+
+ function sortDescending() {
+ return false;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ global $wgCountCategorizedImagesAsUsed;
+ $dbr = wfGetDB( DB_SLAVE );
+
+ if ( $wgCountCategorizedImagesAsUsed ) {
+ list( $page, $image, $imagelinks, $categorylinks ) = $dbr->tableNamesN( 'page', 'image', 'imagelinks', 'categorylinks' );
+
+ return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
+ img_user, img_user_text, img_description
+ FROM ((($page AS I LEFT JOIN $categorylinks AS L ON I.page_id = L.cl_from)
+ LEFT JOIN $imagelinks AS P ON I.page_title = P.il_to)
+ INNER JOIN $image AS G ON I.page_title = G.img_name)
+ WHERE I.page_namespace = ".NS_IMAGE." AND L.cl_from IS NULL AND P.il_to IS NULL";
+ } else {
+ list( $image, $imagelinks ) = $dbr->tableNamesN( 'image','imagelinks' );
+
+ return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
+ img_user, img_user_text, img_description
+ FROM $image LEFT JOIN $imagelinks ON img_name=il_to WHERE il_to IS NULL ";
+ }
+ }
+
+ function getPageHeader() {
+ return wfMsgExt( 'unusedimagestext', array( 'parse') );
+ }
+
+}
+
+/**
+ * Entry point
+ */
+function wfSpecialUnusedimages() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $uip = new UnusedimagesPage();
+
+ return $uip->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUnusedtemplates.php b/includes/specials/SpecialUnusedtemplates.php
new file mode 100644
index 00000000..89acd09c
--- /dev/null
+++ b/includes/specials/SpecialUnusedtemplates.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Unusedtemplates
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright © 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @ingroup SpecialPage
+ */
+class UnusedtemplatesPage extends QueryPage {
+
+ function getName() { return( 'Unusedtemplates' ); }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+ function sortDescending() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $templatelinks) = $dbr->tableNamesN( 'page', 'templatelinks' );
+ $sql = "SELECT 'Unusedtemplates' AS type, page_title AS title,
+ page_namespace AS namespace, 0 AS value
+ FROM $page
+ LEFT JOIN $templatelinks
+ ON page_namespace = tl_namespace AND page_title = tl_title
+ WHERE page_namespace = 10 AND tl_from IS NULL
+ AND page_is_redirect = 0";
+ return $sql;
+ }
+
+ function formatResult( $skin, $result ) {
+ $title = Title::makeTitle( NS_TEMPLATE, $result->title );
+ $pageLink = $skin->makeKnownLinkObj( $title, '', 'redirect=no' );
+ $wlhLink = $skin->makeKnownLinkObj(
+ SpecialPage::getTitleFor( 'Whatlinkshere' ),
+ wfMsgHtml( 'unusedtemplateswlh' ),
+ 'target=' . $title->getPrefixedUrl() );
+ return wfSpecialList( $pageLink, $wlhLink );
+ }
+
+ function getPageHeader() {
+ return wfMsgExt( 'unusedtemplatestext', array( 'parse' ) );
+ }
+
+}
+
+function wfSpecialUnusedtemplates() {
+ list( $limit, $offset ) = wfCheckLimits();
+ $utp = new UnusedtemplatesPage();
+ $utp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php
new file mode 100644
index 00000000..64ab3729
--- /dev/null
+++ b/includes/specials/SpecialUnwatchedpages.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page that displays a list of pages that are not on anyones watchlist.
+ * Implements Special:Unwatchedpages
+ *
+ * @ingroup SpecialPage
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class UnwatchedpagesPage extends QueryPage {
+
+ function getName() { return 'Unwatchedpages'; }
+ function isExpensive() { return true; }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $watchlist ) = $dbr->tableNamesN( 'page', 'watchlist' );
+ $mwns = NS_MEDIAWIKI;
+ return
+ "
+ SELECT
+ 'Unwatchedpages' as type,
+ page_namespace as namespace,
+ page_title as title,
+ page_namespace as value
+ FROM $page
+ LEFT JOIN $watchlist ON wl_namespace = page_namespace AND page_title = wl_title
+ WHERE wl_title IS NULL AND page_is_redirect = 0 AND page_namespace<>$mwns
+ ";
+ }
+
+ function sortDescending() { return false; }
+
+ function formatResult( $skin, $result ) {
+ global $wgContLang;
+
+ $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' );
+
+ return wfSpecialList( $plink, $wlink );
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUnwatchedpages() {
+ global $wgUser, $wgOut;
+
+ if ( ! $wgUser->isAllowed( 'unwatchedpages' ) )
+ return $wgOut->permissionRequired( 'unwatchedpages' );
+
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new UnwatchedpagesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php
new file mode 100644
index 00000000..8fe2f52f
--- /dev/null
+++ b/includes/specials/SpecialUpload.php
@@ -0,0 +1,1755 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+
+/**
+ * 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_LENGHT_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;
+ /**#@-*/
+
+ /**
+ * Constructor : initialise object
+ * Get data POSTed through the form and assign them to the object
+ * @param $request Data posted.
+ */
+ function UploadForm( &$request ) {
+ global $wgAllowCopyUploads;
+ $this->mDesiredDestName = $request->getText( 'wpDestFile' );
+ $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' );
+ $this->mComment = $request->getText( 'wpUploadDescription' );
+
+ if( !$request->wasPosted() ) {
+ # GET requests just give the main form; no data except destination
+ # filename and description
+ return;
+ }
+
+ # Placeholders for text injection by hooks (empty per default)
+ $this->uploadFormTextTop = "";
+ $this->uploadFormTextAfterSummary = "";
+
+ $this->mReUpload = $request->getCheck( 'wpReUpload' );
+ $this->mUploadClicked = $request->getCheck( 'wpUpload' );
+
+ $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 );
+ }
+ }
+ }
+
+ /**
+ * 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
+ }
+
+ /**
+ * Copy a web file to a temporary file
+ * @access private
+ */
+ 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 );
+ }
+
+ /**
+ * Safe copy from URL
+ * Returns true if there was an error, false otherwise
+ */
+ private function curlCopy( $url, $dest ) {
+ global $wgUser, $wgOut;
+
+ if( !$wgUser->isAllowed( 'upload_by_url' ) ) {
+ $wgOut->permissionRequired( 'upload_by_url' );
+ return true;
+ }
+
+ # 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;
+ }
+
+ $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);
+ 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;
+ }
+
+ /**
+ * Callback function for CURL-based web transfer
+ * Write data to file unless we've passed the length limit;
+ * if so, abort immediately.
+ * @access private
+ */
+ function uploadCurlCallback( $ch, $data ) {
+ global $wgMaxUploadSize;
+ $length = strlen( $data );
+ $this->mFileSize += $length;
+ if( $this->mFileSize > $wgMaxUploadSize ) {
+ return 0;
+ }
+ fwrite( $this->mCurlDestHandle, $data );
+ return $length;
+ }
+
+ /**
+ * Start doing stuff
+ * @access public
+ */
+ function execute() {
+ global $wgUser, $wgOut;
+ global $wgEnableUploads;
+
+ # Check uploading enabled
+ if( !$wgEnableUploads ) {
+ $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) );
+ return;
+ }
+
+ # Check permissions
+ if( !$wgUser->isAllowed( 'upload' ) ) {
+ if( !$wgUser->isLoggedIn() ) {
+ $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+ } else {
+ $wgOut->permissionRequired( 'upload' );
+ }
+ return;
+ }
+
+ # Check blocks
+ if( $wgUser->isBlocked() ) {
+ $wgOut->blockedPage();
+ return;
+ }
+
+ if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ if( $this->mReUpload ) {
+ if( !$this->unsaveUploadedFile() ) {
+ return;
+ }
+ # Because it is probably checked and shouldn't be
+ $this->mIgnoreWarning = false;
+
+ $this->mainUploadForm();
+ } else if( 'submit' == $this->mAction || $this->mUploadClicked ) {
+ $this->processUpload();
+ } else {
+ $this->mainUploadForm();
+ }
+
+ $this->cleanupTempFile();
+ }
+
+ /**
+ * Do the upload
+ * Checks are made in SpecialUpload::execute()
+ *
+ * @access private
+ */
+ 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_LENGHT_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;
+
+ case self::OVERWRITE_EXISTING_FILE:
+ $errorText = $details['overwrite'];
+ $this->uploadError( $wgOut->parse( $errorText ) );
+ break;
+
+ 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 ),
+ implode(
+ wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
+ $wgFileExtensions
+ ),
+ $wgLang->formatNum( count($wgFileExtensions) )
+ )
+ );
+ 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;
+
+ default:
+ throw new MWException( __METHOD__ . ": Unknown value `{$value}`" );
+ }
+ }
+
+ /**
+ * Really do the upload
+ * Checks are made in SpecialUpload::execute()
+ *
+ * @param array $resultDetails contains result-specific dict of additional values
+ *
+ * @access private
+ */
+ function internalProcessUpload( &$resultDetails ) {
+ global $wgUser;
+
+ if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) )
+ {
+ wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
+ return self::BEFORE_PROCESSING;
+ }
+
+ /**
+ * If there was no filename or a zero size given, give up quick.
+ */
+ if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) {
+ return self::EMPTY_FILE;
+ }
+
+ /* Check for curl error */
+ if( $this->mCurlError ) {
+ return self::BEFORE_PROCESSING;
+ }
+
+ /**
+ * 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 );
+
+ /**
+ * 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 = '';
+ }
+
+ # 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];
+ }
+
+ if( strlen( $partname ) < 1 ) {
+ return self::MIN_LENGHT_PARTNAME;
+ }
+
+ $nt = Title::makeTitleSafe( NS_IMAGE, $filtered );
+ if( is_null( $nt ) ) {
+ $resultDetails = array( 'filtered' => $filtered );
+ return self::ILLEGAL_FILENAME;
+ }
+ $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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /* 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;
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+
+ /**
+ * Check for non-fatal conditions
+ */
+ if ( ! $this->mIgnoreWarning ) {
+ $warning = '';
+
+ global $wgCapitalLinks;
+ if( $wgCapitalLinks ) {
+ $filtered = ucfirst( $filtered );
+ }
+ if( $basename != $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 ),
+ implode(
+ wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
+ $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 );
+
+ 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;
+ }
+ }
+
+ /**
+ * Try actually saving the thing...
+ * It will show an error form on failure.
+ */
+ $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;
+ }
+ }
+
+ /**
+ * 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
+ */
+ 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 = '';
+ } else {
+ $n = strrpos( $file->getName(), '.' );
+ $rawExtension = substr( $file->getName(), $n + 1 );
+ $partname = substr( $file->getName(), 0, $n );
+ }
+
+ $sk = $wgUser->getSkin();
+
+ 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_IMAGE, $partname . '.' . $file->getExtension() );
+ $file_lc = wfLocalFile( $nt_lc );
+ } else {
+ $file_lc = false;
+ }
+
+ 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 = '';
+ }
+
+ $warning .= '<li>' .
+ wfMsgExt( 'fileexists-extension', 'parsemag',
+ $file->getTitle()->getPrefixedText(), $dlink ) .
+ '</li>' . $dlink2;
+
+ } 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>';
+ } else {
+ $dlink2 = '';
+ }
+
+ $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>';
+ }
+ }
+
+ $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;
+ }
+ }
+
+ if ( $file->wasDeleted() && !$file->exists() ) {
+ # 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>';
+ }
+ return $warning;
+ }
+
+ /**
+ * Get a list of warnings
+ *
+ * @param string local filename, e.g. 'file exists', 'non-descriptive filename'
+ * @return array list of warning messages
+ */
+ static function ajaxGetExistsWarning( $filename ) {
+ $file = wfFindFile( $filename );
+ if( !$file ) {
+ // Force local file so we have an object to do further checks against
+ // if there isn't an exact match...
+ $file = wfLocalFile( $filename );
+ }
+ $s = '&nbsp;';
+ if ( $file ) {
+ $warning = self::getExistsWarning( $file );
+ if ( $warning !== '' ) {
+ $s = "<ul>$warning</ul>";
+ }
+ }
+ 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_IMAGE, '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.
+ */
+ function getDupeWarning( $tempfile ) {
+ $hash = File::sha1Base36( $tempfile );
+ $dupes = RepoGroup::singleton()->findBySha1( $hash );
+ if( $dupes ) {
+ global $wgOut;
+ $msg = "<gallery>";
+ foreach( $dupes as $file ) {
+ $title = $file->getTitle();
+ $msg .= $title->getPrefixedText() .
+ "|" . $title->getText() . "\n";
+ }
+ $msg .= "</gallery>";
+ return "<li>" .
+ wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) .
+ $wgOut->parse( $msg ) .
+ "</li>\n";
+ } 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;
+ }
+ }
+
+ /**
+ * 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 );
+
+ if( !$stash ) {
+ # Couldn't save the file.
+ return false;
+ }
+
+ $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;
+ }
+
+ /**
+ * Remove a temporarily kept file stashed by saveTempUploadedFile().
+ * @access private
+ * @return success
+ */
+ function unsaveUploadedFile() {
+ global $wgOut;
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $success = $repo->freeTemp( $this->mTempPath );
+ if ( ! $success ) {
+ $wgOut->showFileDeleteError( $this->mTempPath );
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+
+ /**
+ * @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
+ */
+ 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;
+ }
+
+ $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
+ $wgOut->addHTML( '<ul class="warning">' . $warning . "</ul>\n" );
+
+ $titleObj = SpecialPage::getTitleFor( 'Upload' );
+
+ if ( $wgUseCopyrightUpload ) {
+ $copyright = Xml::hidden( 'wpUploadCopyStatus', $this->mCopyrightStatus ) . "\n" .
+ Xml::hidden( 'wpUploadSource', $this->mCopyrightSource ) . "\n";
+ } else {
+ $copyright = '';
+ }
+
+ $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"
+ );
+ }
+
+ /**
+ * Displays the main upload form, optionally with a highlighted
+ * error message up at the top.
+ *
+ * @param string $msg as HTML
+ * @access private
+ */
+ 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" );
+ return false;
+ }
+
+ if( $this->mDesiredDestName ) {
+ $title = Title::makeTitleSafe( NS_IMAGE, $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->isDeleted() > 0 && !$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" );
+
+ # Print a list of allowed file extensions, if so configured. We ignore
+ # MIME type here, it's incomprehensible to most people and too long.
+ global $wgCheckFileExtensions, $wgStrictFileExtensions,
+ $wgFileExtensions, $wgFileBlacklist;
+
+ $allowedExtensions = '';
+ if( $wgCheckFileExtensions ) {
+ $delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) );
+ if( $wgStrictFileExtensions ) {
+ # Everything not permitted is banned
+ $extensionsList =
+ '<div id="mw-upload-permitted">' .
+ wfMsgWikiHtml( 'upload-permitted', implode( $wgFileExtensions, $delim ) ) .
+ "</div>\n";
+ } else {
+ # We have to list both preferred and prohibited
+ $extensionsList =
+ '<div id="mw-upload-preferred">' .
+ wfMsgWikiHtml( 'upload-preferred', implode( $wgFileExtensions, $delim ) ) .
+ "</div>\n" .
+ '<div id="mw-upload-prohibited">' .
+ wfMsgWikiHtml( 'upload-prohibited', implode( $wgFileBlacklist, $delim ) ) .
+ "</div>\n";
+ }
+ } else {
+ # Everything is permitted.
+ $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' ) );
+
+ $summary = wfMsgExt( 'fileuploadsummary', '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"'
+ : '';
+ $warningChecked = $this->mIgnoreWarning ? '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'>&nbsp;</td></tr>";
+ $destOnkeyup = 'onkeyup="wgUploadWarningObj.keypress();"';
+ } else {
+ $warningRow = '';
+ $destOnkeyup = '';
+ }
+
+ $encComment = htmlspecialchars( $this->mComment );
+
+ $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'>
+ <input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='60'
+ value=\"{$encDestName}\" onchange='toggleFilenameFiller()' $destOnkeyup />
+ </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>"
+ );
+
+ if ( $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 ( $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>"
+ );
+ }
+
+ $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->addWikiText( wfMsgForContent( 'edittools' ) );
+ $wgOut->addHTML( "
+ </td>
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ Xml::hidden( 'wpDestFileWarningAck', '', array( 'id' => 'wpDestFileWarningAck' ) ) .
+ 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
+ */
+ 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
+ */
+ 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)
+ && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
+ return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) );
+ }
+ }
+
+ #check for htmlish code and javascript
+ if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
+ 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;
+ }
+
+ /**
+ * 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
+ */
+ 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 );
+ }
+
+ $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(
+ '<body',
+ '<head',
+ '<html', #also in safari
+ '<img',
+ '<pre',
+ '<script', #also in safari
+ '<table'
+ );
+ if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
+ $tags[] = '<title';
+ }
+
+ foreach( $tags as $tag ) {
+ if( false !== strpos( $chunk, $tag ) ) {
+ return true;
+ }
+ }
+
+ /*
+ * 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;
+ }
+
+ /**
+ * 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" );
+ return $output;
+ }
+ }
+
+ /**
+ * 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();
+
+ // 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
+ */
+ function cleanupTempFile() {
+ if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
+ wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
+ unlink( $this->mTempPath );
+ }
+ }
+
+ /**
+ * 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
+ */
+ public static function userCanReUpload( User $user, $img ) {
+ if( $user->isAllowed( 'reupload' ) )
+ return true; // non-conditional
+ if( !$user->isAllowed( 'reupload-own' ) )
+ return false;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow('image',
+ /* SELECT */ 'img_user',
+ /* WHERE */ array( 'img_name' => $img )
+ );
+ if ( !$row )
+ return false;
+
+ return $user->getId() == $row->img_user;
+ }
+
+ /**
+ * Display an error with a wikitext description
+ */
+ function showError( $description ) {
+ global $wgOut;
+ $wgOut->setPageTitle( wfMsg( "internalerror" ) );
+ $wgOut->setRobotpolicy( "noindex,nofollow" );
+ $wgOut->setArticleRelated( false );
+ $wgOut->enableClientCache( false );
+ $wgOut->addWikiText( $description );
+ }
+
+ /**
+ * 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 id="mw-upload-deleted-warn">' );
+ $out->addWikiMsg( 'upload-wasdeleted' );
+ $out->addHTML(
+ $loglist->beginLogEventsList() .
+ $pager->getBody() .
+ $loglist->endLogEventsList()
+ );
+ $out->addHtml( '</div>' );
+ }
+ }
+}
diff --git a/includes/specials/SpecialUploadMogile.php b/includes/specials/SpecialUploadMogile.php
new file mode 100644
index 00000000..7ff8fda6
--- /dev/null
+++ b/includes/specials/SpecialUploadMogile.php
@@ -0,0 +1,135 @@
+<?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
new file mode 100644
index 00000000..27009eed
--- /dev/null
+++ b/includes/specials/SpecialUserlogin.php
@@ -0,0 +1,929 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialUserlogin( $par = '' ) {
+ global $wgRequest;
+ if( session_id() == '' ) {
+ wfSetupSession();
+ }
+
+ $form = new LoginForm( $wgRequest, $par );
+ $form->execute();
+}
+
+/**
+ * implements Special:Login
+ * @ingroup SpecialPage
+ */
+class LoginForm {
+
+ const SUCCESS = 0;
+ const NO_NAME = 1;
+ const ILLEGAL = 2;
+ const WRONG_PLUGIN_PASS = 3;
+ const NOT_EXISTS = 4;
+ const WRONG_PASS = 5;
+ const EMPTY_PASS = 6;
+ const RESET_PASS = 7;
+ const ABORTED = 8;
+ const CREATE_BLOCKED = 9;
+
+ var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
+ var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
+ var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage, $mSkipCookieCheck;
+
+ /**
+ * Constructor
+ * @param WebRequest $request A WebRequest object passed by reference
+ */
+ function LoginForm( &$request, $par = '' ) {
+ global $wgLang, $wgAllowRealName, $wgEnableEmail;
+ global $wgAuth;
+
+ $this->mType = ( $par == 'signup' ) ? $par : $request->getText( 'type' ); # Check for [[Special:Userlogin/signup]]
+ $this->mName = $request->getText( 'wpName' );
+ $this->mPassword = $request->getText( 'wpPassword' );
+ $this->mRetype = $request->getText( 'wpRetype' );
+ $this->mDomain = $request->getText( 'wpDomain' );
+ $this->mReturnTo = $request->getVal( 'returnto' );
+ $this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
+ $this->mPosted = $request->wasPosted();
+ $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
+ $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
+ && $wgEnableEmail;
+ $this->mMailmypassword = $request->getCheck( 'wpMailmypassword' )
+ && $wgEnableEmail;
+ $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
+ $this->mAction = $request->getVal( 'action' );
+ $this->mRemember = $request->getCheck( 'wpRemember' );
+ $this->mLanguage = $request->getText( 'uselang' );
+ $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' );
+
+ if( $wgEnableEmail ) {
+ $this->mEmail = $request->getText( 'wpEmail' );
+ } else {
+ $this->mEmail = '';
+ }
+ if( $wgAllowRealName ) {
+ $this->mRealName = $request->getText( 'wpRealName' );
+ } else {
+ $this->mRealName = '';
+ }
+
+ if( !$wgAuth->validDomain( $this->mDomain ) ) {
+ $this->mDomain = 'invaliddomain';
+ }
+ $wgAuth->setDomain( $this->mDomain );
+
+ # When switching accounts, it sucks to get automatically logged out
+ if( $this->mReturnTo == $wgLang->specialPage( 'Userlogout' ) ) {
+ $this->mReturnTo = '';
+ }
+ }
+
+ function execute() {
+ if ( !is_null( $this->mCookieCheck ) ) {
+ $this->onCookieRedirectCheck( $this->mCookieCheck );
+ return;
+ } else if( $this->mPosted ) {
+ if( $this->mCreateaccount ) {
+ return $this->addNewAccount();
+ } else if ( $this->mCreateaccountMail ) {
+ return $this->addNewAccountMailPassword();
+ } else if ( $this->mMailmypassword ) {
+ return $this->mailPassword();
+ } else if ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
+ return $this->processLogin();
+ }
+ }
+ $this->mainLoginForm( '' );
+ }
+
+ /**
+ * @private
+ */
+ function addNewAccountMailPassword() {
+ global $wgOut;
+
+ if ('' == $this->mEmail) {
+ $this->mainLoginForm( wfMsg( 'noemail', htmlspecialchars( $this->mName ) ) );
+ return;
+ }
+
+ $u = $this->addNewaccountInternal();
+
+ if ($u == NULL) {
+ return;
+ }
+
+ // Wipe the initial password and mail a temporary one
+ $u->setPassword( null );
+ $u->saveSettings();
+ $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' );
+
+ wfRunHooks( 'AddNewAccount', array( $u, true ) );
+
+ $wgOut->setPageTitle( wfMsg( 'accmailtitle' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+
+ if( WikiError::isError( $result ) ) {
+ $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
+ } else {
+ $wgOut->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() );
+ $wgOut->returnToMain( false );
+ }
+ $u = 0;
+ }
+
+
+ /**
+ * @private
+ */
+ function addNewAccount() {
+ global $wgUser, $wgEmailAuthentication;
+
+ # Create the account and abort if there's a problem doing so
+ $u = $this->addNewAccountInternal();
+ if( $u == NULL )
+ return;
+
+ # If we showed up language selection links, and one was in use, be
+ # smart (and sensible) and save that language as the user's preference
+ global $wgLoginLanguageSelector;
+ if( $wgLoginLanguageSelector && $this->mLanguage )
+ $u->setOption( 'language', $this->mLanguage );
+
+ # Send out an email authentication message if needed
+ if( $wgEmailAuthentication && User::isValidEmailAddr( $u->getEmail() ) ) {
+ global $wgOut;
+ $error = $u->sendConfirmationMail();
+ if( WikiError::isError( $error ) ) {
+ $wgOut->addWikiMsg( 'confirmemail_sendfailed', $error->getMessage() );
+ } else {
+ $wgOut->addWikiMsg( 'confirmemail_oncreate' );
+ }
+ }
+
+ # Save settings (including confirmation token)
+ $u->saveSettings();
+
+ # If not logged in, assume the new account as the current one and set session cookies
+ # then show a "welcome" message or a "need cookies" message as needed
+ if( $wgUser->isAnon() ) {
+ $wgUser = $u;
+ $wgUser->setCookies();
+ wfRunHooks( 'AddNewAccount', array( $wgUser ) );
+ if( $this->hasSessionCookie() ) {
+ return $this->successfulLogin( 'welcomecreation', $wgUser->getName(), false );
+ } else {
+ return $this->cookieRedirectCheck( 'new' );
+ }
+ } else {
+ # Confirm that the account was created
+ global $wgOut;
+ $self = SpecialPage::getTitleFor( 'Userlogin' );
+ $wgOut->setPageTitle( wfMsgHtml( 'accountcreated' ) );
+ $wgOut->setArticleRelated( false );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $wgOut->addHtml( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) );
+ $wgOut->returnToMain( false, $self );
+ wfRunHooks( 'AddNewAccount', array( $u ) );
+ return true;
+ }
+ }
+
+ /**
+ * @private
+ */
+ function addNewAccountInternal() {
+ global $wgUser, $wgOut;
+ global $wgEnableSorbs, $wgProxyWhitelist;
+ global $wgMemc, $wgAccountCreationThrottle;
+ global $wgAuth, $wgMinimalPasswordLength;
+ global $wgEmailConfirmToEdit;
+
+ // If the user passes an invalid domain, something is fishy
+ if( !$wgAuth->validDomain( $this->mDomain ) ) {
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ return false;
+ }
+
+ // If we are not allowing users to login locally, we should
+ // be checking to see if the user is actually able to
+ // authenticate to the authentication 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( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mName ) || !$wgAuth->authenticate( $this->mName, $this->mPassword ) ) ) {
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ return false;
+ }
+ }
+
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return false;
+ }
+
+ # Check permissions
+ if ( !$wgUser->isAllowed( 'createaccount' ) ) {
+ $this->userNotPrivilegedMessage();
+ return false;
+ } elseif ( $wgUser->isBlockedFromCreateAccount() ) {
+ $this->userBlockedMessage();
+ return false;
+ }
+
+ $ip = wfGetIP();
+ if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
+ $wgUser->inSorbsBlacklist( $ip ) )
+ {
+ $this->mainLoginForm( wfMsg( 'sorbs_create_account_reason' ) . ' (' . htmlspecialchars( $ip ) . ')' );
+ return;
+ }
+
+ # 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 ) ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return false;
+ }
+
+ if ( 0 != $u->idForName() ) {
+ $this->mainLoginForm( wfMsg( 'userexists' ) );
+ return false;
+ }
+
+ if ( 0 != strcmp( $this->mPassword, $this->mRetype ) ) {
+ $this->mainLoginForm( wfMsg( 'badretype' ) );
+ return false;
+ }
+
+ # check for minimal password length
+ if ( !$u->isValidPassword( $this->mPassword ) ) {
+ if ( !$this->mCreateaccountMail ) {
+ $this->mainLoginForm( wfMsgExt( 'passwordtooshort', array( 'parsemag' ), $wgMinimalPasswordLength ) );
+ return false;
+ } else {
+ # do not force a password for account creation by email
+ # set invalid password, it will be replaced later by a random generated password
+ $this->mPassword = null;
+ }
+ }
+
+ # if you need a confirmed email address to edit, then obviously you need an email address.
+ if ( $wgEmailConfirmToEdit && empty( $this->mEmail ) ) {
+ $this->mainLoginForm( wfMsg( 'noemailtitle' ) );
+ return false;
+ }
+
+ if( !empty( $this->mEmail ) && !User::isValidEmailAddr( $this->mEmail ) ) {
+ $this->mainLoginForm( wfMsg( 'invalidemailaddress' ) );
+ return false;
+ }
+
+ # Set some additional data so the AbortNewAccount hook can be
+ # used for more than just username validation
+ $u->setEmail( $this->mEmail );
+ $u->setRealName( $this->mRealName );
+
+ $abortError = '';
+ if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) {
+ // Hook point to add extra creation throttles and blocks
+ wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
+ $this->mainLoginForm( $abortError );
+ return false;
+ }
+
+ if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
+ $key = wfMemcKey( 'acctcreate', 'ip', $ip );
+ $value = $wgMemc->incr( $key );
+ if ( !$value ) {
+ $wgMemc->set( $key, 1, 86400 );
+ }
+ if ( $value > $wgAccountCreationThrottle ) {
+ $this->throttleHit( $wgAccountCreationThrottle );
+ return false;
+ }
+ }
+
+ if( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
+ $this->mainLoginForm( wfMsg( 'externaldberror' ) );
+ return false;
+ }
+
+ return $this->initUser( $u, false );
+ }
+
+ /**
+ * Actually add a user to the database.
+ * Give it a User object that has been initialised with a name.
+ *
+ * @param $u User object.
+ * @param $autocreate boolean -- true if this is an autocreation via auth plugin
+ * @return User object.
+ * @private
+ */
+ function initUser( $u, $autocreate ) {
+ global $wgAuth;
+
+ $u->addToDatabase();
+
+ if ( $wgAuth->allowPasswordChange() ) {
+ $u->setPassword( $this->mPassword );
+ }
+
+ $u->setEmail( $this->mEmail );
+ $u->setRealName( $this->mRealName );
+ $u->setToken();
+
+ $wgAuth->initUser( $u, $autocreate );
+
+ $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
+ $u->saveSettings();
+
+ # Update user count
+ $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+ $ssUpdate->doUpdate();
+
+ return $u;
+ }
+
+ /**
+ * Internally authenticate the login request.
+ *
+ * This may create a local account as a side effect if the
+ * authentication plugin allows transparent local account
+ * creation.
+ *
+ * @public
+ */
+ function authenticateUserData() {
+ global $wgUser, $wgAuth;
+ if ( '' == $this->mName ) {
+ return self::NO_NAME;
+ }
+
+ // Load $wgUser now, and check to see if we're logging in as the same name.
+ // This is necessary because loading $wgUser (say by calling getName()) calls
+ // the UserLoadFromSession hook, which potentially creates the user in the
+ // database. Until we load $wgUser, checking for user existence using
+ // User::newFromName($name)->getId() below will effectively be using stale data.
+ if ( $wgUser->getName() === $this->mName ) {
+ wfDebug( __METHOD__.": already logged in as {$this->mName}\n" );
+ return self::SUCCESS;
+ }
+ $u = User::newFromName( $this->mName );
+ if( is_null( $u ) || !User::isUsableName( $u->getName() ) ) {
+ return self::ILLEGAL;
+ }
+
+ $isAutoCreated = false;
+ if ( 0 == $u->getID() ) {
+ $status = $this->attemptAutoCreate( $u );
+ if ( $status !== self::SUCCESS ) {
+ return $status;
+ } else {
+ $isAutoCreated = true;
+ }
+ } else {
+ $u->load();
+ }
+
+ // Give general extensions, such as a captcha, a chance to abort logins
+ $abort = self::ABORTED;
+ if( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort ) ) ) {
+ return $abort;
+ }
+
+ if (!$u->checkPassword( $this->mPassword )) {
+ if( $u->checkTemporaryPassword( $this->mPassword ) ) {
+ // The e-mailed temporary password should not be used
+ // for actual logins; that's a very sloppy habit,
+ // and insecure if an attacker has a few seconds to
+ // click "search" on someone's open mail reader.
+ //
+ // Allow it to be used only to reset the password
+ // a single time to a new value, which won't be in
+ // the user's e-mail archives.
+ //
+ // For backwards compatibility, we'll still recognize
+ // it at the login form to minimize surprises for
+ // people who have been logging in with a temporary
+ // password for some time.
+ //
+ // As a side-effect, we can authenticate the user's
+ // e-mail address if it's not already done, since
+ // the temporary password was sent via e-mail.
+ //
+ if( !$u->isEmailConfirmed() ) {
+ $u->confirmEmail();
+ $u->saveSettings();
+ }
+
+ // At this point we just return an appropriate code
+ // indicating that the UI should show a password
+ // reset form; bot interfaces etc will probably just
+ // fail cleanly here.
+ //
+ $retval = self::RESET_PASS;
+ } else {
+ $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
+ }
+ } else {
+ $wgAuth->updateUser( $u );
+ $wgUser = $u;
+
+ if ( $isAutoCreated ) {
+ // Must be run after $wgUser is set, for correct new user log
+ wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
+ }
+
+ $retval = self::SUCCESS;
+ }
+ wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) );
+ return $retval;
+ }
+
+ /**
+ * Attempt to automatically create a user on login.
+ * Only succeeds if there is an external authentication method which allows it.
+ * @return integer Status code
+ */
+ function attemptAutoCreate( $user ) {
+ global $wgAuth, $wgUser;
+ /**
+ * If the external authentication plugin allows it,
+ * automatically create 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;
+ }
+
+ wfDebug( __METHOD__.": creating account\n" );
+ $user = $this->initUser( $user, true );
+ return self::SUCCESS;
+ }
+
+ function processLogin() {
+ global $wgUser, $wgAuth;
+
+ switch ($this->authenticateUserData())
+ {
+ case self::SUCCESS:
+ # We've verified now, update the real record
+ if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) {
+ $wgUser->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
+ $wgUser->saveSettings();
+ } else {
+ $wgUser->invalidateCache();
+ }
+ $wgUser->setCookies();
+
+ if( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
+ /* Replace the language object to provide user interface in correct
+ * language immediately on this first page load.
+ */
+ global $wgLang, $wgRequest;
+ $code = $wgRequest->getVal( 'uselang', $wgUser->getOption( 'language' ) );
+ $wgLang = Language::factory( $code );
+ return $this->successfulLogin( 'loginsuccess', $wgUser->getName() );
+ } else {
+ return $this->cookieRedirectCheck( 'login' );
+ }
+ break;
+
+ case self::NO_NAME:
+ case self::ILLEGAL:
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ break;
+ case self::WRONG_PLUGIN_PASS:
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ break;
+ case self::NOT_EXISTS:
+ if( $wgUser->isAllowed( 'createaccount' ) ){
+ $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) );
+ } else {
+ $this->mainLoginForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->mName ) ) );
+ }
+ break;
+ case self::WRONG_PASS:
+ $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+ break;
+ case self::EMPTY_PASS:
+ $this->mainLoginForm( wfMsg( 'wrongpasswordempty' ) );
+ break;
+ case self::RESET_PASS:
+ $this->resetLoginForm( wfMsg( 'resetpass_announce' ) );
+ break;
+ case self::CREATE_BLOCKED:
+ $this->userBlockedMessage();
+ break;
+ default:
+ throw new MWException( "Unhandled case value" );
+ }
+ }
+
+ function resetLoginForm( $error ) {
+ global $wgOut;
+ $wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" );
+ $reset = new PasswordResetForm( $this->mName, $this->mPassword );
+ $reset->execute( null );
+ }
+
+ /**
+ * @private
+ */
+ function mailPassword() {
+ global $wgUser, $wgOut, $wgAuth;
+
+ if( !$wgAuth->allowPasswordChange() ) {
+ $this->mainLoginForm( wfMsg( 'resetpass_forbidden' ) );
+ return;
+ }
+
+ # Check against blocked IPs
+ # fixme -- should we not?
+ if( $wgUser->isBlocked() ) {
+ $this->mainLoginForm( wfMsg( 'blocked-mailpassword' ) );
+ return;
+ }
+
+ # Check against the rate limiter
+ if( $wgUser->pingLimiter( 'mailpassword' ) ) {
+ $wgOut->rateLimited();
+ return;
+ }
+
+ if ( '' == $this->mName ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return;
+ }
+ $u = User::newFromName( $this->mName );
+ if( is_null( $u ) ) {
+ $this->mainLoginForm( wfMsg( 'noname' ) );
+ return;
+ }
+ if ( 0 == $u->getID() ) {
+ $this->mainLoginForm( wfMsg( 'nosuchuser', $u->getName() ) );
+ return;
+ }
+
+ # Check against password throttle
+ if ( $u->isPasswordReminderThrottled() ) {
+ global $wgPasswordReminderResendTime;
+ # Round the time in hours to 3 d.p., in case someone is specifying minutes or seconds.
+ $this->mainLoginForm( wfMsgExt( 'throttled-mailpassword', array( 'parsemag' ),
+ round( $wgPasswordReminderResendTime, 3 ) ) );
+ return;
+ }
+
+ $result = $this->mailPasswordInternal( $u, true, 'passwordremindertitle', 'passwordremindertext' );
+ if( WikiError::isError( $result ) ) {
+ $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
+ } else {
+ $this->mainLoginForm( wfMsg( 'passwordsent', $u->getName() ), 'success' );
+ }
+ }
+
+
+ /**
+ * @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
+ * @private
+ */
+ function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) {
+ global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure;
+ global $wgServer, $wgScript;
+
+ if ( '' == $u->getEmail() ) {
+ return new WikiError( wfMsg( 'noemail', $u->getName() ) );
+ }
+
+ $np = $u->randomPassword();
+ $u->setNewpassword( $np, $throttle );
+ $u->saveSettings();
+
+ $ip = wfGetIP();
+ if ( '' == $ip ) { $ip = '(Unknown)'; }
+
+ $m = wfMsg( $emailText, $ip, $u->getName(), $np, $wgServer . $wgScript );
+ $result = $u->sendMail( wfMsg( $emailTitle ), $m );
+
+ return $result;
+ }
+
+
+ /**
+ * @param string $msg Message key that will be shown on success
+ * @param $params String: parameters for the above message
+ * @param bool $auto Toggle auto-redirect to main page; default true
+ * @private
+ */
+ function successfulLogin( $msg, $params, $auto = true ) {
+ global $wgUser;
+ global $wgOut;
+
+ # Run any hooks; ignore results
+
+ $injected_html = '';
+ wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html));
+
+ $wgOut->setPageTitle( wfMsg( 'loginsuccesstitle' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+ $wgOut->addWikiMsgArray( $msg, $params );
+ $wgOut->addHtml( $injected_html );
+ if ( !empty( $this->mReturnTo ) ) {
+ $wgOut->returnToMain( $auto, $this->mReturnTo );
+ } else {
+ $wgOut->returnToMain( $auto );
+ }
+ }
+
+ /** */
+ function userNotPrivilegedMessage($errors) {
+ global $wgOut;
+
+ $wgOut->setPageTitle( wfMsg( 'permissionserrors' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+
+ $wgOut->addWikitext( $wgOut->formatPermissionsErrorMessage( $errors, 'createaccount' ) );
+ // Stuff that might want to be added at the end. For example, instructions if blocked.
+ $wgOut->addWikiMsg( 'cantcreateaccount-nonblock-text' );
+
+ $wgOut->returnToMain( false );
+ }
+
+ /** */
+ function userBlockedMessage() {
+ global $wgOut, $wgUser;
+
+ # Let's be nice about this, it's likely that this feature will be used
+ # for blocking large numbers of innocent people, e.g. range blocks on
+ # schools. Don't blame it on the user. There's a small chance that it
+ # really is the user's fault, i.e. the username is blocked and they
+ # haven't bothered to log out before trying to create an account to
+ # evade it, but we'll leave that to their guilty conscience to figure
+ # out.
+
+ $wgOut->setPageTitle( wfMsg( 'cantcreateaccounttitle' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+
+ $ip = wfGetIP();
+ $blocker = User::whoIs( $wgUser->mBlock->mBy );
+ $block_reason = $wgUser->mBlock->mReason;
+
+ if ( strval( $block_reason ) === '' ) {
+ $block_reason = wfMsg( 'blockednoreason' );
+ }
+ $wgOut->addWikiMsg( 'cantcreateaccount-text', $ip, $block_reason, $blocker );
+ $wgOut->returnToMain( false );
+ }
+
+ /**
+ * @private
+ */
+ function mainLoginForm( $msg, $msgtype = 'error' ) {
+ global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail;
+ global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector;
+ global $wgAuth, $wgEmailConfirmToEdit;
+
+ $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+
+ if ( $this->mType == 'signup' ) {
+ // Block signup here if in readonly. Keeps user from
+ // going through the process (filling out data, etc)
+ // and being informed later.
+ if ( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
+ return;
+ } elseif ( $wgUser->isBlockedFromCreateAccount() ) {
+ $this->userBlockedMessage();
+ return;
+ } elseif ( count( $permErrors = $titleObj->getUserPermissionsErrors( 'createaccount', $wgUser, true ) )>0 ) {
+ $wgOut->showPermissionsErrorPage( $permErrors, 'createaccount' );
+ return;
+ }
+ }
+
+ if ( '' == $this->mName ) {
+ if ( $wgUser->isLoggedIn() ) {
+ $this->mName = $wgUser->getName();
+ } else {
+ $this->mName = isset( $_COOKIE[$wgCookiePrefix.'UserName'] ) ? $_COOKIE[$wgCookiePrefix.'UserName'] : null;
+ }
+ }
+
+ $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+
+ if ( $this->mType == 'signup' ) {
+ $template = new UsercreateTemplate();
+ $q = 'action=submitlogin&type=signup';
+ $linkq = 'type=login';
+ $linkmsg = 'gotaccount';
+ } else {
+ $template = new UserloginTemplate();
+ $q = 'action=submitlogin&type=login';
+ $linkq = 'type=signup';
+ $linkmsg = 'nologin';
+ }
+
+ if ( !empty( $this->mReturnTo ) ) {
+ $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
+ $q .= $returnto;
+ $linkq .= $returnto;
+ }
+
+ # Pass any language selection on to the mode switch link
+ if( $wgLoginLanguageSelector && $this->mLanguage )
+ $linkq .= '&uselang=' . $this->mLanguage;
+
+ $link = '<a href="' . htmlspecialchars ( $titleObj->getLocalUrl( $linkq ) ) . '">';
+ $link .= wfMsgHtml( $linkmsg . 'link' ); # Calling either 'gotaccountlink' or 'nologinlink'
+ $link .= '</a>';
+
+ # Don't show a "create account" link if the user can't
+ if( $this->showCreateOrLoginLink( $wgUser ) )
+ $template->set( 'link', wfMsgHtml( $linkmsg, $link ) );
+ else
+ $template->set( 'link', '' );
+
+ $template->set( 'header', '' );
+ $template->set( 'name', $this->mName );
+ $template->set( 'password', $this->mPassword );
+ $template->set( 'retype', $this->mRetype );
+ $template->set( 'email', $this->mEmail );
+ $template->set( 'realname', $this->mRealName );
+ $template->set( 'domain', $this->mDomain );
+
+ $template->set( 'action', $titleObj->getLocalUrl( $q ) );
+ $template->set( 'message', $msg );
+ $template->set( 'messagetype', $msgtype );
+ $template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() );
+ $template->set( 'userealname', $wgAllowRealName );
+ $template->set( 'useemail', $wgEnableEmail );
+ $template->set( 'emailrequired', $wgEmailConfirmToEdit );
+ $template->set( 'canreset', $wgAuth->allowPasswordChange() );
+ $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember );
+
+ # Prepare language selection links as needed
+ if( $wgLoginLanguageSelector ) {
+ $template->set( 'languages', $this->makeLanguageSelector() );
+ if( $this->mLanguage )
+ $template->set( 'uselang', $this->mLanguage );
+ }
+
+ // Give authentication and captcha plugins a chance to modify the form
+ $wgAuth->modifyUITemplate( $template );
+ if ( $this->mType == 'signup' ) {
+ wfRunHooks( 'UserCreateForm', array( &$template ) );
+ } else {
+ wfRunHooks( 'UserLoginForm', array( &$template ) );
+ }
+
+ $wgOut->setPageTitle( wfMsg( 'userlogin' ) );
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ $wgOut->setArticleRelated( false );
+ $wgOut->disallowUserJs(); // just in case...
+ $wgOut->addTemplate( $template );
+ }
+
+ /**
+ * @private
+ */
+ function showCreateOrLoginLink( &$user ) {
+ if( $this->mType == 'signup' ) {
+ return( true );
+ } elseif( $user->isAllowed( 'createaccount' ) ) {
+ return( true );
+ } else {
+ return( false );
+ }
+ }
+
+ /**
+ * Check if a session cookie is present.
+ *
+ * This will not pick up a cookie set during _this_ request, but is
+ * meant to ensure that the client is returning the cookie which was
+ * set on a previous pass through the system.
+ *
+ * @private
+ */
+ function hasSessionCookie() {
+ global $wgDisableCookieCheck, $wgRequest;
+ return $wgDisableCookieCheck ? true : $wgRequest->checkSessionCookie();
+ }
+
+ /**
+ * @private
+ */
+ function cookieRedirectCheck( $type ) {
+ global $wgOut;
+
+ $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+ $check = $titleObj->getFullURL( 'wpCookieCheck='.$type );
+
+ return $wgOut->redirect( $check );
+ }
+
+ /**
+ * @private
+ */
+ function onCookieRedirectCheck( $type ) {
+ global $wgUser;
+
+ if ( !$this->hasSessionCookie() ) {
+ if ( $type == 'new' ) {
+ return $this->mainLoginForm( wfMsgExt( 'nocookiesnew', array( 'parseinline' ) ) );
+ } else if ( $type == 'login' ) {
+ return $this->mainLoginForm( wfMsgExt( 'nocookieslogin', array( 'parseinline' ) ) );
+ } else {
+ # shouldn't happen
+ return $this->mainLoginForm( wfMsg( 'error' ) );
+ }
+ } else {
+ return $this->successfulLogin( 'loginsuccess', $wgUser->getName() );
+ }
+ }
+
+ /**
+ * @private
+ */
+ function throttleHit( $limit ) {
+ global $wgOut;
+
+ $wgOut->addWikiMsg( 'acct_creation_throttle_hit', $limit );
+ }
+
+ /**
+ * Produce a bar of links which allow the user to select another language
+ * during login/registration but retain "returnto"
+ *
+ * @return string
+ */
+ function makeLanguageSelector() {
+ $msg = wfMsgForContent( 'loginlanguagelinks' );
+ if( $msg != '' && !wfEmptyMsg( 'loginlanguagelinks', $msg ) ) {
+ $langs = explode( "\n", $msg );
+ $links = array();
+ foreach( $langs as $lang ) {
+ $lang = trim( $lang, '* ' );
+ $parts = explode( '|', $lang );
+ if (count($parts) >= 2) {
+ $links[] = $this->makeLanguageSelectorLink( $parts[0], $parts[1] );
+ }
+ }
+ return count( $links ) > 0 ? wfMsgHtml( 'loginlanguagelabel', implode( ' | ', $links ) ) : '';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Create a language selector link for a particular language
+ * Links back to this page preserving type and returnto
+ *
+ * @param $text Link text
+ * @param $lang Language code
+ */
+ function makeLanguageSelectorLink( $text, $lang ) {
+ global $wgUser;
+ $self = SpecialPage::getTitleFor( 'Userlogin' );
+ $attr[] = 'uselang=' . $lang;
+ if( $this->mType == 'signup' )
+ $attr[] = 'type=signup';
+ if( $this->mReturnTo )
+ $attr[] = 'returnto=' . $this->mReturnTo;
+ $skin = $wgUser->getSkin();
+ return $skin->makeKnownLinkObj( $self, htmlspecialchars( $text ), implode( '&', $attr ) );
+ }
+}
diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php
new file mode 100644
index 00000000..137eadb4
--- /dev/null
+++ b/includes/specials/SpecialUserlogout.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialUserlogout() {
+ global $wgUser, $wgOut;
+
+ $oldName = $wgUser->getName();
+ $wgUser->logout();
+ $wgOut->setRobotpolicy( 'noindex,nofollow' );
+
+ // Hook.
+ $injected_html = '';
+ wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html, $oldName) );
+
+ $wgOut->addHTML( wfMsgExt( 'logouttext', array( 'parse' ) ) . $injected_html );
+ $wgOut->returnToMain();
+}
diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php
new file mode 100644
index 00000000..fd3c690b
--- /dev/null
+++ b/includes/specials/SpecialUserrights.php
@@ -0,0 +1,589 @@
+<?php
+/**
+ * Special page to allow managing user group membership
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A class to manage user levels rights.
+ * @ingroup SpecialPage
+ */
+class UserrightsPage extends SpecialPage {
+ # The target of the local right-adjuster's interest. Can be gotten from
+ # either a GET parameter or a subpage-style parameter, so have a member
+ # variable for it.
+ protected $mTarget;
+ protected $isself = false;
+
+ public function __construct() {
+ parent::__construct( 'Userrights' );
+ }
+
+ public function isRestricted() {
+ return true;
+ }
+
+ public function userCanExecute( $user ) {
+ $available = $this->changeableGroups();
+ return !empty( $available['add'] )
+ or !empty( $available['remove'] )
+ or ($this->isself and
+ (!empty( $available['add-self'] )
+ or !empty( $available['remove-self'] )));
+ }
+
+ /**
+ * Manage forms to be shown according to posted data.
+ * Depending on the submit button used, call a form or a save function.
+ *
+ * @param $par Mixed: string if any subpage provided, else null
+ */
+ 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;
+
+ if( $par ) {
+ $this->mTarget = $par;
+ } else {
+ $this->mTarget = $wgRequest->getVal( 'user' );
+ }
+
+ 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']))
+ $this->mTarget = $wgUser->getName();
+ }
+
+ if ($this->mTarget == $wgUser->getName())
+ $this->isself = true;
+
+ if( !$this->userCanExecute( $wgUser ) ) {
+ // fixme... there may be intermediate groups we can mention.
+ global $wgOut;
+ $wgOut->showPermissionsErrorPage( array(
+ $wgUser->isAnon()
+ ? 'userrights-nologin'
+ : 'userrights-notallowed' ) );
+ return;
+ }
+
+ if ( wfReadOnly() ) {
+ global $wgOut;
+ $wgOut->readOnlyPage();
+ return;
+ }
+
+ $this->outputHeader();
+
+ $this->setHeaders();
+
+ // show the general form
+ $this->switchForm();
+
+ if( $wgRequest->wasPosted() ) {
+ // save settings
+ if( $wgRequest->getCheck( 'saveusergroups' ) ) {
+ $reason = $wgRequest->getVal( 'user-reason' );
+ if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $this->mTarget ) ) {
+ $this->saveUserGroups(
+ $this->mTarget,
+ $reason
+ );
+ }
+ }
+ }
+
+ // show some more forms
+ if( $this->mTarget ) {
+ $this->editUserGroupsForm( $this->mTarget );
+ }
+ }
+
+ /**
+ * Save user groups changes in the database.
+ * Data comes from the editUserGroupsForm() form function
+ *
+ * @param $username String: username to apply changes to.
+ * @param $reason String: reason for group change
+ * @return null
+ */
+ function saveUserGroups( $username, $reason = '') {
+ global $wgRequest, $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+
+ $user = $this->fetchUser( $username );
+ if( !$user ) {
+ return;
+ }
+
+ $allgroups = $this->getAllGroups();
+ $addgroup = array();
+ $removegroup = array();
+
+ // 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) {
+ // 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" )) {
+ $addgroup[] = $group;
+ } else {
+ $removegroup[] = $group;
+ }
+ }
+
+ // Validate input set...
+ $changeable = $this->changeableGroups();
+ if ($wgUser->getId() != 0 && $wgUser->getId() == $user->getId()) {
+ $addable = array_merge($changeable['add'], $wgGroupsAddToSelf);
+ $removable = array_merge($changeable['remove'], $wgGroupsRemoveFromSelf);
+ } else {
+ $addable = $changeable['add'];
+ $removable = $changeable['remove'];
+ }
+
+ $removegroup = array_unique(
+ array_intersect( (array)$removegroup, $removable ) );
+ $addgroup = array_unique(
+ array_intersect( (array)$addgroup, $addable ) );
+
+ $oldGroups = $user->getGroups();
+ $newGroups = $oldGroups;
+ // remove then add groups
+ if( $removegroup ) {
+ $newGroups = array_diff($newGroups, $removegroup);
+ foreach( $removegroup as $group ) {
+ $user->removeGroup( $group );
+ }
+ }
+ if( $addgroup ) {
+ $newGroups = array_merge($newGroups, $addgroup);
+ foreach( $addgroup as $group ) {
+ $user->addGroup( $group );
+ }
+ }
+ $newGroups = array_unique( $newGroups );
+
+ // Ensure that caches are cleared
+ $user->invalidateCache();
+
+ wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
+ wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
+ if( $user instanceof User ) {
+ // hmmm
+ wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) );
+ }
+
+ if( $newGroups != $oldGroups ) {
+ $this->addLogEntry( $user, $oldGroups, $newGroups );
+ }
+ }
+
+ /**
+ * Add a rights log entry for an action.
+ */
+ function addLogEntry( $user, $oldGroups, $newGroups ) {
+ global $wgRequest;
+ $log = new LogPage( 'rights' );
+
+ $log->addEntry( 'rights',
+ $user->getUserPage(),
+ $wgRequest->getText( 'user-reason' ),
+ array(
+ $this->makeGroupNameListForLog( $oldGroups ),
+ $this->makeGroupNameListForLog( $newGroups )
+ )
+ );
+ }
+
+ /**
+ * Edit user groups membership
+ * @param $username String: name of the user.
+ */
+ function editUserGroupsForm( $username ) {
+ global $wgOut;
+
+ $user = $this->fetchUser( $username );
+ if( !$user ) {
+ return;
+ }
+
+ $groups = $user->getGroups();
+
+ $this->showEditUserGroupsForm( $user, $groups );
+
+ // This isn't really ideal logging behavior, but let's not hide the
+ // interwiki logs if we're using them as is.
+ $this->showLogFragment( $user, $wgOut );
+ }
+
+ /**
+ * Normalize the input username, which may be local or remote, and
+ * return a user (or proxy) object for manipulating it.
+ *
+ * Side effects: error output for invalid access
+ * @return mixed User, UserRightsProxy, or null
+ */
+ function fetchUser( $username ) {
+ global $wgOut, $wgUser;
+
+ $parts = explode( '@', $username );
+ if( count( $parts ) < 2 ) {
+ $name = trim( $username );
+ $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( $name == '' ) {
+ $wgOut->addWikiMsg( 'nouserspecified' );
+ return false;
+ }
+
+ if( $name{0} == '#' ) {
+ // Numeric ID can be specified...
+ // We'll do a lookup for the name internally.
+ $id = intval( substr( $name, 1 ) );
+
+ if( $database == '' ) {
+ $name = User::whoIs( $id );
+ } else {
+ $name = UserRightsProxy::whoIs( $database, $id );
+ }
+
+ if( !$name ) {
+ $wgOut->addWikiMsg( 'noname' );
+ return null;
+ }
+ }
+
+ if( $database == '' ) {
+ $user = User::newFromName( $name );
+ } else {
+ $user = UserRightsProxy::newFromName( $database, $name );
+ }
+
+ if( !$user || $user->isAnon() ) {
+ $wgOut->addWikiMsg( 'nosuchusershort', $username );
+ return null;
+ }
+
+ return $user;
+ }
+
+ function makeGroupNameList( $ids ) {
+ if( empty( $ids ) ) {
+ return wfMsg( 'rightsnone' );
+ } else {
+ return implode( ', ', $ids );
+ }
+ }
+
+ function makeGroupNameListForLog( $ids ) {
+ if( empty( $ids ) ) {
+ return '';
+ } else {
+ return $this->makeGroupNameList( $ids );
+ }
+ }
+
+ /**
+ * Output a form to allow searching for a user
+ */
+ function switchForm() {
+ global $wgOut, $wgScript;
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser', 'id' => 'mw-userrights-form1' ) ) .
+ Xml::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', array(), wfMsg( 'userrights-lookup-user' ) ) .
+ Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user', 'username', 30, $this->mTarget ) . ' ' .
+ Xml::submitButton( wfMsg( 'editusergroup' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+ }
+
+ /**
+ * Go through used and available groups and return the ones that this
+ * form will be able to manipulate based on the current user's system
+ * permissions.
+ *
+ * @param $groups Array: list of groups the given user is in
+ * @return Array: Tuple of addable, then removable groups
+ */
+ protected function splitGroups( $groups ) {
+ global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+ list($addable, $removable) = array_values( $this->changeableGroups() );
+
+ $removable = array_intersect(
+ array_merge($this->isself ? $wgGroupsRemoveFromSelf : array(), $removable),
+ $groups ); // Can't remove groups the user doesn't have
+ $addable = array_diff(
+ array_merge($this->isself ? $wgGroupsAddToSelf : array(), $addable),
+ $groups ); // Can't add groups the user does have
+
+ return array( $addable, $removable );
+ }
+
+ /**
+ * Show the form to edit group memberships.
+ *
+ * @param $user User or UserRightsProxy you're editing
+ * @param $groups Array: Array of groups the user is in
+ */
+ protected function showEditUserGroupsForm( $user, $groups ) {
+ global $wgOut, $wgUser, $wgLang;
+
+ list( $addable, $removable ) = $this->splitGroups( $groups );
+
+ $list = array();
+ foreach( $user->getGroups() as $group )
+ $list[] = self::buildGroupLink( $group );
+
+ $grouplist = '';
+ if( count( $list ) > 0 ) {
+ $grouplist = wfMsgHtml( 'userrights-groupsmember' );
+ $grouplist = '<p>' . $grouplist . ' ' . $wgLang->listToText( $list ) . '</p>';
+ }
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL(), 'name' => 'editGroup', 'id' => 'mw-userrights-form2' ) ) .
+ Xml::hidden( 'user', $this->mTarget ) .
+ Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->mTarget ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
+ wfMsgExt( 'editinguser', array( 'parse' ), wfEscapeWikiText( $user->getName() ) ) .
+ wfMsgExt( 'userrights-groups-help', array( 'parse' ) ) .
+ $grouplist .
+ Xml::tags( 'p', null, $this->groupCheckboxes( $groups ) ) .
+ Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-userrights-table-outer' ) ) .
+ "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' ) . "\n" .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' ) . "\n"
+ );
+ }
+
+ /**
+ * Format a link to a group description page
+ *
+ * @param $group string
+ * @return string
+ */
+ private static function buildGroupLink( $group ) {
+ static $cache = array();
+ if( !isset( $cache[$group] ) )
+ $cache[$group] = User::makeGroupLinkHtml( $group, 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();
+ }
+
+ /**
+ * Adds a table with checkboxes where you can select what groups to add/remove
+ *
+ * @param $usergroups Array: groups the user belongs to
+ * @return string XHTML table element with checkboxes
+ */
+ private function groupCheckboxes( $usergroups ) {
+ $allgroups = $this->getAllGroups();
+ $ret = '';
+
+ $column = 1;
+ $settable_col = '';
+ $unsettable_col = '';
+
+ foreach ($allgroups as $group) {
+ $set = in_array( $group, $usergroups );
+ # Should the checkbox be disabled?
+ $disabled = !(
+ ( $set && $this->canRemove( $group ) ) ||
+ ( !$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";
+ } else {
+ $settable_col .= "$checkbox<br />\n";
+ }
+ }
+
+ 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>
+";
+ }
+ $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @param $group String: the name of the group to check
+ * @return bool Can we remove the group?
+ */
+ private function canRemove( $group ) {
+ // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
+ // PHP.
+ $groups = $this->changeableGroups();
+ return in_array( $group, $groups['remove'] ) || ($this->isself && in_array( $group, $groups['remove-self'] ));
+ }
+
+ /**
+ * @param $group string: the name of the group to check
+ * @return bool Can we add the group?
+ */
+ private function canAdd( $group ) {
+ $groups = $this->changeableGroups();
+ return in_array( $group, $groups['add'] ) || ($this->isself && in_array( $group, $groups['add-self'] ));
+ }
+
+ /**
+ * Returns an array of the groups that the user can add/remove.
+ *
+ * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
+ */
+ function changeableGroups() {
+ global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+
+ 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' => $wgGroupsAddToSelf,
+ 'remove-self' => $wgGroupsRemoveFromSelf);
+ $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'] );
+ }
+ 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 ) )
+ */
+ private function changeableByGroup( $group ) {
+ global $wgAddGroups, $wgRemoveGroups;
+
+ $groups = array( 'add' => array(), 'remove' => 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];
+ }
+ return $groups;
+ }
+
+ /**
+ * Show a rights log fragment for the specified user
+ *
+ * @param $user User to show log for
+ * @param $output OutputPage to use
+ */
+ protected function showLogFragment( $user, $output ) {
+ $output->addHtml( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) );
+ LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage()->getPrefixedText() );
+ }
+}
diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php
new file mode 100644
index 00000000..8c8e386d
--- /dev/null
+++ b/includes/specials/SpecialVersion.php
@@ -0,0 +1,391 @@
+<?php
+/**#@+
+ * Give information about the version of MediaWiki, PHP, the DB and extensions
+ *
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialVersion() {
+ $version = new SpecialVersion;
+ $version->execute();
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+class SpecialVersion {
+ private $firstExtOpened = true;
+
+ /**
+ * main()
+ */
+ function execute() {
+ global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks;
+ $wgMessageCache->loadAllMessages();
+
+ $wgOut->addHTML( '<div dir="ltr">' );
+ $text =
+ $this->MediaWikiCredits() .
+ $this->softwareInformation() .
+ $this->extensionCredits();
+ if ( $wgSpecialVersionShowHooks ) {
+ $text .= $this->wgHooks();
+ }
+ $wgOut->addWikiText( $text );
+ $wgOut->addHTML( $this->IPInfo() );
+ $wgOut->addHTML( '</div>' );
+ }
+
+ /**#@+
+ * @private
+ */
+
+ /**
+ * @return wiki text showing the license information
+ */
+ static function MediaWikiCredits() {
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ) .
+ "__NOTOC__
+ This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
+ copyright (C) 2001-2008 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.
+
+ MediaWiki 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.
+
+ MediaWiki 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 [{{SERVER}}{{SCRIPTPATH}}/COPYING 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
+ or [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].
+ ";
+
+ return str_replace( "\t\t", '', $ret ) . "\n";
+ }
+
+ /**
+ * @return wiki text showing the third party software versions (apache, php, mysql).
+ */
+ 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' ) ) .
+ "<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' );
+ }
+
+ /**
+ * Return a string of the MediaWiki version with SVN revision if available
+ *
+ * @return mixed
+ */
+ public static function getVersion() {
+ global $wgVersion, $IP;
+ wfProfileIn( __METHOD__ );
+ $svn = self::getSvnRevision( $IP );
+ $version = $svn ? "$wgVersion (r$svn)" : $wgVersion;
+ wfProfileOut( __METHOD__ );
+ return $version;
+ }
+
+ /**
+ * Return a string of the MediaWiki version with a link to 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;
+ wfProfileOut( __METHOD__ );
+ return $version;
+ }
+
+ /** Generate wikitext showing extensions name, URL, author and description */
+ function extensionCredits() {
+ global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunctions;
+
+ if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunctions ) )
+ return '';
+
+ $extensionTypes = array(
+ 'specialpage' => wfMsg( 'version-specialpages' ),
+ 'parserhook' => wfMsg( 'version-parserhooks' ),
+ 'variable' => wfMsg( 'version-variables' ),
+ 'media' => wfMsg( 'version-mediahandlers' ),
+ 'other' => wfMsg( 'version-other' ),
+ );
+ wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
+
+ $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'sv-ext' ) );
+
+ foreach ( $extensionTypes as $type => $text ) {
+ if ( isset ( $wgExtensionCredits[$type] ) && count ( $wgExtensionCredits[$type] ) ) {
+ $out .= $this->openExtType( $text );
+
+ usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
+
+ foreach ( $wgExtensionCredits[$type] as $extension ) {
+ if ( isset( $extension['version'] ) ) {
+ $version = $extension['version'];
+ } elseif ( isset( $extension['svn-revision'] ) &&
+ preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/',
+ $extension['svn-revision'], $m ) )
+ {
+ $version = 'r' . $m[1];
+ } else {
+ $version = null;
+ }
+
+ $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'] : ''
+ );
+ }
+ }
+ }
+
+ if ( count( $wgExtensionFunctions ) ) {
+ $out .= $this->openExtType( wfMsg( 'version-extension-functions' ) );
+ $out .= '<tr><td colspan="3">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
+ }
+
+ if ( $cnt = count( $tags = $wgParser->getTags() ) ) {
+ for ( $i = 0; $i < $cnt; ++$i )
+ $tags[$i] = "&lt;{$tags[$i]}&gt;";
+ $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ) );
+ $out .= '<tr><td colspan="3">' . $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";
+ }
+
+ if ( count( $wgSkinExtensionFunctions ) ) {
+ $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ) );
+ $out .= '<tr><td colspan="3">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n";
+ }
+ $out .= Xml::closeElement( 'table' );
+ return $out;
+ }
+
+ /** Callback to sort extensions by type */
+ function compare( $a, $b ) {
+ global $wgLang;
+ if( $a['name'] === $b['name'] ) {
+ return 0;
+ } else {
+ return $wgLang->lc( $a['name'] ) > $wgLang->lc( $b['name'] )
+ ? 1
+ : -1;
+ }
+ }
+
+ 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)" : '';
+
+ # Look for a localized description
+ if( isset( $descriptionMsg ) ) {
+ $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>
+ </tr>\n";
+ }
+
+ /**
+ * @return string
+ */
+ function wgHooks() {
+ global $wgHooks;
+
+ if ( count( $wgHooks ) ) {
+ $myWgHooks = $wgHooks;
+ ksort( $myWgHooks );
+
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
+ Xml::openElement( 'table', array( 'id' => 'sv-hooks' ) ) .
+ "<tr>
+ <th>" . wfMsg( 'version-hook-name' ) . "</th>
+ <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
+ </tr>\n";
+
+ foreach ( $myWgHooks as $hook => $hooks )
+ $ret .= "<tr>
+ <td>$hook</td>
+ <td>" . $this->listToText( $hooks ) . "</td>
+ </tr>\n";
+
+ $ret .= Xml::closeElement( 'table' );
+ return $ret;
+ } else
+ return '';
+ }
+
+ private function openExtType($text, $name = null) {
+ $opt = array( 'colspan' => 3 );
+ $out = '';
+
+ 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"; }
+
+ $out .= "<tr>" . Xml::element( 'th', $opt, $text) . "</tr>\n";
+ return $out;
+ }
+
+ /**
+ * @static
+ *
+ * @return string
+ */
+ function IPInfo() {
+ $ip = str_replace( '--', ' - ', htmlspecialchars( wfGetIP() ) );
+ return "<!-- visited from $ip -->\n" .
+ "<span style='display:none'>visited from $ip</span>";
+ }
+
+ /**
+ * @param array $list
+ * @return string
+ */
+ function listToText( $list ) {
+ $cnt = count( $list );
+
+ if ( $cnt == 1 ) {
+ // Enforce always returning a string
+ return (string)$this->arrayToString( $list[0] );
+ } elseif ( $cnt == 0 ) {
+ return '';
+ } else {
+ sort( $list );
+ $t = array_slice( $list, 0, $cnt - 1 );
+ $one = array_map( array( &$this, 'arrayToString' ), $t );
+ $two = $this->arrayToString( $list[$cnt - 1] );
+ $and = wfMsg( 'and' );
+
+ return implode( ', ', $one ) . " $and $two";
+ }
+ }
+
+ /**
+ * @static
+ *
+ * @param mixed $list Will convert an array to string if given and return
+ * the paramater unaltered otherwise
+ * @return mixed
+ */
+ function arrayToString( $list ) {
+ if( is_object( $list ) ) {
+ $class = get_class( $list );
+ return "($class)";
+ } elseif ( ! is_array( $list ) ) {
+ return $list;
+ } else {
+ $class = get_class( $list[0] );
+ return "($class, {$list[1]})";
+ }
+ }
+
+ /**
+ * Retrieve the revision number of a Subversion working directory.
+ *
+ * @param string $dir
+ * @return mixed revision number as int, or false if not a SVN checkout
+ */
+ public static function getSvnRevision( $dir ) {
+ // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
+ $entries = $dir . '/.svn/entries';
+
+ if( !file_exists( $entries ) ) {
+ return false;
+ }
+
+ $content = file( $entries );
+
+ // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
+ if( preg_match( '/^<\?xml/', $content[0] ) ) {
+ // subversion is release <= 1.3
+ if( !function_exists( 'simplexml_load_file' ) ) {
+ // We could fall back to expat... YUCK
+ return false;
+ }
+
+ // SimpleXml whines about the xmlns...
+ wfSuppressWarnings();
+ $xml = simplexml_load_file( $entries );
+ wfRestoreWarnings();
+
+ if( $xml ) {
+ foreach( $xml->entry as $entry ) {
+ if( $xml->entry[0]['name'] == '' ) {
+ // The directory entry should always have a revision marker.
+ if( $entry['revision'] ) {
+ return intval( $entry['revision'] );
+ }
+ }
+ }
+ }
+ return false;
+ } else {
+ // subversion is release 1.4
+ return intval( $content[3] );
+ }
+ }
+
+ /**#@-*/
+}
+
+/**#@-*/
diff --git a/includes/specials/SpecialWantedcategories.php b/includes/specials/SpecialWantedcategories.php
new file mode 100644
index 00000000..7497f9be
--- /dev/null
+++ b/includes/specials/SpecialWantedcategories.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A querypage to list the most wanted categories - implements Special:Wantedcategories
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @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 {
+
+ 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' );
+ $name = $dbr->addQuotes( $this->getName() );
+ return
+ "
+ SELECT
+ $name as type,
+ " . NS_CATEGORY . " as namespace,
+ cl_to as title,
+ COUNT(*) as value
+ FROM $categorylinks
+ LEFT JOIN $page ON cl_to = page_title AND page_namespace = ". NS_CATEGORY ."
+ WHERE page_title IS NULL
+ GROUP BY cl_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 ) );
+
+ $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value ) );
+ return wfSpecialList($plink, $nlinks);
+ }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialWantedCategories() {
+ list( $limit, $offset ) = wfCheckLimits();
+
+ $wpp = new WantedCategoriesPage();
+
+ $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/SpecialWantedpages.php b/includes/specials/SpecialWantedpages.php
new file mode 100644
index 00000000..10133409
--- /dev/null
+++ b/includes/specials/SpecialWantedpages.php
@@ -0,0 +1,131 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Wantedpages
+ * @ingroup SpecialPage
+ */
+class WantedPagesPage extends QueryPage {
+ var $nlinks;
+
+ function WantedPagesPage( $inc = false, $nlinks = true ) {
+ $this->setListoutput( $inc );
+ $this->nlinks = $nlinks;
+ }
+
+ function getName() {
+ return 'Wantedpages';
+ }
+
+ function isExpensive() {
+ return true;
+ }
+ function isSyndicated() { return false; }
+
+ function getSQL() {
+ global $wgWantedPagesThreshold;
+ $count = $wgWantedPagesThreshold - 1;
+ $dbr = wfGetDB( DB_SLAVE );
+ $pagelinks = $dbr->tableName( 'pagelinks' );
+ $page = $dbr->tableName( 'page' );
+ return
+ "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";
+ }
+
+ /**
+ * 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 "Invalid title in result set; {$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;
+ }
+ }
+
+}
+
+/**
+ * constructor
+ */
+function wfSpecialWantedpages( $par = null, $specialPage ) {
+ $inc = $specialPage->including();
+
+ if ( $inc ) {
+ @list( $limit, $nlinks ) = explode( '/', $par, 2 );
+ $limit = (int)$limit;
+ $nlinks = $nlinks === 'nlinks';
+ $offset = 0;
+ } else {
+ list( $limit, $offset ) = wfCheckLimits();
+ $nlinks = true;
+ }
+
+ $wpp = new WantedPagesPage( $inc, $nlinks );
+
+ $wpp->doQuery( $offset, $limit, !$inc );
+}
diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php
new file mode 100644
index 00000000..db7cd423
--- /dev/null
+++ b/includes/specials/SpecialWatchlist.php
@@ -0,0 +1,383 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage Watchlist
+ */
+
+/**
+ * Constructor
+ *
+ * @param $par Parameter passed to the page
+ */
+function wfSpecialWatchlist( $par ) {
+ global $wgUser, $wgOut, $wgLang, $wgRequest;
+ global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
+ global $wgEnotifWatchlist;
+ $fname = 'wfSpecialWatchlist';
+
+ $skin = $wgUser->getSkin();
+ $specialTitle = SpecialPage::getTitleFor( 'Watchlist' );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+
+ # Anons don't get a watchlist
+ if( $wgUser->isAnon() ) {
+ $wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
+ $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() );
+ $wgOut->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
+ return;
+ }
+
+ $wgOut->setPageTitle( wfMsg( 'watchlist' ) );
+
+ $sub = wfMsgExt( 'watchlistfor', 'parseinline', $wgUser->getName() );
+ $sub .= '<br />' . WatchlistEditor::buildTools( $wgUser->getSkin() );
+ $wgOut->setSubtitle( $sub );
+
+ if( ( $mode = WatchlistEditor::getMode( $wgRequest, $par ) ) !== false ) {
+ $editor = new WatchlistEditor();
+ $editor->execute( $wgUser, $wgOut, $wgRequest, $mode );
+ return;
+ }
+
+ $uid = $wgUser->getId();
+ if( ($wgEnotifWatchlist || $wgShowUpdatedMarker) && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) {
+ $wgUser->clearAllNotifications( $uid );
+ $wgOut->redirect( $specialTitle->getFullUrl() );
+ return;
+ }
+
+ $defaults = array(
+ /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
+ /* bool */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ),
+ /* bool */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ),
+ /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ),
+ /* ? */ 'namespace' => 'all',
+ );
+
+ extract($defaults);
+
+ # Extract variables from the request, falling back to user preferences or
+ # other default values if these don't exist
+ $prefs['days' ] = floatval( $wgUser->getOption( 'watchlistdays' ) );
+ $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' );
+ $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' );
+ $prefs['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' );
+
+ # Get query variables
+ $days = $wgRequest->getVal( 'days', $prefs['days'] );
+ $hideOwn = $wgRequest->getBool( 'hideOwn', $prefs['hideown'] );
+ $hideBots = $wgRequest->getBool( 'hideBots', $prefs['hidebots'] );
+ $hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] );
+
+ # Get namespace value, if supplied, and prepare a WHERE fragment
+ $nameSpace = $wgRequest->getIntOrNull( 'namespace' );
+ if( !is_null( $nameSpace ) ) {
+ $nameSpace = intval( $nameSpace );
+ $nameSpaceClause = " AND rc_namespace = $nameSpace";
+ } else {
+ $nameSpace = '';
+ $nameSpaceClause = '';
+ }
+
+ $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
+ list( $page, $watchlist, $recentchanges ) = $dbr->tableNamesN( 'page', 'watchlist', 'recentchanges' );
+
+ $watchlistCount = $dbr->selectField( 'watchlist', 'COUNT(*)',
+ array( 'wl_user' => $uid ), __METHOD__ );
+ // Adjust for page X, talk:page X, which are both stored separately,
+ // but treated together
+ $nitems = floor($watchlistCount / 2);
+
+ if( is_null($days) || !is_numeric($days) ) {
+ $big = 1000; /* The magical big */
+ if($nitems > $big) {
+ # Set default cutoff shorter
+ $days = $defaults['days'] = (12.0 / 24.0); # 12 hours...
+ } else {
+ $days = $defaults['days']; # default cutoff for shortlisters
+ }
+ } else {
+ $days = floatval($days);
+ }
+
+ // Dump everything here
+ $nondefaults = array();
+
+ wfAppendToArrayIfNotDefault('days' , $days , $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault('hideOwn' , (int)$hideOwn , $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault('hideBots' , (int)$hideBots, $defaults, $nondefaults);
+ wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults );
+ wfAppendToArrayIfNotDefault('namespace', $nameSpace , $defaults, $nondefaults);
+
+ $hookSql = "";
+ if( ! wfRunHooks('BeforeWatchlist', array($nondefaults, $wgUser, &$hookSql)) ) {
+ return;
+ }
+
+ if($nitems == 0) {
+ $wgOut->addWikiMsg( 'nowatchlist' );
+ return;
+ }
+
+ if ( $days <= 0 ) {
+ $andcutoff = '';
+ } else {
+ $andcutoff = "AND rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'";
+ /*
+ $sql = "SELECT COUNT(*) AS n FROM $page, $revision WHERE rev_timestamp>'$cutoff' AND page_id=rev_page";
+ $res = $dbr->query( $sql, $fname );
+ $s = $dbr->fetchObject( $res );
+ $npages = $s->n;
+ */
+ }
+
+ # If the watchlist is relatively short, it's simplest to zip
+ # down its entirety and then sort the results.
+
+ # If it's relatively long, it may be worth our while to zip
+ # through the time-sorted page list checking for watched items.
+
+ # Up estimate of watched items by 15% to compensate for talk pages...
+
+ # Toggles
+ $andHideOwn = $hideOwn ? "AND (rc_user <> $uid)" : '';
+ $andHideBots = $hideBots ? "AND (rc_bot = 0)" : '';
+ $andHideMinor = $hideMinor ? 'AND rc_minor = 0' : '';
+
+ # Show watchlist header
+ $header = '';
+ if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
+ $header .= wfMsg( 'wlheader-enotif' ) . "\n";
+ }
+ if ( $wgShowUpdatedMarker ) {
+ $header .= wfMsg( 'wlheader-showupdated' ) . "\n";
+ }
+
+ # Toggle watchlist content (all recent edits or just the latest)
+ if( $wgUser->getOption( 'extendwatchlist' )) {
+ $andLatest='';
+ $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) );
+ } else {
+ # Top log Ids for a page are not stored
+ $andLatest = 'AND (rc_this_oldid=page_latest OR rc_type=' . RC_LOG . ') ';
+ $limitWatchlist = '';
+ }
+
+ $header .= wfMsgExt( 'watchlist-details', array( 'parsemag' ), $wgLang->formatNum( $nitems ) );
+ $wgOut->addWikiText( $header );
+
+ # Show a message about slave lag, if applicable
+ if( ( $lag = $dbr->getLag() ) > 0 )
+ $wgOut->showLagWarning( $lag );
+
+ if ( $wgShowUpdatedMarker ) {
+ $wgOut->addHTML( '<form action="' .
+ $specialTitle->escapeLocalUrl() .
+ '" method="post"><input type="submit" name="dummy" value="' .
+ htmlspecialchars( wfMsg( 'enotif_reset' ) ) .
+ '" /><input type="hidden" name="reset" value="all" /></form>' .
+ "\n\n" );
+ }
+ if ( $wgShowUpdatedMarker ) {
+ $wltsfield = ", ${watchlist}.wl_notificationtimestamp ";
+ } else {
+ $wltsfield = '';
+ }
+ $sql = "SELECT ${recentchanges}.* ${wltsfield}
+ FROM $watchlist,$recentchanges
+ LEFT JOIN $page ON rc_cur_id=page_id
+ WHERE wl_user=$uid
+ AND wl_namespace=rc_namespace
+ AND wl_title=rc_title
+ $andcutoff
+ $andLatest
+ $andHideOwn
+ $andHideBots
+ $andHideMinor
+ $nameSpaceClause
+ $hookSql
+ ORDER BY rc_timestamp DESC
+ $limitWatchlist";
+
+ $res = $dbr->query( $sql, $fname );
+ $numRows = $dbr->numRows( $res );
+
+ /* Start bottom header */
+ $wgOut->addHTML( "<hr />\n" );
+
+ if($days >= 1) {
+ $wgOut->addHTML(
+ wfMsgExt( 'rcnote', 'parseinline',
+ $wgLang->formatNum( $numRows ),
+ $wgLang->formatNum( $days ),
+ $wgLang->timeAndDate( wfTimestampNow(), true ),
+ $wgLang->date( wfTimestampNow(), true ),
+ $wgLang->time( wfTimestampNow(), true )
+ ) . '<br />'
+ );
+ } elseif($days > 0) {
+ $wgOut->addHtml(
+ wfMsgExt( 'wlnote', 'parseinline',
+ $wgLang->formatNum( $numRows ),
+ $wgLang->formatNum( round($days*24) )
+ ) . '<br />'
+ );
+ }
+
+ $wgOut->addHTML( "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n" );
+
+ # Spit out some control panel links
+ $thisTitle = SpecialPage::getTitleFor( 'Watchlist' );
+ $skin = $wgUser->getSkin();
+
+ # Hide/show bot edits
+ $label = $hideBots ? wfMsgHtml( 'watchlist-show-bots' ) : wfMsgHtml( 'watchlist-hide-bots' );
+ $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults );
+ $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+
+ # Hide/show own edits
+ $label = $hideOwn ? wfMsgHtml( 'watchlist-show-own' ) : wfMsgHtml( 'watchlist-hide-own' );
+ $linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults );
+ $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+
+ # Hide/show minor edits
+ $label = $hideMinor ? wfMsgHtml( 'watchlist-show-minor' ) : wfMsgHtml( 'watchlist-hide-minor' );
+ $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults );
+ $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+
+ $wgOut->addHTML( implode( ' | ', $links ) );
+
+ # Form for namespace filtering
+ $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl() ) );
+ $form .= '<p>';
+ $form .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;';
+ $form .= Xml::namespaceSelector( $nameSpace, '' ) . '&nbsp;';
+ $form .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</p>';
+ $form .= Xml::hidden( 'days', $days );
+ if( $hideOwn )
+ $form .= Xml::hidden( 'hideOwn', 1 );
+ if( $hideBots )
+ $form .= Xml::hidden( 'hideBots', 1 );
+ if( $hideMinor )
+ $form .= Xml::hidden( 'hideMinor', 1 );
+ $form .= Xml::closeElement( 'form' );
+ $wgOut->addHtml( $form );
+
+ # If there's nothing to show, stop here
+ if( $numRows == 0 ) {
+ $wgOut->addWikiMsg( 'watchnochange' );
+ return;
+ }
+
+ /* End bottom header */
+
+ /* Do link batch query */
+ $linkBatch = new LinkBatch;
+ while ( $row = $dbr->fetchObject( $res ) ) {
+ $userNameUnderscored = str_replace( ' ', '_', $row->rc_user_text );
+ if ( $row->rc_user != 0 ) {
+ $linkBatch->add( NS_USER, $userNameUnderscored );
+ }
+ $linkBatch->add( NS_USER_TALK, $userNameUnderscored );
+ }
+ $linkBatch->execute();
+ $dbr->dataSeek( $res, 0 );
+
+ $list = ChangesList::newFromUser( $wgUser );
+
+ $s = $list->beginRecentChangesList();
+ $counter = 1;
+ while ( $obj = $dbr->fetchObject( $res ) ) {
+ # Make RC entry
+ $rc = RecentChange::newFromRow( $obj );
+ $rc->counter = $counter++;
+
+ if ( $wgShowUpdatedMarker ) {
+ $updated = $obj->wl_notificationtimestamp;
+ } else {
+ $updated = false;
+ }
+
+ if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
+ $rc->numberofWatchingusers = $dbr->selectField( 'watchlist',
+ 'COUNT(*)',
+ array(
+ 'wl_namespace' => $obj->rc_namespace,
+ 'wl_title' => $obj->rc_title,
+ ),
+ __METHOD__ );
+ } else {
+ $rc->numberofWatchingusers = 0;
+ }
+
+ $s .= $list->recentChangesLine( $rc, $updated );
+ }
+ $s .= $list->endRecentChangesList();
+
+ $dbr->freeResult( $res );
+ $wgOut->addHTML( $s );
+
+}
+
+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 ) );
+ 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 ) );
+ return $s;
+}
+
+/**
+ * Returns html
+ */
+function wlCutoffLinks( $days, $page = 'Watchlist', $options = array() ) {
+ $hours = array( 1, 2, 6, 12 );
+ $days = array( 1, 3, 7 );
+ $i = 0;
+ foreach( $hours as $h ) {
+ $hours[$i++] = wlHoursLink( $h, $page, $options );
+ }
+ $i = 0;
+ foreach( $days as $d ) {
+ $days[$i++] = wlDaysLink( $d, $page, $options );
+ }
+ return wfMsgExt('wlshowlast',
+ array('parseinline', 'replaceafter'),
+ implode(' | ', $hours),
+ implode(' | ', $days),
+ wlDaysLink( 0, $page, $options ) );
+}
+
+/**
+ * Count the number of items on a user's watchlist
+ *
+ * @param $talk Include talk pages
+ * @return integer
+ */
+function wlCountItems( &$user, $talk = true ) {
+ $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
+
+ # Fetch the raw count
+ $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->mId ), 'wlCountItems' );
+ $row = $dbr->fetchObject( $res );
+ $count = $row->count;
+ $dbr->freeResult( $res );
+
+ # Halve to remove talk pages if needed
+ if( !$talk )
+ $count = floor( $count / 2 );
+
+ return( $count );
+}
diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php
new file mode 100644
index 00000000..3502e33c
--- /dev/null
+++ b/includes/specials/SpecialWhatlinkshere.php
@@ -0,0 +1,408 @@
+<?php
+/**
+ * @todo Use some variant of Pager or something; the pagination here is lousy.
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * 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;
+
+ // Stored objects
+ protected $opts, $target, $selfTitle;
+
+ // Stored globals
+ protected $skin, $request;
+
+ protected $limits = array( 20, 50, 100, 250, 500 );
+
+ function WhatLinksHerePage( $request, $par = null ) {
+ global $wgUser;
+ $this->request = $request;
+ $this->skin = $wgUser->getSkin();
+ $this->par = $par;
+ }
+
+ function execute() {
+ global $wgOut;
+
+ $opts = new FormOptions();
+
+ $opts->add( 'target', '' );
+ $opts->add( 'namespace', '', FormOptions::INTNULL );
+ $opts->add( 'limit', 50 );
+ $opts->add( 'from', 0 );
+ $opts->add( 'back', 0 );
+ $opts->add( 'hideredirs', false );
+ $opts->add( 'hidetrans', false );
+ $opts->add( 'hidelinks', false );
+ $opts->add( 'hideimages', false );
+
+ $opts->fetchValuesFromRequest( $this->request );
+ $opts->validateIntBounds( 'limit', 0, 5000 );
+
+ // Give precedence to subpage syntax
+ if ( isset($this->par) ) {
+ $opts->setValue( 'target', $this->par );
+ }
+
+ // Bind to member variable
+ $this->opts = $opts;
+
+ $this->target = Title::newFromURL( $opts->getValue( 'target' ) );
+ if( !$this->target ) {
+ $wgOut->addHTML( $this->whatlinkshereForm() );
+ return;
+ }
+
+ $this->selfTitle = SpecialPage::getTitleFor( 'Whatlinkshere', $this->target->getPrefixedDBkey() );
+
+ $wgOut->setPageTitle( wfMsg( 'whatlinkshere-title', $this->target->getPrefixedText() ) );
+ $wgOut->setSubtitle( wfMsgHtml( 'linklistsub' ) );
+
+ $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' ' .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n");
+
+ $this->showIndirectLinks( 0, $this->target, $opts->getValue( 'limit' ),
+ $opts->getValue( 'from' ), $opts->getValue( 'back' ) );
+ }
+
+ /**
+ * @param $level int Recursion level
+ * @param $target Title Target title
+ * @param $limit int Number of entries to display
+ * @param $from Title Display from this article ID
+ * @param $back Title Display from this article ID at backwards scrolling
+ * @private
+ */
+ function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
+ global $wgOut, $wgMaxRedirectLinksRetrieved;
+ $dbr = wfGetDB( DB_SLAVE );
+ $options = array();
+
+ $hidelinks = $this->opts->getValue( 'hidelinks' );
+ $hideredirs = $this->opts->getValue( 'hideredirs' );
+ $hidetrans = $this->opts->getValue( 'hidetrans' );
+ $hideimages = $target->getNamespace() != NS_IMAGE || $this->opts->getValue( 'hideimages' );
+
+ $fetchlinks = (!$hidelinks || !$hideredirs);
+
+ // Make the query
+ $plConds = array(
+ 'page_id=pl_from',
+ 'pl_namespace' => $target->getNamespace(),
+ 'pl_title' => $target->getDBkey(),
+ );
+ if( $hideredirs ) {
+ $plConds['page_is_redirect'] = 0;
+ } elseif( $hidelinks ) {
+ $plConds['page_is_redirect'] = 1;
+ }
+
+ $tlConds = array(
+ 'page_id=tl_from',
+ 'tl_namespace' => $target->getNamespace(),
+ 'tl_title' => $target->getDBkey(),
+ );
+
+ $ilConds = array(
+ 'page_id=il_from',
+ 'il_to' => $target->getDBkey(),
+ );
+
+ $namespace = $this->opts->getValue( 'namespace' );
+ if ( is_int($namespace) ) {
+ $plConds['page_namespace'] = $namespace;
+ $tlConds['page_namespace'] = $namespace;
+ $ilConds['page_namespace'] = $namespace;
+ }
+
+ if ( $from ) {
+ $tlConds[] = "tl_from >= $from";
+ $plConds[] = "pl_from >= $from";
+ $ilConds[] = "il_from >= $from";
+ }
+
+ // Read an extra row as an at-end check
+ $queryLimit = $limit + 1;
+
+ // Enforce join order, sometimes namespace selector may
+ // trigger filesorts which are far less efficient than scanning many entries
+ $options[] = 'STRAIGHT_JOIN';
+
+ $options['LIMIT'] = $queryLimit;
+ $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' );
+
+ if( $fetchlinks ) {
+ $options['ORDER BY'] = 'pl_from';
+ $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields,
+ $plConds, __METHOD__, $options );
+ }
+
+ if( !$hidetrans ) {
+ $options['ORDER BY'] = 'tl_from';
+ $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields,
+ $tlConds, __METHOD__, $options );
+ }
+
+ if( !$hideimages ) {
+ $options['ORDER BY'] = 'il_from';
+ $ilRes = $dbr->select( array( 'imagelinks', 'page' ), $fields,
+ $ilConds, __METHOD__, $options );
+ }
+
+ if( ( !$fetchlinks || !$dbr->numRows($plRes) ) && ( $hidetrans || !$dbr->numRows($tlRes) ) && ( $hideimages || !$dbr->numRows($ilRes) ) ) {
+ if ( 0 == $level ) {
+ $wgOut->addHTML( $this->whatlinkshereForm() );
+ $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere';
+ $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
+ // Show filters only if there are links
+ if( $hidelinks || $hidetrans || $hideredirs || $hideimages )
+ $wgOut->addHTML( $this->getFilterPanel() );
+ }
+ return;
+ }
+
+ // Read the rows into an array and remove duplicates
+ // templatelinks comes second so that the templatelinks row overwrites the
+ // pagelinks row, so we get (inclusion) rather than nothing
+ if( $fetchlinks ) {
+ while ( $row = $dbr->fetchObject( $plRes ) ) {
+ $row->is_template = 0;
+ $row->is_image = 0;
+ $rows[$row->page_id] = $row;
+ }
+ $dbr->freeResult( $plRes );
+
+ }
+ if( !$hidetrans ) {
+ while ( $row = $dbr->fetchObject( $tlRes ) ) {
+ $row->is_template = 1;
+ $row->is_image = 0;
+ $rows[$row->page_id] = $row;
+ }
+ $dbr->freeResult( $tlRes );
+ }
+ if( !$hideimages ) {
+ while ( $row = $dbr->fetchObject( $ilRes ) ) {
+ $row->is_template = 0;
+ $row->is_image = 1;
+ $rows[$row->page_id] = $row;
+ }
+ $dbr->freeResult( $ilRes );
+ }
+
+ // Sort by key and then change the keys to 0-based indices
+ ksort( $rows );
+ $rows = array_values( $rows );
+
+ $numRows = count( $rows );
+
+ // Work out the start and end IDs, for prev/next links
+ if ( $numRows > $limit ) {
+ // More rows available after these ones
+ // Get the ID from the last row in the result set
+ $nextId = $rows[$limit]->page_id;
+ // Remove undisplayed rows
+ $rows = array_slice( $rows, 0, $limit );
+ } else {
+ // No more rows after
+ $nextId = false;
+ }
+ $prevId = $from;
+
+ if ( $level == 0 ) {
+ $wgOut->addHTML( $this->whatlinkshereForm() );
+ $wgOut->addHTML( $this->getFilterPanel() );
+ $wgOut->addWikiMsg( 'linkshere', $this->target->getPrefixedText() );
+
+ $prevnext = $this->getPrevNext( $prevId, $nextId );
+ $wgOut->addHTML( $prevnext );
+ }
+
+ $wgOut->addHTML( $this->listStart() );
+ foreach ( $rows as $row ) {
+ $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
+
+ if ( $row->page_is_redirect && $level < 2 ) {
+ $wgOut->addHTML( $this->listItem( $row, $nt, true ) );
+ $this->showIndirectLinks( $level + 1, $nt, $wgMaxRedirectLinksRetrieved );
+ $wgOut->addHTML( Xml::closeElement( 'li' ) );
+ } else {
+ $wgOut->addHTML( $this->listItem( $row, $nt ) );
+ }
+ }
+
+ $wgOut->addHTML( $this->listEnd() );
+
+ if( $level == 0 ) {
+ $wgOut->addHTML( $prevnext );
+ }
+ }
+
+ protected function listStart() {
+ return Xml::openElement( 'ul' );
+ }
+
+ protected function listItem( $row, $nt, $notClose = false ) {
+ # local message cache
+ static $msgcache = null;
+ if ( $msgcache === null ) {
+ static $msgs = array( 'isredirect', 'istemplate', 'semicolon-separator',
+ 'whatlinkshere-links', 'isimage' );
+ $msgcache = array();
+ foreach ( $msgs as $msg ) {
+ $msgcache[$msg] = wfMsgHtml( $msg );
+ }
+ }
+
+ $suppressRedirect = $row->page_is_redirect ? 'redirect=no' : '';
+ $link = $this->skin->makeKnownLinkObj( $nt, '', $suppressRedirect );
+
+ // Display properties (redirect or template)
+ $propsText = '';
+ $props = array();
+ if ( $row->page_is_redirect )
+ $props[] = $msgcache['isredirect'];
+ if ( $row->is_template )
+ $props[] = $msgcache['istemplate'];
+ if( $row->is_image )
+ $props[] = $msgcache['isimage'];
+
+ if ( count( $props ) ) {
+ $propsText = '(' . implode( $msgcache['semicolon-separator'], $props ) . ')';
+ }
+
+ # Space for utilities links, with a what-links-here link provided
+ $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] );
+ $wlh = Xml::wrapClass( "($wlhLink)", 'mw-whatlinkshere-tools' );
+
+ return $notClose ?
+ Xml::openElement( 'li' ) . "$link $propsText $wlh\n" :
+ Xml::tags( 'li', null, "$link $propsText $wlh" ) . "\n";
+ }
+
+ protected function listEnd() {
+ return Xml::closeElement( 'ul' );
+ }
+
+ protected function wlhLink( Title $target, $text ) {
+ static $title = null;
+ if ( $title === null )
+ $title = SpecialPage::getTitleFor( 'Whatlinkshere' );
+
+ $targetText = $target->getPrefixedUrl();
+ return $this->skin->makeKnownLinkObj( $title, $text, 'target=' . $targetText );
+ }
+
+ function makeSelfLink( $text, $query ) {
+ return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query );
+ }
+
+ function getPrevNext( $prevId, $nextId ) {
+ global $wgLang;
+ $currentLimit = $this->opts->getValue( 'limit' );
+ $fmtLimit = $wgLang->formatNum( $currentLimit );
+ $prev = wfMsgExt( 'whatlinkshere-prev', array( 'parsemag', 'escape' ), $fmtLimit );
+ $next = wfMsgExt( 'whatlinkshere-next', array( 'parsemag', 'escape' ), $fmtLimit );
+
+ $changed = $this->opts->getChangedValues();
+ unset($changed['target']); // Already in the request title
+
+ if ( 0 != $prevId ) {
+ $overrides = array( 'from' => $this->opts->getValue( 'back' ) );
+ $prev = $this->makeSelfLink( $prev, wfArrayToCGI( $overrides, $changed ) );
+ }
+ if ( 0 != $nextId ) {
+ $overrides = array( 'from' => $nextId, 'back' => $prevId );
+ $next = $this->makeSelfLink( $next, wfArrayToCGI( $overrides, $changed ) );
+ }
+
+ $limitLinks = array();
+ foreach ( $this->limits as $limit ) {
+ $prettyLimit = $wgLang->formatNum( $limit );
+ $overrides = array( 'limit' => $limit );
+ $limitLinks[] = $this->makeSelfLink( $prettyLimit, wfArrayToCGI( $overrides, $changed ) );
+ }
+
+ $nums = implode ( ' | ', $limitLinks );
+
+ return wfMsgHtml( 'viewprevnext', $prev, $next, $nums );
+ }
+
+ function whatlinkshereForm() {
+ global $wgScript, $wgTitle;
+
+ // We get nicer value from the title object
+ $this->opts->consumeValue( 'target' );
+ // Reset these for new requests
+ $this->opts->consumeValues( array( 'back', 'from' ) );
+
+ $target = $this->target ? $this->target->getPrefixedText() : '';
+ $namespace = $this->opts->consumeValue( 'namespace' );
+
+ # Build up the form
+ $f = Xml::openElement( 'form', array( 'action' => $wgScript ) );
+
+ # Values that should not be forgotten
+ $f .= Xml::hidden( 'title', $wgTitle->getPrefixedText() );
+ foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
+ $f .= Xml::hidden( $name, $value );
+ }
+
+ $f .= Xml::fieldset( wfMsg( 'whatlinkshere' ) );
+
+ # Target input
+ $f .= Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target',
+ 'mw-whatlinkshere-target', 40, $target );
+
+ $f .= ' ';
+
+ # Namespace selector
+ $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;' .
+ Xml::namespaceSelector( $namespace, '' );
+
+ # Submit
+ $f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+
+ # Close
+ $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
+
+ return $f;
+ }
+
+ function getFilterPanel() {
+ $show = wfMsgHtml( 'show' );
+ $hide = wfMsgHtml( 'hide' );
+
+ $changed = $this->opts->getChangedValues();
+ unset($changed['target']); // Already in the request title
+
+ $links = array();
+ $types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
+ if( $this->target->getNamespace() == NS_IMAGE )
+ $types[] = 'hideimages';
+ foreach( $types as $type ) {
+ $chosen = $this->opts->getValue( $type );
+ $msg = wfMsgHtml( "whatlinkshere-{$type}", $chosen ? $show : $hide );
+ $overrides = array( $type => !$chosen );
+ $links[] = $this->makeSelfLink( $msg, wfArrayToCGI( $overrides, $changed ) );
+ }
+ return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), implode( '&nbsp;|&nbsp;', $links ) );
+ }
+}
diff --git a/includes/specials/SpecialWithoutinterwiki.php b/includes/specials/SpecialWithoutinterwiki.php
new file mode 100644
index 00000000..2092e43b
--- /dev/null
+++ b/includes/specials/SpecialWithoutinterwiki.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page lists pages without language links
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class WithoutInterwikiPage extends PageQueryPage {
+ private $prefix = '';
+
+ function getName() {
+ return 'Withoutinterwiki';
+ }
+
+ function getPageHeader() {
+ global $wgScript, $wgMiserMode;
+
+ # Do not show useless input form if wiki is running in misermode
+ if( $wgMiserMode ) {
+ return '';
+ }
+
+ $prefix = $this->prefix;
+ $t = SpecialPage::getTitleFor( $this->getName() );
+
+ return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'withoutinterwiki-legend' ) ) .
+ Xml::hidden( 'title', $t->getPrefixedText() ) .
+ Xml::inputLabel( wfMsg( 'allpagesprefix' ), 'prefix', 'wiprefix', 20, $prefix ) . ' ' .
+ Xml::submitButton( wfMsg( 'withoutinterwiki-submit' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' );
+ }
+
+ function sortDescending() {
+ return false;
+ }
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ function getSQL() {
+ $dbr = wfGetDB( DB_SLAVE );
+ list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' );
+ $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : '';
+ return
+ "SELECT 'Withoutinterwiki' AS type,
+ page_namespace AS namespace,
+ page_title AS title,
+ page_title AS value
+ FROM $page
+ LEFT JOIN $langlinks
+ ON ll_from = page_id
+ WHERE ll_title IS NULL
+ AND page_namespace=" . NS_MAIN . "
+ AND page_is_redirect = 0
+ {$prefix}";
+ }
+
+ function setPrefix( $prefix = '' ) {
+ $this->prefix = $prefix;
+ }
+
+}
+
+function wfSpecialWithoutinterwiki() {
+ global $wgRequest, $wgContLang, $wgCapitalLinks;
+ list( $limit, $offset ) = wfCheckLimits();
+ if( $wgCapitalLinks ) {
+ $prefix = $wgContLang->ucfirst( $wgRequest->getVal( 'prefix' ) );
+ } else {
+ $prefix = $wgRequest->getVal( 'prefix' );
+ }
+ $wip = new WithoutInterwikiPage();
+ $wip->setPrefix( $prefix );
+ $wip->doQuery( $offset, $limit );
+}