diff options
Diffstat (limited to 'includes/specials')
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> </td> + <td class='mw-input'>" . + Xml::checkLabel( wfMsg( 'ipbanononly' ), + 'wpAnonOnly', 'wpAnonOnly', $this->BlockAnonOnly, + array( 'tabindex' => '6' ) ) . " + </td> + </tr> + <tr id='wpCreateAccountRow'> + <td> </td> + <td class='mw-input'>" . + Xml::checkLabel( wfMsg( 'ipbcreateaccount' ), + 'wpCreateAccount', 'wpCreateAccount', $this->BlockCreateAccount, + array( 'tabindex' => '7' ) ) . " + </td> + </tr> + <tr id='wpEnableAutoblockRow'> + <td> </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> </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> </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> </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'> </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 .= ' ' . 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 ) . ' '; + $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> </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 ) . + ' ' . + 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> </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> </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×tamp=$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> </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( + ' ', + Xml::element( 'span', array( 'class' => 'error' ), + wfMsgExt( 'badsiglength', 'parsemag', $wgLang->formatNum( $wgMaxSigChars ) ) ) + ); + } elseif( !empty( $this->mToggles['fancysig'] ) && + false === $wgParser->validateSig( $this->mNick ) ) { + $invalidSig = $this->tableRow( + ' ', + Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) ) + ); + } else { + $invalidSig = ''; + } + + $wgOut->addHTML( + $this->tableRow( + Xml::label( wfMsg( 'yournick' ), 'wpNick' ), + Xml::input( 'wpNick', 25, $this->mNick, + array( + 'id' => 'wpNick', + // Note: $wgMaxSigChars is enforced in Unicode characters, + // both on the backend and now in the browser. + // Badly-behaved requests may still try to submit + // an overlong string, however. + 'maxlength' => $wgMaxSigChars ) ) + ) . + $invalidSig . + $this->tableRow( ' ', $this->getToggle( 'fancysig' ) ) + ); + + 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> ' ); + $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() ) . " \n" . + $this->getNamespaceMenu( $namespace ) . " \n" . + $this->getTypeMenu( $type ) . " \n" . + $this->getLevelMenu( $level ) . " \n" . + "<br /><span style='white-space: nowrap'> " . + $this->getExpiryCheck( $indefOnly ) . " \n" . + $this->getSizeLimit( $sizetype, $size ) . " \n" . + "</span>" . + " " . 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' ) . ' ' + . 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 ) . + ' ' . + Xml::radioLabel( wfMsg('maximum-size'), 'sizetype', 'max', 'wpmax', $max ) . + ' ' . + Xml::input( 'size', 9, $size, array( 'id' => 'wpsize' ) ) . + ' ' . + 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 ) . ' ' . + 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 ) . ' ' . + 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 ) . " \n" . + $this->getNamespaceMenu( $namespace ) . " \n" . + // $this->getLevelMenu( $level ) . "<br/>\n" . + " " . 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' ) + . ' ' + . 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 ) . ' ' . + 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("< ".$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×tamp=" . $rev->getTimestamp() ); + $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'), + "target=$target&diff=prev×tamp=" . $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' ) ) . + " " . + $searchField . + " " . + $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() . + '×tamp=' . + 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> </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> </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() . "×tamp=$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()."×tamp=$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> </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 = ' '; + 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'> </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] = "<{$tags[$i]}>"; + $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' ) . ' '; + $form .= Xml::namespaceSelector( $nameSpace, '' ) . ' '; + $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' ) . ' ' . + 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( ' | ', $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 ); +} |